This commit is contained in:
Luiz Silva 2026-01-27 13:14:13 -03:00
parent c4a0d31686
commit e1fec007b6
3 changed files with 253 additions and 197 deletions

View file

@ -8,6 +8,7 @@ import { computed, defineComponent, h, onBeforeUnmount, onMounted, PropType, ref
import type { ComponentPublicInstance } from "vue";
import { ArrowDown, ArrowUp, MoreVertical } from "lucide-vue-next";
import { codigosResposta } from "p-respostas";
import EliTabelaPaginacao from "./EliTabelaPaginacao.vue";
import type { EliTabelaConsulta } from "./types-eli-tabela";
export default defineComponent({
@ -93,18 +94,6 @@ export default defineComponent({
}
}
function paginaAnterior() {
if (paginaAtual.value > 1) {
paginaAtual.value -= 1;
}
}
function proximaPagina() {
if (paginaAtual.value < totalPaginas.value) {
paginaAtual.value += 1;
}
}
function irParaPagina(pagina: number) {
const alvo = Math.min(Math.max(1, pagina), totalPaginas.value);
if (alvo !== paginaAtual.value) {
@ -112,62 +101,6 @@ export default defineComponent({
}
}
function rangePaginacao(maximoBotoes = 7) {
const total = totalPaginas.value;
const atual = paginaAtual.value;
const resultado: Array<{ label: string; pagina?: number; ativo?: boolean; disabled?: boolean }> = [];
if (total <= maximoBotoes) {
for (let pagina = 1; pagina <= total; pagina += 1) {
resultado.push({
label: String(pagina),
pagina,
ativo: pagina === atual,
});
}
return resultado;
}
const adicionarPagina = (pagina: number) => {
resultado.push({
label: String(pagina),
pagina,
ativo: pagina === atual,
});
};
const adicionarEllipsis = () => {
resultado.push({ label: "…", disabled: true });
};
const visiveis = Math.max(3, maximoBotoes - 2);
let inicio = Math.max(2, atual - Math.floor(visiveis / 2));
let fim = inicio + visiveis - 1;
if (fim >= total) {
fim = total - 1;
inicio = fim - visiveis + 1;
}
adicionarPagina(1);
if (inicio > 2) {
adicionarEllipsis();
}
for (let pagina = inicio; pagina <= fim; pagina += 1) {
adicionarPagina(pagina);
}
if (fim < total - 1) {
adicionarEllipsis();
}
adicionarPagina(total);
return resultado;
}
function handleClickFora(evento: MouseEvent) {
if (menuAberto.value === null) {
return;
@ -475,10 +408,6 @@ export default defineComponent({
);
}
const podeVoltar = paginaAtual.value > 1;
const podeAvancar = paginaAtual.value < totalPaginas.value;
const botoesPaginacao = rangePaginacao();
const conteudoTabela = [
h("table", { class: "eli-tabela__table" }, [
h(
@ -659,72 +588,14 @@ export default defineComponent({
if (totalPaginas.value > 1 && quantidade.value > 0) {
conteudoTabela.push(
h(
"nav",
{
class: "eli-tabela__paginacao",
role: "navigation",
"aria-label": "Paginação de resultados",
h(EliTabelaPaginacao, {
pagina: paginaAtual.value,
totalPaginas: totalPaginas.value,
maximoBotoes: props.tabela.maximo_botoes_paginacao,
onAlterar: (pagina: number) => {
irParaPagina(pagina);
},
[
h(
"button",
{
type: "button",
class: "eli-tabela__pagina-botao",
onClick: paginaAnterior,
disabled: !podeVoltar,
"aria-label": "Página anterior",
},
"<<"
),
...botoesPaginacao.map((item, indice) =>
item.disabled || !item.pagina
? h(
"span",
{
key: `${item.label}-${indice}`,
class: "eli-tabela__pagina-ellipsis",
"aria-hidden": "true",
},
item.label
)
: h(
"button",
{
key: `${item.label}-${indice}`,
type: "button",
class: [
"eli-tabela__pagina-botao",
item.ativo
? "eli-tabela__pagina-botao--ativo"
: undefined,
],
disabled: item.ativo,
"aria-current": item.ativo ? "page" : undefined,
"aria-label": `Ir para página ${item.label}`,
onClick: () => {
if (item.pagina) {
irParaPagina(item.pagina);
}
},
},
item.label
)
),
h(
"button",
{
type: "button",
class: "eli-tabela__pagina-botao",
onClick: proximaPagina,
disabled: !podeAvancar,
"aria-label": "Próxima página",
},
">>"
),
]
)
})
);
}
@ -818,66 +689,6 @@ export default defineComponent({
opacity: 0;
}
.eli-tabela__paginacao {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 12px;
flex-wrap: wrap;
}
.eli-tabela__pagina-botao {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
border-radius: 9999px;
border: 1px solid rgba(15, 23, 42, 0.12);
background: #ffffff;
font-size: 0.875rem;
font-weight: 500;
color: rgba(15, 23, 42, 0.82);
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.eli-tabela__pagina-botao:hover,
.eli-tabela__pagina-botao:focus-visible {
background-color: rgba(37, 99, 235, 0.08);
border-color: rgba(37, 99, 235, 0.4);
color: rgba(37, 99, 235, 0.95);
}
.eli-tabela__pagina-botao:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.45);
outline-offset: 2px;
}
.eli-tabela__pagina-botao:disabled {
cursor: default;
opacity: 0.5;
background: rgba(148, 163, 184, 0.08);
border-color: rgba(148, 163, 184, 0.18);
color: rgba(71, 85, 105, 0.75);
}
.eli-tabela__pagina-botao--ativo {
background: rgba(37, 99, 235, 0.12);
border-color: rgba(37, 99, 235, 0.4);
color: rgba(37, 99, 235, 0.95);
}
.eli-tabela__pagina-ellipsis {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
color: rgba(107, 114, 128, 0.85);
font-size: 0.9rem;
}
.eli-tabela__tr:last-child .eli-tabela__td {
border-bottom: none;
}

View file

@ -0,0 +1,244 @@
<template>
<nav
v-if="totalPaginasExibidas > 1"
class="eli-tabela__paginacao"
role="navigation"
aria-label="Paginação de resultados"
>
<button
type="button"
class="eli-tabela__pagina-botao"
:disabled="anteriorDesabilitado"
aria-label="Página anterior"
@click="irParaPagina(paginaAtual - 1)"
>
<<
</button>
<template v-for="(item, index) in botoes" :key="`${item.label}-${index}`">
<span
v-if="item.ehEllipsis"
class="eli-tabela__pagina-ellipsis"
aria-hidden="true"
>
{{ item.label }}
</span>
<button
v-else
type="button"
class="eli-tabela__pagina-botao"
:class="item.ativo ? 'eli-tabela__pagina-botao--ativo' : undefined"
:disabled="item.ativo"
:aria-current="item.ativo ? 'page' : undefined"
:aria-label="`Ir para página ${item.label}`"
@click="irParaPagina(item.pagina)"
>
{{ item.label }}
</button>
</template>
<button
type="button"
class="eli-tabela__pagina-botao"
:disabled="proximaDesabilitada"
aria-label="Próxima página"
@click="irParaPagina(paginaAtual + 1)"
>
>>
</button>
</nav>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
export default defineComponent({
name: "EliTabelaPaginacao",
props: {
pagina: {
type: Number,
required: true,
},
totalPaginas: {
type: Number,
required: true,
},
maximoBotoes: {
type: Number,
required: false,
},
},
emits: {
alterar(pagina: number) {
return Number.isFinite(pagina);
},
},
setup(props, { emit }) {
/**
* Define o limite de botões visíveis. Mantemos um mínimo de 7 para garantir
* uma navegação confortável, mesmo quando o consumidor não informa o valor.
*/
const maximoBotoesVisiveis = computed(() => {
const valor = props.maximoBotoes;
if (typeof valor === "number" && valor >= 5) {
return Math.floor(valor);
}
return 7;
});
/**
* Constrói a lista de botões/reticências que serão exibidos na paginação.
* Mantemos sempre a primeira e a última página visíveis, posicionando as
* demais de forma dinâmica ao redor da página atual.
*/
const botoes = computed(() => {
const total = props.totalPaginas;
const atual = props.pagina;
const limite = maximoBotoesVisiveis.value;
const resultado: Array<{
label: string;
pagina?: number;
ativo?: boolean;
ehEllipsis?: boolean;
}> = [];
const adicionarPagina = (pagina: number) => {
resultado.push({
label: String(pagina),
pagina,
ativo: pagina === atual,
});
};
const adicionarReticencias = () => {
resultado.push({ label: "…", ehEllipsis: true });
};
if (total <= limite) {
for (let pagina = 1; pagina <= total; pagina += 1) {
adicionarPagina(pagina);
}
return resultado;
}
const visiveisCentrais = Math.max(3, limite - 2);
let inicio = Math.max(2, atual - Math.floor(visiveisCentrais / 2));
let fim = inicio + visiveisCentrais - 1;
if (fim >= total) {
fim = total - 1;
inicio = fim - visiveisCentrais + 1;
}
adicionarPagina(1);
if (inicio > 2) {
adicionarReticencias();
}
for (let pagina = inicio; pagina <= fim; pagina += 1) {
adicionarPagina(pagina);
}
if (fim < total - 1) {
adicionarReticencias();
}
adicionarPagina(total);
return resultado;
});
/**
* Emite a requisição de mudança de página garantindo que o valor esteja
* dentro dos limites válidos informados pelo componente pai.
*/
function irParaPagina(pagina: number | undefined) {
if (!pagina) {
return;
}
const alvo = Math.min(Math.max(1, pagina), props.totalPaginas);
if (alvo !== props.pagina) {
emit("alterar", alvo);
}
}
const anteriorDesabilitado = computed(() => props.pagina <= 1);
const proximaDesabilitada = computed(() => props.pagina >= props.totalPaginas);
const paginaAtual = computed(() => props.pagina);
const totalPaginasExibidas = computed(() => props.totalPaginas);
return {
botoes,
irParaPagina,
anteriorDesabilitado,
proximaDesabilitada,
paginaAtual,
totalPaginasExibidas,
};
},
});
</script>
<style scoped>
.eli-tabela__paginacao {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 12px;
flex-wrap: wrap;
}
.eli-tabela__pagina-botao {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
border-radius: 9999px;
border: 1px solid rgba(15, 23, 42, 0.12);
background: #ffffff;
font-size: 0.875rem;
font-weight: 500;
color: rgba(15, 23, 42, 0.82);
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease,
color 0.2s ease;
}
.eli-tabela__pagina-botao:hover,
.eli-tabela__pagina-botao:focus-visible {
background-color: rgba(37, 99, 235, 0.08);
border-color: rgba(37, 99, 235, 0.4);
color: rgba(37, 99, 235, 0.95);
}
.eli-tabela__pagina-botao:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.45);
outline-offset: 2px;
}
.eli-tabela__pagina-botao:disabled {
cursor: default;
opacity: 0.5;
background: rgba(148, 163, 184, 0.08);
border-color: rgba(148, 163, 184, 0.18);
color: rgba(71, 85, 105, 0.75);
}
.eli-tabela__pagina-botao--ativo {
background: rgba(37, 99, 235, 0.12);
border-color: rgba(37, 99, 235, 0.4);
color: rgba(37, 99, 235, 0.95);
}
.eli-tabela__pagina-ellipsis {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
color: rgba(107, 114, 128, 0.85);
font-size: 0.9rem;
}
</style>

View file

@ -39,6 +39,7 @@ export type EliTabelaConsulta<T> = {
offSet?: number;
limit?: number;
}) => Promise<tipoResposta<EliConsultaPaginada<T>>>;
maximo_botoes_paginacao?: number;
/** Mensagem exibida quando a consulta retorna ok porém sem dados. */
mensagemVazio?: string;
acoes?: EliTabelaAcao<T>[];