基于vue3.0和element-plus的组件库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

16 KiB

Rebuild list-table-v2 with TanStack + pretext

Goal

Replace el-table-v2 dependency with a custom implementation using:

  • @chenglou/pretext for text measurement (column widths, row heights for plain text)
  • Custom prefix-sum virtualizer (pretext-table style, lightweight)
  • TanStack Table for headless table logic (optional, depends on complexity)

Keep the existing prop interface of list-table-v2.vue so consumers don't need changes.

What I already know

Existing Component (list-table-v2.vue)

  • Vue 3 + TypeScript component
  • Props: data, columns (with key, dataKey, name, i18n, type, width, minWidth, fixed, align, slot, dict, timestamp, filesize, cellRenderer, headerCellRenderer), page, height, minHeight, maxHeight, example, rowKey, selectKey, treeProps, lazy, border, timestampFormat, rowHeight, estimatedRowHeight, headerHeight, debug
  • Emits: query, selection-change, row-click, cell-click
  • Uses mini hidden table for row height probing
  • Uses el-auto-resizer + el-table-v2
  • Features: pagination, fixed columns, sorting via slot, dict/timestamp/filesize formatting

pretext-table (React) Implementation Insights

  • @chenglou/pretext two-phase API:
    • prepare(text, font) → cached handle (Canvas measureText for segmentation)
    • layout(prepared, maxWidth, lineHeight){ height, lineCount } (pure arithmetic, no DOM)
  • Column width: Binary search with pretext to find minimum single-line width. Samples rows, measures header + cell text widths, respects minWidth/maxWidth
  • Row height: prepare() per cell text, layout() at column width, takes max cell height + padding
  • Virtual scrolling: Custom binary search over prefix-sum offsets (NOT TanStack Virtual)
  • Does NOT use TanStack Table or TanStack Virtual

Available Packages

  • @tanstack/vue-table v8.21.3 - headless Vue 3 table
  • @tanstack/vue-virtual v3.13.23 - headless Vue 3 virtualization
  • @chenglou/pretext v0.0.3 - text measurement

Virtualization Approach: Custom Prefix-Sum (pretext-table Style)

Chosen over TanStack Virtual because:

  1. Lightweight — binary search + prefix-sum offsets, no library dependency
  2. Works naturally with pretext — all row heights pre-computed via layout(), just math
  3. Full control over how pretext integrates

Algorithm

  1. Pretext computes ALL row heights upfront (pure arithmetic, no DOM)
  2. Build prefix-sum offset array: offsets[i] = sum(heights[0..i-1])
  3. Binary search on scroll to find visible range: O(log n)
  4. Render only visible rows + overscan

cellRenderer Return Types

Plain VNode (backward compatible):

cellRenderer: ({ data }) => <span>{data}</span>

Size-hinted VNode (new):

cellRenderer: ({ data }) => ({
  vnode: <el-tag>{data}</el-tag>,
  minHeight: 30,  // pixel hint for row height estimation
  minWidth: 100    // pixel hint for column width estimation (no runtime augmentation)
})

Runtime Augmentation for Row Heights

