Skip to content
API 风格:

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 ​

Uso básico
Nombre
Responsabilidad
Ciudad
Shen Yan
Arquitectura
Hangzhou
Su Wan
Frontend
Suzhou
Lu Li
Diseño
Pekín
vue
<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>
vue
<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 ​

Ordenación + columnas fijas + filas alternadas
Nombre
Responsabilidad
Ciudad
Shen Yan
34
Arquitectura
Hangzhou
Su Wan
28
Frontend
Suzhou
Lu Li
31
Diseño
Pekín
vue
<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>
vue
<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.

Desplazamiento virtual de 1000 filas
Nombre
Responsabilidad
Ciudad
Fila 1
Arquitectura
Hangzhou
Fila 2
Frontend
Hangzhou
Fila 3
Arquitectura
Hangzhou
Fila 4
Frontend
Hangzhou
Fila 5
Arquitectura
Hangzhou
Fila 6
Frontend
Hangzhou
vue
<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>
vue
<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 ​

Personalización mediante slots
Nombre
Responsabilidad
Shen Yan
Arquitectura
Su Wan
Frontend
Lu Li
Diseño
vue
<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>
vue
<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.

Cabecera de dos niveles
Nombre
Información del proyecto
Responsabilidad
Ciudad
Shen Yan
Arquitectura
Hangzhou
34
Su Wan
Frontend
Suzhou
28
Lu Li
Diseño
Pekín
31
vue
<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>
vue
<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 de columnas (controlado)
Nombre
Responsabilidad
Shen Yan
Arquitectura
Su Wan
Frontend
Lu Li
Diseño

Filtrado actual: {}

vue
<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>
vue
<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.

Arrastre para ajustar el ancho de columna
Nombre
Responsabilidad
Ciudad
Shen Yan
Arquitectura
Hangzhou
Su Wan
Frontend
Suzhou
Lu Li
Diseño
Pekín

Último ajuste: (sin ajustar)

vue
<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>
vue
<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.

rowKey / maxHeight / ordenación por defecto
Nombre
Responsabilidad
Shen Yan
34
Arquitectura
Lu Li
31
Diseño
Su Wan
28
Frontend
vue
<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>
vue
<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 ​

PropDescripciónTipoPredeterminado
dataDatos de las filas[LhTableRow](#type-LhTableRow)[]—
columnsConfiguración de columnas[LhTableColumn](#type-LhTableColumn)[]—
heightAltura del cuerpo de la tabla (al establecerlo se activa el desplazamiento virtual; un número se interpreta como px)string | number—
maxHeightAltura máxima en modo no virtualstring | number—
rowHeightAltura de las filas en el desplazamiento virtual (debe coincidir con la altura real)number[LH_TABLE_ROW_HEIGHT](#const-LH_TABLE_ROW_HEIGHT)
stripeFilas alternadasbooleanfalse
borderBordes de las celdasbooleanfalse
sizeTamaño"small" | "default" | "large"'default'
rowKeyNombre del campo clave de la filastring'id'
defaultSortPropColumna de ordenación por defectostring—
defaultSortOrderDirección de ordenación por defecto"asc" | "desc"—
sorterFunción de ordenación personalizada(a: LhTableRow, b: LhTableRow) => number—
resizableActiva el arrastre del ancho de columna (necesita además el resizable de columna)booleanfalse
defaultFilteredValuesEstado de filtrado por defecto (no controlado; la clave es el prop de la columna){ [x: string]: (string | number)[]; }() => ({})
filteredValuesEstado de filtrado controlado (la clave es el prop de la columna){ [x: string]: (string | number)[]; }—
sxProps de maquetación rápida sxLhSxProps—

Eventos ​

EventoDescripciónParámetro
sortChangeSe 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 })
rowClickSe dispara al pulsar una fila(row: LhTableRow, index: number)
filterChangeSe dispara tras enviar el filtrado con "Confirmar" o "Restablecer"(payload: { prop: string, values: (string | number)[] })
update:filteredValuesSe dispara al cambiar el estado de filtrado (v-model:filtered-values)(values: Record<string, (string | number)[]>)
columnResizeSe 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 ​

CampoDescripciónTipo
propNombre del campo de datosstring
labelTítulo de la columnastring
widthAncho de la columna (un número es px; si no se establece, se reparte a partes iguales)number | string
alignAlineación'left' | 'center' | 'right'
fixedColumna fija (necesita ir con width)'left' | 'right'
sortableOrdenableboolean
slotNombre del slot de celda personalizadostring
formatterFormateo del valor(row, value) => string
childrenColumnas hijas (al configurarlas se obtienen cabeceras multinivel)LhTableColumn[]
filtersOpciones de filtrado de la columna (solo afectan a las columnas hoja)LhTableFilterOption[]
filterMethodCriterio de filtrado personalizado (por defecto, comparación de igualdad del prop)(value, row) => boolean
filterMultipleSi el panel de filtrado es de selección múltipleboolean
resizableSi 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 son row, las cabeceras de columna columnheader y las celdas gridcell
  • 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: sticky de CSS (sin cálculos de fijación propios)
  • El disparador de filtrado lleva aria-haspopup + aria-expanded y el panel es role="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
ts
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)
ts
/** Dirección de ordenación */
export type LhTableSortOrder = 'asc' | 'desc'
LhTableRow (de @lh-ui/share/core/table)
ts
/** Tipo de los datos de las filas */
export type LhTableRow = Record<string, any>
LhTableColumn (de @lh-ui/share/core/table)
ts
/** 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)
ts
/** 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)
ts
/** 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)
ts
/** Altura de fila predeterminada del desplazamiento virtual */
export const LH_TABLE_ROW_HEIGHT = 48
LH_TABLE_OVERSCAN (de @lh-ui/share/core/table)
ts
/** Filas adicionales que se renderizan por arriba y por abajo de la ventana (buffer contra el parpadeo al desplazar) */
export const LH_TABLE_OVERSCAN = 3
LH_TABLE_MIN_COL_WIDTH (de @lh-ui/share/core/table)
ts
// ============================================================================
// 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

La belleza de la tradición, el cimiento del código