This commit is contained in:
Luiz Silva 2026-01-27 13:48:54 -03:00
parent 4414eb0be6
commit df798df8d7
4 changed files with 213 additions and 14 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 EliTabelaCaixaDeBusca from "./EliTabelaCaixaDeBusca.vue";
import EliTabelaPaginacao from "./EliTabelaPaginacao.vue";
import type { EliTabelaConsulta } from "./types-eli-tabela";
@ -31,9 +32,11 @@ export default defineComponent({
const acoesVisiveis = ref<boolean[][]>([]);
const menuAberto = ref<number | null>(null);
const menuElementos = new Map<number, HTMLElement>();
const valorBusca = ref<string>("");
const paginaAtual = ref(1);
const colunaOrdenacao = ref<string | null>(null);
const direcaoOrdenacao = ref<"asc" | "desc">("asc");
const exibirBusca = computed(() => Boolean(props.tabela.mostrarCaixaDeBusca));
const registrosPorConsulta = computed(() => {
const valor = props.tabela.registros_por_consulta;
if (typeof valor === "number" && valor > 0) {
@ -94,6 +97,18 @@ export default defineComponent({
}
}
function atualizarBusca(texto: string) {
if (valorBusca.value === texto) {
return;
}
valorBusca.value = texto;
if (paginaAtual.value !== 1) {
paginaAtual.value = 1;
} else {
void carregar();
}
}
function irParaPagina(pagina: number) {
const alvo = Math.min(Math.max(1, pagina), totalPaginas.value);
if (alvo !== paginaAtual.value) {
@ -174,11 +189,16 @@ export default defineComponent({
direcao_ordem?: "asc" | "desc";
offSet: number;
limit: number;
texto_busca?: string;
} = {
offSet: offset,
limit: limite,
};
if (valorBusca.value) {
parametrosConsulta.texto_busca = valorBusca.value;
}
if (colunaOrdenacao.value) {
parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never;
parametrosConsulta.direcao_ordem = direcaoOrdenacao.value;
@ -215,7 +235,7 @@ export default defineComponent({
return;
}
const acoes = tabelaConfig.acoes ?? [];
const acoes = tabelaConfig.acoesLinha ?? [];
if (!acoes.length) {
acoesVisiveis.value = [];
@ -284,6 +304,20 @@ export default defineComponent({
void carregar();
});
watch(
() => props.tabela.mostrarCaixaDeBusca,
(mostrar) => {
if (!mostrar && valorBusca.value) {
valorBusca.value = "";
if (paginaAtual.value !== 1) {
paginaAtual.value = 1;
} else {
void carregar();
}
}
}
);
watch(paginaAtual, (nova, antiga) => {
if (nova !== antiga) {
void carregar();
@ -301,6 +335,7 @@ export default defineComponent({
menuElementos.clear();
colunaOrdenacao.value = null;
direcaoOrdenacao.value = "asc";
valorBusca.value = "";
if (paginaAtual.value !== 1) {
paginaAtual.value = 1;
} else {
@ -337,7 +372,7 @@ export default defineComponent({
}
const colunas = tabela.colunas;
const acoes = tabela.acoes ?? [];
const acoes = tabela.acoesLinha ?? [];
const temAcoes = acoes.length > 0;
if (!linhas.value.length) {
@ -408,7 +443,18 @@ export default defineComponent({
);
}
const conteudoTabela = [
const conteudoTabela: ReturnType<typeof h>[] = [];
if (exibirBusca.value) {
conteudoTabela.push(
h(EliTabelaCaixaDeBusca, {
modelo: valorBusca.value,
onBuscar: atualizarBusca,
})
);
}
conteudoTabela.push(
h("table", { class: "eli-tabela__table" }, [
h(
"thead",
@ -583,8 +629,8 @@ export default defineComponent({
);
})
),
]),
];
])
);
if (totalPaginas.value > 1 && quantidade.value > 0) {
conteudoTabela.push(

View file

@ -0,0 +1,116 @@
<template>
<div class="eli-tabela__busca">
<div class="eli-tabela__busca-input-wrapper">
<input
id="eli-tabela-busca"
v-model="texto"
type="search"
class="eli-tabela__busca-input"
placeholder="Digite termos para filtrar"
@keyup.enter="emitirBusca"
/>
<button
type="button"
class="eli-tabela__busca-botao"
@click="emitirBusca"
>
Buscar
</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
export default defineComponent({
name: "EliTabelaCaixaDeBusca",
props: {
modelo: {
type: String,
required: false,
default: "",
},
},
emits: {
buscar(valor: string) {
return typeof valor === "string";
},
},
setup(props, { emit }) {
/**
* Estado local da entrada para que o usuário possa digitar livremente antes
* de disparar uma nova consulta.
*/
const texto = ref(props.modelo ?? "");
watch(
() => props.modelo,
(novo) => {
if (novo !== undefined && novo !== texto.value) {
texto.value = novo;
}
}
);
function emitirBusca() {
emit("buscar", texto.value.trim());
}
return { texto, emitirBusca };
},
});
</script>
<style scoped>
.eli-tabela__busca {
display: flex;
align-items: flex-end;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.eli-tabela__busca-input-wrapper {
display: inline-flex;
align-items: stretch;
border-radius: 9999px;
border: 1px solid rgba(15, 23, 42, 0.15);
overflow: hidden;
background: #fff;
}
.eli-tabela__busca-input {
padding: 6px 12px;
border: none;
outline: none;
font-size: 0.875rem;
color: rgba(15, 23, 42, 0.85);
}
.eli-tabela__busca-input::placeholder {
color: rgba(107, 114, 128, 0.85);
}
.eli-tabela__busca-botao {
border: none;
background: rgba(37, 99, 235, 0.12);
color: rgba(37, 99, 235, 0.95);
padding: 0 12px;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.eli-tabela__busca-botao:hover,
.eli-tabela__busca-botao:focus-visible {
background: rgba(37, 99, 235, 0.2);
color: rgba(37, 99, 235, 1);
}
.eli-tabela__busca-botao:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.35);
outline-offset: 2px;
}
</style>

View file

@ -46,8 +46,11 @@ export type EliTabelaAcao<T> = {
*
* - `colunas`: definição de colunas e como renderizar cada célula
* - `consulta`: função que recupera os dados, com suporte a ordenação/paginação
* - `mostrarCaixaDeBusca`: habilita um campo de busca textual no cabeçalho
*/
export type EliTabelaConsulta<T> = {
/** Indica se a caixa de busca deve ser exibida acima da tabela. */
mostrarCaixaDeBusca?: boolean;
/** Lista de colunas da tabela. */
colunas: EliColuna<T>[];
/** Quantidade de registros solicitados por consulta (padrão `10`). */
@ -61,11 +64,28 @@ export type EliTabelaConsulta<T> = {
direcao_ordem?: "asc" | "desc";
offSet?: number;
limit?: number;
/** Texto digitado na caixa de busca, quando habilitada. */
texto_busca?: string;
}) => Promise<tipoResposta<EliConsultaPaginada<T>>>;
/** Quantidade máxima de botões exibidos na paginação (padrão `7`). */
maximo_botoes_paginacao?: number;
/** Mensagem exibida quando a consulta retorna ok porém sem dados. */
mensagemVazio?: string;
/** Ações exibidas à direita de cada linha. */
acoes?: EliTabelaAcao<T>[];
acoesLinha?: EliTabelaAcao<T>[];
/** configurações do botões que serão inseridos a direta da caixa de busca, seu uso mais como será para criar novo regitros, mas poderá ter outras utilidades */
acoesTabela?: {
/** Ícone (Lucide) exibido no botão */
icone?: LucideIcon;
/** Cor aplicada ao botão. */
cor?: string;
/** Texto descritivo da ação. */
rotulo: string;
/** Função executada ao clicar no botão. */
acao: () => void;
}[];
};

View file

@ -29,7 +29,7 @@ export default defineComponent({
name: "TabelaPlayground",
components: { EliTabela },
setup() {
const acoesTabela: EliTabelaConsulta<Linha>["acoes"] = [
const acoesTabela: EliTabelaConsulta<Linha>["acoesLinha"] = [
{
icone: Eye,
cor: "#2563eb",
@ -200,19 +200,33 @@ export default defineComponent({
];
const filtrarPorBusca = (linhas: Linha[], texto?: string) => {
const termo = texto?.trim().toLowerCase();
if (!termo) {
return [...linhas];
}
return linhas.filter((linha) => {
const campos = [linha.empreendedor, linha.empreendimento];
return campos.some((valor) => valor.toLowerCase().includes(termo));
});
};
const ordenarLinhas = (
linhas: Linha[],
parametros?: { coluna_ordem?: keyof Linha; direcao_ordem?: "asc" | "desc" }
parametros?: { coluna_ordem?: keyof Linha; direcao_ordem?: "asc" | "desc"; texto_busca?: string }
) => {
const listaFiltrada = filtrarPorBusca(linhas, parametros?.texto_busca);
if (!parametros?.coluna_ordem) {
return [...linhas];
return listaFiltrada;
}
const direcao = parametros.direcao_ordem ?? "asc";
const chave = parametros.coluna_ordem;
const multiplicador = direcao === "asc" ? 1 : -1;
return [...linhas].sort((a, b) => {
return [...listaFiltrada].sort((a, b) => {
const valorA = a[chave];
const valorB = b[chave];
@ -241,6 +255,7 @@ export default defineComponent({
const tabelaOk: EliTabelaConsulta<Linha> = {
registros_por_consulta: 10,
mostrarCaixaDeBusca: true,
colunas: [
{
rotulo: "Empreendedor",
@ -272,7 +287,7 @@ export default defineComponent({
coluna_ordem: "telefone",
},
],
acoes: acoesTabela,
acoesLinha: acoesTabela,
consulta: async (parametrosConsulta) => {
const ordenadas = ordenarLinhas(linhasPadrao, parametrosConsulta);
const valores = aplicarPaginacao(ordenadas, parametrosConsulta);
@ -283,7 +298,7 @@ export default defineComponent({
eErro: false,
mensagem: undefined,
valor: {
quantidade: linhasPadrao.length,
quantidade: ordenadas.length,
valores,
},
};
@ -292,6 +307,7 @@ export default defineComponent({
const tabelaVazia: EliTabelaConsulta<Linha> = {
registros_por_consulta: tabelaOk.registros_por_consulta,
mostrarCaixaDeBusca: tabelaOk.mostrarCaixaDeBusca,
colunas: tabelaOk.colunas,
consulta: async (_parametrosConsulta) => {
return {
@ -306,13 +322,14 @@ export default defineComponent({
};
},
mensagemVazio: "Nada para mostrar aqui.",
acoes: acoesTabela,
acoesLinha: acoesTabela,
};
const tabelaErro: EliTabelaConsulta<Linha> = {
registros_por_consulta: tabelaOk.registros_por_consulta,
mostrarCaixaDeBusca: tabelaOk.mostrarCaixaDeBusca,
colunas: tabelaOk.colunas,
acoes: acoesTabela,
acoesLinha: acoesTabela,
consulta: async (_parametrosConsulta) => {
return {
cod: codigosResposta.erroConhecido,