For columns with custom cellRenderer (where pretext can't measure):

  1. Initial: Use minHeight from size-hinted return, or sensible default (44px)
  2. After mount: Measure actual DOM height per row using ResizeObserver
  3. Running average per column: Maintain Map<columnKey, { samples: number[], avg: number }>
  4. Self-adjust: When average shifts >10%, recompute offsets and re-render

Column Width Strategy (Pre-computed, No Runtime Augmentation)

Flex parameters computed via pretext-measured sampling:

  1. Sample: For each column, sample values from data rows
  2. Measure: For each sampled value, measure "shrink wrap" width via walkLineRanges (finds widest line, handles \n)
  3. Compute statistics: mean (μ), variance (σ²) of measured widths
  4. Assign flex parameters:
    • flexBasis: μ (average measured width + padding) — can be overridden per column
    • flexGrow: derived from variance (see formula below) — can be overridden per column
    • flexShrink: derived from variance — can be overridden per column
    • minWidth: max(μ - 2*σ, 50px) — can be overridden per column
    • maxWidth: 300px fixed for now

Variance-based flex formula (normalized 0-1 scale):

const varianceScore = Math.min(1, σ / (μ * 0.5)); // 0 = no variance, 1 = high variance
const flexGrow = 0.1 + varianceScore * 1.9;         // 0.1 (stable) to 2.0 (variable)
const flexShrink = 0.1 + varianceScore * 1.9;       // same scaling

User overrides: Column definition can specify flexGrow, flexShrink, flexBasis, minWidth, maxWidth directly, which takes precedence over computed values.

Dynamic Wrapping Behavior

  • CSS word-break: break-word + white-space: normal on cells
  • When column width reaches maxWidth (300px), text wraps to multiple lines
  • When table can't expand (parent has overflow-x: hidden), columns compress (flexShrink applies)
  • For custom renderer columns: uses minWidth from size-hinted return or default 80px

User can override via column definition props. No runtime width adjustment.

Assumptions (to validate)

  • pretext performance is acceptable for large datasets (~1M rows)
  • Custom virtualizer approach is sufficient (no TanStack Virtual needed)
  • Runtime height adjustment for custom renderers is acceptable UX

Open Questions

All resolved:

  • Fixed columns: YES (left/right pinning via fixed: 'left' | 'right')
  • flexGrow/flexShrink: Data-driven via variance; average affects flexBasis; manual overrides allowed
  • maxWidth: 300px fixed

Requirements

  • Backward compatible Props: Keep existing prop interface (data, columns, page, height, rowKey, border, etc.)
  • Config-less behavior: Table works without any width configuration
  • Content-aware column widths: Pretext measures sampled data; variance drives flexGrow/flexShrink, mean drives flexBasis
  • User overrides: Column definition can override flexGrow, flexShrink, flexBasis, minWidth, maxWidth
  • Dynamic wrapping: maxWidth=300px triggers CSS wrapping; parent overflow-x hidden triggers compression
  • Virtual scrolling: Custom prefix-sum + binary search virtualizer; all row heights pre-computed via pretext
  • Runtime height augmentation: For custom cellRenderer columns, measure DOM after mount, maintain running average
  • cellRenderer flexibility: Return plain VNode or {vnode, minHeight, minWidth} object
  • Pagination: Support existing pagination UI and behavior
  • Fixed columns: Support left/right pinning via fixed: 'left' | 'right'
  • Slots: Support column slots and header slots
  • Dict/timestamp/filesize formatting: Keep existing built-in formatters

Acceptance Criteria (evolving)

  • Existing prop interface maintained (backward compatible)
  • Table renders correctly without any width config
  • Column widths adapt to content
  • Long text wraps instead of overflowing
  • Virtual scrolling works for large datasets
  • Row heights estimated via pretext (no DOM measurement for height)
  • Pagination works
  • Fixed columns work
  • Lint/typecheck passes

Out of Scope (explicit)

  • Tree data / expandable rows (not in current impl either)
  • Column resizing by drag (nice to have)
  • Sort/filter (via slot customization, not built-in)

Technical Notes

Implementation Structure (planned)

packages/base/data/list-table-v2.vue  (main component)
├── usePretextColumnWidths()    # pretext-based column width computation
├── usePretextRowHeights()      # pretext-based row height pre-computation
├── useVirtualRows()            # custom prefix-sum + binary search virtualizer
└── useRuntimeHeightAugment()   # ResizeObserver + running average for custom renderers

Column Width Computation (usePretextColumnWidths)

  1. Sample data rows (up to 100 samples evenly distributed)
  2. For each sampled value, measure "shrink wrap" width via pretext:
    let maxW = 0;
    walkLineRanges(prepared, 10000, line => {
      if (line.width > maxW) maxW = line.width;
    });
    // maxW = minimum width to contain all lines (widest line wins)
    

    This handles embedded \n correctly and avoids binary search.

  3. Compute mean (μ) and variance (σ²) per column
  4. Derive flex parameters:
    • flexBasis = μ + padding
    • flexGrow = 0.1 + (σ/μ*0.5) * 1.9 (clamped 0.1-2.0)
    • flexShrink = same as flexGrow
    • minWidth = max(μ - 2*σ, 50) + padding
    • maxWidth = 300px
  5. User overrides from column definition applied after computation

Row Height Computation (usePretextRowHeights)

For plain text columns (no custom renderer):

  1. For each row, for each column:
    • prepare(text, font) → cached
    • layout(prepared, columnWidth - padding, lineHeight) → height
  2. Row height = max cell height + row padding

For custom renderer columns:

  1. Use minHeight from size-hinted return
  2. After mount, measure DOM via ResizeObserver
  3. Update running average per column
  4. Recompute offsets when average shifts >10%

Virtualization (useVirtualRows)

  1. Build prefix-sum offsets: offsets[i+1] = offsets[i] + heights[i]
  2. On scroll: binary search for startIndex where offsets[i+1] >= scrollTop
  3. Linear scan to find endIndex where offsets[i] < scrollTop + viewportHeight
  4. Render rows [startIndex-overscan, endIndex+overscan] with transform: translateY(offsets[startIndex])

Packages Needed

  • @chenglou/pretext v0.0.4 — text measurement

Implementation Plan (4 PRs)

PR1: Scaffold Hooks & Types COMPLETED

Files created:

packages/base/data/list-table-v2/
├── index.ts                      # Exports all hooks and types
├── types.ts                      # ListTableColumn<T>, ListTableProps, CellRendererResult, etc.
├── measureText.ts                # Cached pretext measurement (prepareWithSegments caching)
├── usePretextColumnWidths.ts     # Flex params from variance/mean
├── usePretextRowHeights.ts       # Pre-computed row heights via pretext
├── useVirtualRows.ts            # Prefix-sum + binary search virtualizer
└── useRuntimeHeightAugment.ts   # ResizeObserver + running average

Key decisions:

  • prepareWithSegments() for everything (superset type works with layout() + walkLineRanges())
  • Single cache for PreparedTextWithSegments, LRU-capped at 10,000 entries
  • Types isolated in types.ts for backward compatibility

PR2: Core Virtualizer + Pretext Measurement (PENDING)

Goal: Wire hooks into Vue component, implement flex container, basic table shell with virtual scrolling.

Files to create/modify:

  • packages/base/data/list-table-v2/list-table-v2.vue (rewrite)

Implementation steps:

  1. Vue component shell

    • Props: same as existing list-table-v2.vue (data, columns, page, height, etc.)
    • Emits: query, selection-change, row-click, cell-click
    • Use el-auto-resizer replacement or manual ResizeObserver for container width
  2. Wire usePretextColumnWidths

    • const { computedConfigs, totalFlexBasis } = usePretextColumnWidths(data, columns, containerWidth)
    • Convert computed flex configs to CSS/flexbox styling
  3. Wire usePretextRowHeights

    • const { rowHeights, totalHeight } = usePretextRowHeights(data, columns, columnWidths)
    • Returns array of heights for virtualizer
  4. Wire useVirtualRows

    • const { visibleRows, totalHeight, onScroll, scrollToIndex } = useVirtualRows(rowHeights, viewportHeight)
    • Container div with overflow-y: auto
    • Inner spacer div with height: totalHeight
    • Render visible rows at transform: translateY(offsetY)
  5. Basic cell rendering

    • Plain text cells: render formatted value
    • Use existing formatters (dict, timestamp, filesize)
  6. CSS flex container for columns

    • Each column: flex: ${flexGrow} ${flexShrink} ${flexBasis}px
    • min-width: ${minWidth}px
    • max-width: ${maxWidth}px
    • CSS word-break: break-word for wrapping
  7. Header row

    • Fixed height: 44px (or headerHeight prop if provided)
    • Render header cells with same flex styling

Acceptance criteria:

  • Table renders without any width config
  • Column widths adapt to content
  • Scrolling works for 1000+ rows (virtualized)
  • No layout shift on scroll

PR3: Custom cellRenderer + Runtime Augmentation (PENDING)

Goal: Support flexible cellRenderer return types and DOM measurement for custom renderers.

Files to create/modify:

  • packages/base/data/list-table-v2/list-table-v2.vue (update)

Implementation steps:

  1. cellRenderer return type handling

    function resolveCellContent(cellResult: CellRendererResult, params) {
      if (!cellResult) return null;
      // Plain VNode
      if (cellResult.component) return cellResult; // VNode has 'component'
      // Size-hinted object: { vnode, minHeight, minWidth }
      return cellResult.vnode;
    }
    
  2. Track which columns have custom renderers

    • const customRendererColumns = computed(() => columns.filter(c => c.cellRenderer))
  3. Wire useRuntimeHeightAugment

    • const { recordHeight, flushSamples, getColumnHeight } = useRuntimeHeightAugment()
  4. Add ResizeObserver for custom renderer rows

    • For each rendered row, observe cells with custom renderers
    • On resize: call recordHeight(columnKey, measuredHeight)
  5. Recompute on significant change

    • After flushSamples(), check if any column average shifted >10%
    • If so, update rowHeights and re-trigger virtualizer
  6. Initial height for custom renderers

    • Check if cellRenderer returns {vnode, minHeight} — use that
    • Otherwise fall back to 44px default

Acceptance criteria:

  • cellRenderer returning plain VNode works as before
  • cellRenderer returning {vnode, minHeight, minWidth} works
  • DOM heights measured after mount for custom renderers
  • Running average updates row heights dynamically

PR4: Fixed Columns, Pagination, Slots, Polish (PENDING)

Goal: Full feature parity with existing list-table-v2.vue

Files to create/modify:

  • packages/base/data/list-table-v2/list-table-v2.vue (update)

Implementation steps:

  1. Fixed columns (left/right pinning)

    • Split columns into: left-fixed, scrollable, right-fixed
    • Left-fixed: position: sticky; left: 0
    • Right-fixed: position: sticky; right: 0
    • Main scroll container only contains scrollable columns
    • Synchronize scroll position between fixed and scrollable headers
  2. Pagination

    • Add pagination div below table
    • Use existing el-pagination component
    • On page/size change: emit query
  3. Column slots

    • v-slot:[column.key] support for custom cell rendering
    • v-slot:header-[column.key] support for custom header rendering
  4. Slots passthrough

    • Pass through slots from parent component
  5. Built-in formatters

    • dict: formatterByDist() from existing code
    • timestamp: TzDateTime component
    • filesize: formatFileSize() from existing code
  6. Debug mode

    • Show estimated row heights, column flex params
    • Toggle via debug prop
  7. Border styling

    • CSS borders on cells/rows when border prop is true
  8. Row/column events

    • @row-click: emit when row div clicked
    • @cell-click: emit when cell clicked

Acceptance criteria:

  • Fixed left/right columns work
  • Pagination emits query event
  • Column slots work
  • dict/timestamp/filesize formatting works
  • Debug mode shows info
  • Border styling works
  • Row/cell click events fire

How to Resume After Context Clear

  1. Read prd.md for full requirements
  2. Check task.json for current PR status (subtasks array)
  3. PR1 is complete — hooks are scaffolded in packages/base/data/list-table-v2/
  4. Continue with PR2: implement list-table-v2.vue component
  5. PR3: add cellRenderer support + runtime augmentation
  6. PR4: fixed columns, pagination, slots, polish