Tabla
Tabla basada en configuración de columnas. Admite ordenación, columnas fijas, filas alternadas, slots de celda y desplazamiento virtual para grandes volúmenes de datos (la ventana visible la calculan funciones puras de core, implementación propia sin librerías externas).
Uso básico
<script setup>
const cols = [
{ prop: 'name', label: 'Nombre', width: 140 },
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120 },
]
const rows = [
{ id: 1, name: 'Shen Yan', role: 'Arquitectura', city: 'Hangzhou' },
{ id: 2, name: 'Su Wan', role: 'Frontend', city: 'Suzhou' },
{ id: 3, name: 'Lu Li', role: 'Diseño', city: 'Pekín' },
]
</script>
<template>
<LhTable :data="rows" :columns="cols" />
</template><script>
export default {
data() {
return {
cols: [
{ prop: 'name', label: 'Nombre', width: 140 },
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120 },
],
rows: [
{ id: 1, name: 'Shen Yan', role: 'Arquitectura', city: 'Hangzhou' },
{ id: 2, name: 'Su Wan', role: 'Frontend', city: 'Suzhou' },
{ id: 3, name: 'Lu Li', role: 'Diseño', city: 'Pekín' },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="cols" />
</template>Ordenación y columnas fijas
<script setup>
const sortCols = [
{ prop: 'name', label: 'Nombre', width: 140, fixed: 'left' },
{ prop: 'age', label: 'Edad', width: 100, sortable: true, align: 'center' },
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120 },
]
</script>
<template>
<LhTable :data="rows" :columns="sortCols" stripe border size="small" />
</template><script>
export default {
data() {
return {
sortCols: [
{ prop: 'name', label: 'Nombre', width: 140, fixed: 'left' },
{ prop: 'age', label: 'Edad', width: 100, sortable: true, align: 'center' },
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120 },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="sortCols" stripe border size="small" />
</template>Desplazamiento virtual (grandes volúmenes de datos)
Basta con establecer height para activarlo: solo se renderizan las filas de la ventana visible; rowHeight debe coincidir con la altura real de las filas.
<script setup lang="ts">
const cols = [
{ prop: 'name', label: 'Nombre', width: 140 },
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120 },
]
const bigRows = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `Fila ${i + 1}`,
role: i % 2 === 0 ? 'Arquitectura' : 'Frontend',
city: 'Hangzhou',
}))
</script>
<template>
<LhTable :data="bigRows" :columns="cols" :height="240" :row-height="48" />
</template><script>
export default {
data() {
return {
cols: [
{ prop: 'name', label: 'Nombre', width: 140 },
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120 },
],
bigRows: Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `Fila ${i + 1}`,
role: i % 2 === 0 ? 'Arquitectura' : 'Frontend',
city: 'Hangzhou',
})),
}
},
}
</script>
<template>
<LhTable :data="bigRows" :columns="cols" :height="240" :row-height="48" />
</template>Celdas personalizadas
<script setup lang="ts">
const rows = [
{ id: 1, name: 'Shen Yan', age: 34, role: 'Arquitectura', city: 'Hangzhou' },
{ id: 2, name: 'Su Wan', age: 28, role: 'Frontend', city: 'Suzhou' },
{ id: 3, name: 'Lu Li', age: 31, role: 'Diseño', city: 'Pekín' },
]
const slotCols = [
{ prop: 'name', label: 'Nombre', width: 140 },
{ prop: 'role', label: 'Responsabilidad', slot: 'role' },
]
</script>
<template>
<!-- Slot personalizado para la columna de responsabilidad -->
<LhTable :data="rows" :columns="slotCols">
<template #role="{ value }">
<LhTag :type="value === 'Arquitectura' ? 'primary' : 'info'">{{ value }}</LhTag>
</template>
</LhTable>
</template><script>
export default {
data() {
return {
rows: [
{ id: 1, name: 'Shen Yan', age: 34, role: 'Arquitectura', city: 'Hangzhou' },
{ id: 2, name: 'Su Wan', age: 28, role: 'Frontend', city: 'Suzhou' },
{ id: 3, name: 'Lu Li', age: 31, role: 'Diseño', city: 'Pekín' },
],
slotCols: [
{ prop: 'name', label: 'Nombre', width: 140 },
{ prop: 'role', label: 'Responsabilidad', slot: 'role' },
],
}
},
}
</script>
<template>
<!-- Slot personalizado para la columna de responsabilidad -->
<LhTable :data="rows" :columns="slotCols">
<template #role="{ value }">
<LhTag :type="value === 'Arquitectura' ? 'primary' : 'info'">{{ value }}</LhTag>
</template>
</LhTable>
</template>Cabeceras multinivel
Anidando children en la configuración de columnas se obtienen cabeceras multinivel, con anidamiento ilimitado; el cuerpo de la tabla se alinea por las columnas hoja.
<script setup>
const nestedCols = [
{ prop: 'name', label: 'Nombre', width: 140, fixed: 'left' },
{
label: 'Información del proyecto',
children: [
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120 },
],
},
{ prop: 'age', label: 'Edad', sortable: true, align: 'center' },
]
</script>
<template>
<LhTable :data="rows" :columns="nestedCols" border />
</template><script>
export default {
data() {
return {
nestedCols: [
{ prop: 'name', label: 'Nombre', width: 140, fixed: 'left' },
{
label: 'Información del proyecto',
children: [
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120 },
],
},
{ prop: 'age', label: 'Edad', sortable: true, align: 'center' },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="nestedCols" border />
</template>Filtrado
Con filters en la configuración de la columna aparece el disparador de filtrado en la cabecera; tras marcar las opciones, "Confirmar" las aplica y "Restablecer" las limpia. filterMultiple: false activa la selección única, filterMethod define el criterio personalizado y el uso controlado se muestra en el ejemplo siguiente.
Filtrado actual: {}
<script setup>
import { ref } from 'vue'
const filteredValues = ref({})
const filterCols = [
{ prop: 'name', label: 'Nombre', width: 140 },
{
prop: 'role', label: 'Responsabilidad', filters: [
{ text: 'Arquitectura', value: 'Arquitectura' },
{ text: 'Frontend', value: 'Frontend' },
{ text: 'Diseño', value: 'Diseño' },
],
},
]
function onFilterChange({ prop, values }) {
console.log('filterChange', prop, values)
}
</script>
<template>
<!-- Filtrado controlado -->
<LhTable
:data="rows"
:columns="filterCols"
v-model:filtered-values="filteredValues"
@filter-change="onFilterChange"
/>
</template><script>
export default {
data() {
return {
filteredValues: {},
filterCols: [
{ prop: 'name', label: 'Nombre', width: 140 },
{
prop: 'role', label: 'Responsabilidad', filters: [
{ text: 'Arquitectura', value: 'Arquitectura' },
{ text: 'Frontend', value: 'Frontend' },
{ text: 'Diseño', value: 'Diseño' },
],
},
],
}
},
methods: {
onFilterChange({ prop, values }) {
console.log('filterChange', prop, values)
},
},
}
</script>
<template>
<!-- Filtrado controlado -->
<LhTable
:data="rows"
:columns="filterCols"
v-model:filtered-values="filteredValues"
@filter-change="onFilterChange"
/>
</template>Arrastre del ancho de columna
Activa resizable (interruptor general) y marca las columnas con resizable: true (columnas hoja con un width numérico). Aparece un asa de arrastre en el borde derecho de la cabecera; con el foco activo, ←/→ ajusta ±10px cada vez y el límite inferior del arrastre es 40px.
Último ajuste: (sin ajustar)
<script setup>
import { ref } from 'vue'
const resizeLog = ref('(sin ajustar)')
const resizeCols = [
{ prop: 'name', label: 'Nombre', width: 140, resizable: true, fixed: 'left' },
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120, resizable: true },
]
function onColumnResize({ prop, width }) {
resizeLog.value = `${prop} → ${width}px`
}
</script>
<template>
<!-- Arrastre del ancho de columna -->
<LhTable :data="rows" :columns="resizeCols" resizable border @column-resize="onColumnResize" />
</template><script>
export default {
data() {
return {
resizeLog: '(sin ajustar)',
resizeCols: [
{ prop: 'name', label: 'Nombre', width: 140, resizable: true, fixed: 'left' },
{ prop: 'role', label: 'Responsabilidad' },
{ prop: 'city', label: 'Ciudad', width: 120, resizable: true },
],
}
},
methods: {
onColumnResize({ prop, width }) {
this.resizeLog = `${prop} → ${width}px`
},
},
}
</script>
<template>
<!-- Arrastre del ancho de columna -->
<LhTable :data="rows" :columns="resizeCols" resizable border @column-resize="onColumnResize" />
</template>Clave de fila, altura máxima y ordenación por defecto
row-key especifica la clave única de la fila (recomendado); con max-height la cabecera queda fija y el cuerpo se desplaza; sorter es una función de ordenación personalizada; default-sort-prop / default-sort-order establecen la ordenación inicial y default-filtered-values el filtrado inicial.
<script setup>
const sorterCols = [
{ prop: 'name', label: 'Nombre', width: 140 },
{ prop: 'age', label: 'Edad', sortable: true, sorter: (a, b) => a.age - b.age },
{ prop: 'role', label: 'Responsabilidad' },
]
</script>
<template>
<LhTable
:data="rows"
:columns="sorterCols"
row-key="name"
max-height="200"
default-sort-prop="age"
default-sort-order="desc"
/>
</template><script>
export default {
data() {
return {
sorterCols: [
{ prop: 'name', label: 'Nombre', width: 140 },
{ prop: 'age', label: 'Edad', sortable: true, sorter: (a, b) => a.age - b.age },
{ prop: 'role', label: 'Responsabilidad' },
],
}
},
}
</script>
<template>
<LhTable
:data="rows"
:columns="sorterCols"
row-key="name"
max-height="200"
default-sort-prop="age"
default-sort-order="desc"
/>
</template>API
LhTable Props
| Prop | Descripción | Tipo | Predeterminado |
|---|---|---|---|
| data | Datos de las filas | [LhTableRow](#type-LhTableRow)[] | — |
| columns | Configuración de columnas | [LhTableColumn](#type-LhTableColumn)[] | — |
| height | Altura del cuerpo de la tabla (al establecerlo se activa el desplazamiento virtual; un número se interpreta como px) | string | number | — |
| maxHeight | Altura máxima en modo no virtual | string | number | — |
| rowHeight | Altura de las filas en el desplazamiento virtual (debe coincidir con la altura real) | number | [LH_TABLE_ROW_HEIGHT](#const-LH_TABLE_ROW_HEIGHT) |
| stripe | Filas alternadas | boolean | false |
| border | Bordes de las celdas | boolean | false |
| size | Tamaño | "small" | "default" | "large" | 'default' |
| rowKey | Nombre del campo clave de la fila | string | 'id' |
| defaultSortProp | Columna de ordenación por defecto | string | — |
| defaultSortOrder | Dirección de ordenación por defecto | "asc" | "desc" | — |
| sorter | Función de ordenación personalizada | (a: LhTableRow, b: LhTableRow) => number | — |
| resizable | Activa el arrastre del ancho de columna (necesita además el resizable de columna) | boolean | false |
| defaultFilteredValues | Estado de filtrado por defecto (no controlado; la clave es el prop de la columna) | { [x: string]: (string | number)[]; } | () => ({}) |
| filteredValues | Estado de filtrado controlado (la clave es el prop de la columna) | { [x: string]: (string | number)[]; } | — |
| sx | Props de maquetación rápida sx | LhSxProps | — |
Eventos
| Evento | Descripción | Parámetro |
|---|---|---|
| sortChange | Se dispara al cambiar la ordenación (al cambiar de columna se reinicia desde asc; en la misma columna se cicla asc → desc → cancelar) | (payload: { prop: string, order: 'asc' | 'desc' | undefined }) |
| rowClick | Se dispara al pulsar una fila | (row: LhTableRow, index: number) |
| filterChange | Se dispara tras enviar el filtrado con "Confirmar" o "Restablecer" | (payload: { prop: string, values: (string | number)[] }) |
| update:filteredValues | Se dispara al cambiar el estado de filtrado (v-model:filtered-values) | (values: Record<string, (string | number)[]>) |
| columnResize | Se dispara al terminar el ajuste del ancho de columna (fin del arrastre o ←/→ del teclado) | (payload: { prop: string, width: number }) |
Slots: empty, <column.slot> (parámetros de ámbito { row, column, index, value }).
LhTableColumn
| Campo | Descripción | Tipo |
|---|---|---|
| prop | Nombre del campo de datos | string |
| label | Título de la columna | string |
| width | Ancho de la columna (un número es px; si no se establece, se reparte a partes iguales) | number | string |
| align | Alineación | 'left' | 'center' | 'right' |
| fixed | Columna fija (necesita ir con width) | 'left' | 'right' |
| sortable | Ordenable | boolean |
| slot | Nombre del slot de celda personalizado | string |
| formatter | Formateo del valor | (row, value) => string |
| children | Columnas hijas (al configurarlas se obtienen cabeceras multinivel) | LhTableColumn[] |
| filters | Opciones de filtrado de la columna (solo afectan a las columnas hoja) | LhTableFilterOption[] |
| filterMethod | Criterio de filtrado personalizado (por defecto, comparación de igualdad del prop) | (value, row) => boolean |
| filterMultiple | Si el panel de filtrado es de selección múltiple | boolean |
| resizable | Si la columna se puede ensanchar arrastrando (debe ser una columna hoja con un width numérico) | boolean |
Accesibilidad
role="grid"+aria-rowcount/aria-colcount; las filas sonrow, las cabeceras de columnacolumnheadery las celdasgridcell- Las cabeceras de columna ordenables exponen
aria-sort(none/ascending/descending) y el disparador de ordenación es un<button>real - Las columnas fijas usan
position: stickyde CSS (sin cálculos de fijación propios) - El disparador de filtrado lleva
aria-haspopup+aria-expandedy el panel esrole="group" - El asa del ancho de columna es
role="separator", puede recibir el foco y se ajusta con ←/→ en pasos de ±10px
Definiciones de tipos
Las estructuras de tipos TypeScript que exporta este componente son las siguientes (extraídas automáticamente de packages/components/table/src/types.ts y del código fuente de @lh-ui/share/core/*; despliega cada entrada para ver la definición completa; la descripción de los atributos está en la tabla de Props de arriba):
LhTableProps
export interface LhTableProps {
/** Datos de las filas */
data: LhTableRow[]
/** Configuración de columnas */
columns: LhTableColumn[]
/** Altura del cuerpo de la tabla (al establecerlo se activa el desplazamiento virtual; un número es px) */
height?: number | string
/** Altura máxima en modo no virtual */
maxHeight?: number | string
/** Altura de las filas en el desplazamiento virtual (debe coincidir con la altura real) */
rowHeight?: number
/** Filas alternadas */
stripe?: boolean
/** Bordes de las celdas */
border?: boolean
/** Tamaño */
size?: 'small' | 'default' | 'large'
/** Nombre del campo clave de la fila */
rowKey?: string
/** Columna de ordenación por defecto */
defaultSortProp?: string
/** Dirección de ordenación por defecto */
defaultSortOrder?: LhTableSortOrder
/** Función de ordenación personalizada */
sorter?: (a: LhTableRow, b: LhTableRow) => number
/** Activa el arrastre del ancho de columna (necesita además el resizable de columna y que la columna tenga un width numérico) */
resizable?: boolean
/** Estado de filtrado por defecto (no controlado; la clave es el prop de la columna) */
defaultFilteredValues?: Record<string, (string | number)[]>
/** Estado de filtrado controlado (la clave es el prop de la columna) */
filteredValues?: Record<string, (string | number)[]>
/** Props de maquetación rápida sx */
sx?: LhSxProps
}LhTableSortOrder (de @lh-ui/share/core/table)
/** Dirección de ordenación */
export type LhTableSortOrder = 'asc' | 'desc'LhTableRow (de @lh-ui/share/core/table)
/** Tipo de los datos de las filas */
export type LhTableRow = Record<string, any>LhTableColumn (de @lh-ui/share/core/table)
/** Configuración de columnas */
export interface LhTableColumn {
/** Nombre del campo de datos */
prop: string
/** Título de la columna */
label: string
/** Ancho de la columna (un número es px) */
width?: number | string
/** Alineación */
align?: LhTableAlign
/** Columna fija (necesita ir con width) */
fixed?: 'left' | 'right'
/** Ordenable */
sortable?: boolean
/** Nombre del slot de celda personalizado */
slot?: string
/** Formateo del valor */
formatter?: (row: LhTableRow, value: any) => string
/** Columnas hijas (al configurarlas se obtienen cabeceras multinivel, con anidamiento ilimitado) */
children?: LhTableColumn[]
/** Opciones de filtrado de la columna (solo afectan a las columnas hoja) */
filters?: LhTableFilterOption[]
/** Criterio de filtrado personalizado (por defecto, comparación de igualdad del prop) */
filterMethod?: (value: string | number, row: LhTableRow) => boolean
/** Si el panel de filtrado es de selección múltiple (true por defecto) */
filterMultiple?: boolean
/** Si la columna se puede ensanchar arrastrando (debe ser una columna hoja con un width numérico) */
resizable?: boolean
}LhTableFilterOption (de @lh-ui/share/core/table)
/** Opción de filtrado */
export interface LhTableFilterOption {
/** Texto de la opción */
text: string
/** Valor de la opción (interviene en la comparación de filterMethod) */
value: string | number
}LhTableAlign (de @lh-ui/share/core/table)
/** Alineación de las celdas */
export type LhTableAlign = 'left' | 'center' | 'right'Constantes
Las constantes públicas que utiliza este componente son las siguientes (en la columna «Tipo / Predeterminado» de la tabla de Props puedes pulsar una entrada de igual nombre para saltar hasta aquí):
LH_TABLE_ROW_HEIGHT (de @lh-ui/share/core/table)
/** Altura de fila predeterminada del desplazamiento virtual */
export const LH_TABLE_ROW_HEIGHT = 48LH_TABLE_OVERSCAN (de @lh-ui/share/core/table)
/** Filas adicionales que se renderizan por arriba y por abajo de la ventana (buffer contra el parpadeo al desplazar) */
export const LH_TABLE_OVERSCAN = 3LH_TABLE_MIN_COL_WIDTH (de @lh-ui/share/core/table)
// ============================================================================
// Arrastre del ancho de columna
// ============================================================================
/** Límite inferior del ensanchado (px); por debajo, la celda no puede contener el contenido */
export const LH_TABLE_MIN_COL_WIDTH = 40