This commit is contained in:
Luiz Silva 2026-01-27 17:11:13 -03:00
parent 67dc4c465a
commit 92662a0b13
20 changed files with 1005 additions and 774 deletions

View file

@ -10,6 +10,7 @@ import { ArrowDown, ArrowUp, MoreVertical } from "lucide-vue-next";
import { codigosResposta } from "p-respostas";
import EliTabelaCaixaDeBusca from "./EliTabelaCaixaDeBusca.vue";
import EliTabelaPaginacao from "./EliTabelaPaginacao.vue";
import EliTabelaCelula from "./celulas/EliTabelaCelula.vue";
import type { EliTabelaConsulta } from "./types-eli-tabela";
export default defineComponent({
@ -131,15 +132,6 @@ export default defineComponent({
menuAberto.value = null;
}
function normalizarFilhos(filhos: unknown) {
// `VNodeChild` pode ser null/undefined/boolean.
// Para a assinatura de `h()`, normalizamos para string vazia.
if (filhos === null || filhos === undefined || filhos === false) {
return "";
}
return filhos as never;
}
function obterClasseAlinhamento(alinhamento?: string) {
if (alinhamento === "direita") {
return "eli-tabela__celula--direita";
@ -157,34 +149,23 @@ export default defineComponent({
return typeof largura === "number" ? `${largura}px` : String(largura);
}
function renderConteudoCelula(valor: unknown, coluna: (typeof props.tabela.colunas)[number]) {
const filhos = normalizarFilhos(valor);
const truncar = Boolean(coluna.truncar);
if (!truncar) {
return filhos;
function obterTooltipCelula(celula: unknown) {
if (!Array.isArray(celula)) {
return undefined;
}
// Só truncamos de forma segura quando o conteúdo é textual.
if (typeof filhos !== "string" && typeof filhos !== "number") {
return filhos;
const tipo = celula[0];
const dados = celula[1] as any;
if (tipo === "textoSimples") {
return typeof dados?.texto === "string" ? dados.texto : undefined;
}
const tooltip = String(filhos);
if (tipo === "numero") {
return typeof dados?.numero === "number" ? String(dados.numero) : undefined;
}
return h(
"span",
{
class: "eli-tabela__celula-conteudo",
style: coluna.largura_maxima
? {
maxWidth: obterMaxWidth(coluna.largura_maxima),
}
: undefined,
title: tooltip,
},
tooltip
);
return undefined;
}
function renderErro(mensagem: string) {
@ -557,19 +538,45 @@ export default defineComponent({
{ class: "eli-tabela__tbody" },
linhas.value.map((linha, i) => {
const celulas = colunas.map((coluna, j) =>
h(
"td",
{
class: [
"eli-tabela__td",
coluna.acao ? "eli-tabela__td--clicavel" : undefined,
obterClasseAlinhamento(coluna.alinhamento),
],
key: `${i}-${j}`,
onClick: coluna.acao ? () => coluna.acao?.() : undefined,
},
renderConteudoCelula(coluna.celula(linha as never), coluna)
)
(() => {
const celula = coluna.celula(linha as never);
const truncar = Boolean(coluna.truncar);
const tooltip = truncar ? obterTooltipCelula(celula) : undefined;
const conteudo = h(EliTabelaCelula, {
celula: celula as never,
});
const conteudoFinal = truncar
? h(
"span",
{
class: "eli-tabela__celula-conteudo",
style: coluna.largura_maxima
? {
maxWidth: obterMaxWidth(coluna.largura_maxima),
}
: undefined,
title: tooltip,
},
conteudo
)
: conteudo;
return h(
"td",
{
class: [
"eli-tabela__td",
coluna.acao ? "eli-tabela__td--clicavel" : undefined,
obterClasseAlinhamento(coluna.alinhamento),
],
key: `${i}-${j}`,
onClick: coluna.acao ? () => coluna.acao?.() : undefined,
},
conteudoFinal
);
})()
);
if (temAcoes) {

View file

@ -0,0 +1,39 @@
<template>
<!--
Os componentes de célula possuem props tipadas por `tipo`.
Aqui o TS do template não consegue inferir o narrowing do union do `dados`,
então normalizamos para `unknown` e deixamos a validação de runtime do Vue.
-->
<component :is="Componente" :dados="dadosParaComponente" />
</template>
<script lang="ts">
import type { Component } from "vue";
import { computed, defineComponent, PropType } from "vue";
import type { ComponenteCelula, TipoTabelaCelula, TiposTabelaCelulas } from "../types-eli-tabela";
import { registryTabelaCelulas } from "./registryTabelaCelulas";
export default defineComponent({
name: "EliTabelaCelula",
props: {
celula: {
// `ComponenteCelula` é uma tupla `readonly [tipo, dados]`.
type: Array as unknown as PropType<ComponenteCelula>,
required: true,
},
},
setup(props) {
const tipo = computed(() => props.celula[0] as TipoTabelaCelula);
const dados = computed(() => props.celula[1] as TiposTabelaCelulas[TipoTabelaCelula]);
// Observação: mantemos o registry tipado, mas o TS do template não consegue
// fazer narrowing do componente com base em `tipo`, então tipamos como `Component`.
const Componente = computed(() => registryTabelaCelulas[tipo.value] as unknown as Component);
const dadosParaComponente = computed(() => dados.value);
return { Componente, dadosParaComponente };
},
});
</script>

View file

@ -4,14 +4,14 @@
<script lang="ts">
import { defineComponent, PropType } from "vue"
import { tiposTabelaCelulas } from "./tiposTabelaCelulas";
import type { TiposTabelaCelulas } from "./tiposTabelaCelulas";
export default defineComponent({
name: "EliTabelaCelulaNumero",
components: {},
props: {
dados: {
type: Object as PropType<tiposTabelaCelulas['numero']>,
type: Object as PropType<TiposTabelaCelulas["numero"]>,
},
},
data() {

View file

@ -4,14 +4,14 @@
<script lang="ts">
import { defineComponent, PropType } from "vue"
import { tiposTabelaCelulas } from "./tiposTabelaCelulas";
import type { TiposTabelaCelulas } from "./tiposTabelaCelulas";
export default defineComponent({
name: "EliTabelaCelulaTextoSimples",
components: {},
props: {
dados: {
type: Object as PropType<tiposTabelaCelulas['textoSimples']>,
type: Object as PropType<TiposTabelaCelulas["textoSimples"]>,
},
},
data() {

View file

@ -0,0 +1,10 @@
import type { Component } from "vue";
import EliTabelaCelulaTextoSimples from "./EliTabelaCelulaTextoSimples.vue";
import EliTabelaCelulaNumero from "./EliTabelaCelulaNumero.vue";
import type { TipoTabelaCelula } from "./tiposTabelaCelulas";
export const registryTabelaCelulas = {
textoSimples: EliTabelaCelulaTextoSimples,
numero: EliTabelaCelulaNumero,
} as const satisfies Record<TipoTabelaCelula, Component>;

View file

@ -2,14 +2,15 @@
* Tipagem dos dados de entrada dos componentes de celulas
*/
export type tiposTabelaCelulas = {
"textoSimples": {
texto: string,
acao?: ()=>{}
}
export type TiposTabelaCelulas = {
textoSimples: {
texto: string;
acao?: () => void;
};
numero: {
numero: number;
acao?: () => void;
};
};
"numero": {
numero: number,
acao?: ()=>{}
}
}
export type TipoTabelaCelula = keyof TiposTabelaCelulas;

View file

@ -1,4 +1,7 @@
export { default as EliTabela } from "./EliTabela.vue";
export * from "./types-eli-tabela";
export * from "./celulas/EliCelulaTextoSimples";
export * from "./celulas/tiposTabelaCelulas";
// Helper para construção de células tipadas.
export { celulaTabela } from "./types-eli-tabela";

View file

@ -1,25 +1,27 @@
import type { tipoResposta } from "p-respostas";
import type { LucideIcon } from "lucide-vue-next";
import { tiposTabelaCelulas } from "./celulas/tiposTabelaCelulas";
import type { TipoTabelaCelula, TiposTabelaCelulas } from "./celulas/tiposTabelaCelulas";
export type ComponenteCelulaBase<T extends keyof tiposTabelaCelulas> =
readonly [T, tiposTabelaCelulas[T]]
export type ComponenteCelulaBase<T extends TipoTabelaCelula> =
readonly [T, TiposTabelaCelulas[T]]
export type ComponenteCelula = {
[K in keyof tiposTabelaCelulas]: ComponenteCelulaBase<K>
}[keyof tiposTabelaCelulas]
[K in TipoTabelaCelula]: ComponenteCelulaBase<K>
}[TipoTabelaCelula]
export const celulaTabela = <T extends keyof tiposTabelaCelulas>(
export const celulaTabela = <T extends TipoTabelaCelula>(
tipo: T,
dados: tiposTabelaCelulas[T],
dados: TiposTabelaCelulas[T],
): ComponenteCelulaBase<T> => {
return [tipo, dados] as const
}
export type { TipoTabelaCelula, TiposTabelaCelulas };
export type EliAlinhamentoColuna = "esquerda" | "centro" | "direita";