This commit is contained in:
Luiz Silva 2026-01-27 13:03:42 -03:00
parent 052337b9da
commit c4a0d31686
3 changed files with 569 additions and 194 deletions

View file

@ -4,7 +4,7 @@
</template>
<script lang="ts">
import { defineComponent, h, onBeforeUnmount, onMounted, PropType, ref, watch } from "vue";
import { computed, defineComponent, h, onBeforeUnmount, onMounted, PropType, ref, watch } from "vue";
import type { ComponentPublicInstance } from "vue";
import { ArrowDown, ArrowUp, MoreVertical } from "lucide-vue-next";
import { codigosResposta } from "p-respostas";
@ -30,8 +30,29 @@ export default defineComponent({
const acoesVisiveis = ref<boolean[][]>([]);
const menuAberto = ref<number | null>(null);
const menuElementos = new Map<number, HTMLElement>();
const paginaAtual = ref(1);
const colunaOrdenacao = ref<string | null>(null);
const direcaoOrdenacao = ref<"asc" | "desc">("asc");
const registrosPorConsulta = computed(() => {
const valor = props.tabela.registros_por_consulta;
if (typeof valor === "number" && valor > 0) {
return Math.floor(valor);
}
return 10;
});
const totalPaginas = computed(() => {
const limite = registrosPorConsulta.value;
if (!limite || limite <= 0) {
return 1;
}
const total = quantidade.value;
if (!total) {
return 1;
}
return Math.max(1, Math.ceil(total / limite));
});
let carregamentoSequencial = 0;
function registrarMenuElemento(indice: number, elemento: HTMLElement | null) {
@ -60,12 +81,91 @@ export default defineComponent({
if (colunaOrdenacao.value === chave) {
direcaoOrdenacao.value =
direcaoOrdenacao.value === "asc" ? "desc" : "asc";
void carregar();
} else {
colunaOrdenacao.value = chave;
direcaoOrdenacao.value = "asc";
if (paginaAtual.value !== 1) {
paginaAtual.value = 1;
} else {
void carregar();
}
}
}
void carregar();
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) {
paginaAtual.value = alvo;
}
}
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) {
@ -133,16 +233,27 @@ export default defineComponent({
menuAberto.value = null;
menuElementos.clear();
const parametrosOrdenacao = colunaOrdenacao.value
? {
coluna_ordem: colunaOrdenacao.value as never,
direcao_ordem: direcaoOrdenacao.value,
const limite = Math.max(1, registrosPorConsulta.value);
const offset = (paginaAtual.value - 1) * limite;
const parametrosConsulta: {
coluna_ordem?: never;
direcao_ordem?: "asc" | "desc";
offSet: number;
limit: number;
} = {
offSet: offset,
limit: limite,
};
if (colunaOrdenacao.value) {
parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never;
parametrosConsulta.direcao_ordem = direcaoOrdenacao.value;
}
: undefined;
try {
const tabelaConfig = props.tabela;
const res = await tabelaConfig.consulta(parametrosOrdenacao);
const res = await tabelaConfig.consulta(parametrosConsulta);
if (idCarregamento !== carregamentoSequencial) {
return;
@ -161,6 +272,16 @@ export default defineComponent({
linhas.value = valores;
quantidade.value = total;
const totalPaginasRecalculado = Math.max(
1,
Math.ceil((total || 0) / limite)
);
if (paginaAtual.value > totalPaginasRecalculado) {
paginaAtual.value = totalPaginasRecalculado;
return;
}
const acoes = tabelaConfig.acoes ?? [];
if (!acoes.length) {
@ -230,6 +351,12 @@ export default defineComponent({
void carregar();
});
watch(paginaAtual, (nova, antiga) => {
if (nova !== antiga) {
void carregar();
}
});
onBeforeUnmount(() => {
document.removeEventListener("click", handleClickFora);
menuElementos.clear();
@ -241,9 +368,23 @@ export default defineComponent({
menuElementos.clear();
colunaOrdenacao.value = null;
direcaoOrdenacao.value = "asc";
// Caso a definição de tabela/consulta mude
if (paginaAtual.value !== 1) {
paginaAtual.value = 1;
} else {
void carregar();
}
}
);
watch(
() => props.tabela.registros_por_consulta,
() => {
if (paginaAtual.value !== 1) {
paginaAtual.value = 1;
} else {
void carregar();
}
}
);
watch(linhas, () => {
@ -334,12 +475,11 @@ export default defineComponent({
);
}
return h(
"div",
{
class: "eli-tabela",
},
[
const podeVoltar = paginaAtual.value > 1;
const podeAvancar = paginaAtual.value < totalPaginas.value;
const botoesPaginacao = rangePaginacao();
const conteudoTabela = [
h("table", { class: "eli-tabela__table" }, [
h(
"thead",
@ -482,9 +622,7 @@ export default defineComponent({
)
: null;
const classesContainer = [
"eli-tabela__acoes-container",
];
const classesContainer = ["eli-tabela__acoes-container"];
if (estaAberto) {
classesContainer.push("eli-tabela__acoes-container--aberto");
@ -517,7 +655,85 @@ 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(
"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",
},
">>"
),
]
)
);
}
return h(
"div",
{
class: "eli-tabela",
},
conteudoTabela
);
};
},
@ -602,6 +818,66 @@ 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

@ -32,9 +32,12 @@ export type EliTabelaAcao<T> = {
*/
export type EliTabelaConsulta<T> = {
colunas: EliColuna<T>[];
registros_por_consulta?: number;
consulta: (parametrosConsulta?: {
coluna_ordem?: keyof T;
direcao_ordem?: "asc" | "desc";
offSet?: number;
limit?: number;
}) => Promise<tipoResposta<EliConsultaPaginada<T>>>;
/** Mensagem exibida quando a consulta retorna ok porém sem dados. */
mensagemVazio?: string;

View file

@ -50,6 +50,84 @@ export default defineComponent({
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
{ nome: "Ana", email: "ana@eli.com" },
{ nome: "Bruno", email: "bruno@eli.com" },
{ nome: "Carla", email: "carla@eli.com" },
];
const ordenarLinhas = (
@ -77,7 +155,22 @@ export default defineComponent({
});
};
const aplicarPaginacao = (
linhas: Linha[],
parametros?: { offSet?: number; limit?: number }
) => {
const offset = Math.max(0, parametros?.offSet ?? 0);
const limit = parametros?.limit ?? linhas.length;
if (limit === undefined || limit <= 0) {
return linhas.slice(offset);
}
return linhas.slice(offset, offset + limit);
};
const tabelaOk: EliTabelaConsulta<Linha> = {
registros_por_consulta: 2,
colunas: [
{
rotulo: "Nome",
@ -96,7 +189,8 @@ export default defineComponent({
],
acoes: acoesTabela,
consulta: async (parametrosConsulta) => {
const valores = ordenarLinhas(linhasPadrao, parametrosConsulta);
const ordenadas = ordenarLinhas(linhasPadrao, parametrosConsulta);
const valores = aplicarPaginacao(ordenadas, parametrosConsulta);
return {
cod: codigosResposta.sucesso,
@ -104,7 +198,7 @@ export default defineComponent({
eErro: false,
mensagem: undefined,
valor: {
quantidade: valores.length,
quantidade: linhasPadrao.length,
valores,
},
};
@ -112,8 +206,9 @@ export default defineComponent({
};
const tabelaVazia: EliTabelaConsulta<Linha> = {
registros_por_consulta: tabelaOk.registros_por_consulta,
colunas: tabelaOk.colunas,
consulta: async () => {
consulta: async (_parametrosConsulta) => {
return {
cod: codigosResposta.sucesso,
eCerto: true,
@ -130,9 +225,10 @@ export default defineComponent({
};
const tabelaErro: EliTabelaConsulta<Linha> = {
registros_por_consulta: tabelaOk.registros_por_consulta,
colunas: tabelaOk.colunas,
acoes: acoesTabela,
consulta: async () => {
consulta: async (_parametrosConsulta) => {
return {
cod: codigosResposta.erroConhecido,
eCerto: false,