forked from mengyxu/noob-components
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.
1147 lines
39 KiB
1147 lines
39 KiB
<template> |
|
<div class="table-v2-demo"> |
|
<!-- Navigation anchors --> |
|
<nav class="demo-nav"> |
|
<a v-for="section in sections" :key="section.id" :href="'#' + section.id" class="nav-link"> |
|
{{ section.label }} |
|
</a> |
|
</nav> |
|
|
|
<!-- ============================================================ |
|
SECTION 1: Basic Usage |
|
============================================================ --> |
|
<section :id="sections[0].id" class="demo-section"> |
|
<h2 class="section-title">1. {{ sections[0].label }}</h2> |
|
<p class="section-desc">Basic data binding with columns, pagination, and event handling.</p> |
|
|
|
<div class="demo-toolbar"> |
|
<span class="test-indicator" :class="testResults.basic.events ? 'pass' : 'pending'"> |
|
@row-click: {{ testResults.basic.events ? "✓ fired" : "—" }} |
|
</span> |
|
<span class="test-indicator" :class="testResults.basic.cellClick ? 'pass' : 'pending'"> |
|
@cell-click: {{ testResults.basic.cellClick ? "✓ fired" : "—" }} |
|
</span> |
|
<span class="test-indicator" :class="testResults.basic.query ? 'pass' : 'pending'"> |
|
@query: {{ testResults.basic.query ? "✓ fired" : "—" }} |
|
</span> |
|
</div> |
|
|
|
<SearchRow :title="t('table.title') + ' (V2) - Basic'"> |
|
<template #default> |
|
<NoobInput v-model="basicExample.aaa" :width="180" placeholder="Filter case name"></NoobInput> |
|
</template> |
|
</SearchRow> |
|
|
|
<ListTableV2 |
|
:data="basicData" |
|
:columns="basicColumns" |
|
:page="true" |
|
:border="true" |
|
:example="basicExample" |
|
:row-key="'id'" |
|
height="350" |
|
@query="handleBasicQuery" |
|
@row-click="handleBasicRowClick" |
|
@cell-click="handleBasicCellClick" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// Columns definition |
|
const basicColumns = [ |
|
{ key: 'id', name: 'ID' }, |
|
{ key: 'caseName', name: 'Case Name' }, |
|
{ key: 'taskName', name: 'Task Name' }, |
|
{ key: 'userId', name: 'User ID' }, |
|
{ key: 'createTime', name: 'Create Time', timestamp: 'unix' }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 2: Styling - Borders & Alignment |
|
============================================================ --> |
|
<section :id="sections[1].id" class="demo-section"> |
|
<h2 class="section-title">2. {{ sections[1].label }}</h2> |
|
<p class="section-desc"> |
|
Table borders and column content alignment via <code>border</code> and <code>align</code> props. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<el-button size="small" @click="toggleBorder">{{ borderDemo.border ? "Remove" : "Add" }} Borders</el-button> |
|
</div> |
|
|
|
<SearchRow title="Styling Demo"> |
|
<template #default> |
|
<NoobSelect v-model="borderDemo.align" dict="test" :width="120"></NoobSelect> |
|
</template> |
|
</SearchRow> |
|
|
|
<ListTableV2 |
|
:data="styleData" |
|
:columns="styleColumns" |
|
:page="false" |
|
:border="borderDemo.border" |
|
:row-key="'id'" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// Alignment options: 'left' | 'center' | 'right' |
|
const styleColumns = [ |
|
{ key: 'id', name: 'ID', align: 'center' }, |
|
{ key: 'caseName', name: 'Case Name', align: 'left' }, |
|
{ key: 'userId', name: 'User ID', align: 'right' }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 3: Fixed Columns |
|
============================================================ --> |
|
<section :id="sections[2].id" class="demo-section"> |
|
<h2 class="section-title">3. {{ sections[2].label }}</h2> |
|
<p class="section-desc"> |
|
Fix columns to left or right using <code>fixed: "left"</code> or <code>fixed: "right"</code>. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<span class="test-indicator" :class="testResults.fixedScroll ? 'pass' : 'pending'"> |
|
Scroll test: {{ testResults.fixedScroll ? "✓" : "—" }} |
|
</span> |
|
<span class="test-indicator">← ID and Case Name fixed left | Actions fixed right →</span> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="fixedData" |
|
:columns="fixedColumns" |
|
:page="true" |
|
:border="true" |
|
:example="fixedExample" |
|
:row-key="'id'" |
|
height="350" |
|
@query="handleFixedQuery" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>const fixedColumns = [ |
|
{ key: 'id', name: 'ID', fixed: 'left', width: 80 }, |
|
{ key: 'caseName', name: 'Case Name', fixed: 'left', width: 200 }, |
|
{ key: 'taskName', name: 'Task Name' }, |
|
{ key: 'userId', name: 'User ID' }, |
|
{ key: 'content', name: 'Content' }, |
|
{ key: 'createTime', name: 'Create Time', timestamp: 'unix' }, |
|
{ key: 'actions', name: 'Actions', fixed: 'right', width: 120 }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 4: Data Formatting |
|
============================================================ --> |
|
<section :id="sections[3].id" class="demo-section"> |
|
<h2 class="section-title">4. {{ sections[3].label }}</h2> |
|
<p class="section-desc">Built-in formatting for timestamps, dictionaries, and file sizes.</p> |
|
|
|
<ListTableV2 |
|
:data="formatData" |
|
:columns="formatColumns" |
|
:page="false" |
|
:border="true" |
|
:row-key="'id'" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>const formatColumns = [ |
|
{ key: 'id', name: 'ID' }, |
|
{ key: 'caseName', name: 'Case Name' }, |
|
// timestamp: 'unix' formats Unix timestamp (seconds) |
|
{ key: 'createTime', name: 'Create Time', timestamp: 'unix' }, |
|
// filesize: true formats bytes to K/M/G |
|
{ key: 'fileSize', name: 'File Size', filesize: true }, |
|
// dict uses state.dict for lookup |
|
{ key: 'status', name: 'Status', dict: 'test' }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 5: Custom Cell Renderer |
|
============================================================ --> |
|
<section :id="sections[4].id" class="demo-section"> |
|
<h2 class="section-title">5. {{ sections[4].label }}</h2> |
|
<p class="section-desc"> |
|
Custom cell content via <code>cellRenderer</code> function. Returns JSX. Critical: must work correctly with the |
|
mini-table probe row for dynamic height measurement. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<span class="test-indicator" :class="testResults.cellRenderer ? 'pass' : 'pending'"> |
|
Custom renderer: {{ testResults.cellRenderer ? "✓ rendered" : "—" }} |
|
</span> |
|
<span class="test-indicator" :class="testResults.cellRendererMini ? 'pass' : 'pending'"> |
|
Mini-table sync: {{ testResults.cellRendererMini ? "✓" : "—" }} |
|
</span> |
|
<el-button size="small" @click="testCellRendererResize">Test Resize Probe</el-button> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="rendererData" |
|
:columns="rendererColumns" |
|
:page="false" |
|
:border="true" |
|
:row-key="'id'" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>import { CellRenderer, CellRendererParams } from 'element-plus'; |
|
import { ElTag } from 'element-plus'; |
|
|
|
// Custom cellRenderer returning JSX |
|
const statusRenderer: CellRenderer<T> = ({ cellData }) => { |
|
const status = cellData as string; |
|
const type = status === 'active' ? 'success' : status === 'pending' ? 'warning' : 'info'; |
|
return <ElTag type={type}>{status}</ElTag>; |
|
}; |
|
|
|
const rendererColumns = [ |
|
{ key: 'id', name: 'ID' }, |
|
{ key: 'caseName', name: 'Case Name' }, |
|
{ key: 'status', name: 'Status', cellRenderer: statusRenderer }, |
|
{ key: 'actions', name: 'Actions', cellRenderer: actionRenderer }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 6: Custom Header Renderer |
|
============================================================ --> |
|
<section :id="sections[5].id" class="demo-section"> |
|
<h2 class="section-title">6. {{ sections[5].label }}</h2> |
|
<p class="section-desc"> |
|
Custom header content via <code>headerCellRenderer</code> function. Must also sync with mini-table header probe. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<span class="test-indicator" :class="testResults.headerRenderer ? 'pass' : 'pending'"> |
|
Header renderer: {{ testResults.headerRenderer ? "✓ rendered" : "—" }} |
|
</span> |
|
<span class="test-indicator" :class="testResults.headerRendererMini ? 'pass' : 'pending'"> |
|
Mini-table header sync: {{ testResults.headerRendererMini ? "✓" : "—" }} |
|
</span> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="headerData" |
|
:columns="headerColumns" |
|
:page="false" |
|
:border="true" |
|
:row-key="'id'" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>import { HeaderCellRenderer, HeaderCellRendererParams } from 'element-plus'; |
|
|
|
// Custom headerCellRenderer returning JSX |
|
const headerRenderer: HeaderCellRenderer<T> = ({ column }) => { |
|
const col: ListTableColumn<T> = column._listTableColumn; |
|
return ( |
|
<span> |
|
<span style="color: var(--el-color-primary)">*</span> {col.title} |
|
</span> |
|
); |
|
}; |
|
|
|
const headerColumns = [ |
|
{ key: 'id', name: 'ID', headerCellRenderer }, |
|
{ key: 'caseName', name: 'Case Name', headerCellRenderer }, |
|
{ key: 'taskName', name: 'Task Name', headerCellRenderer }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 7: Dynamic Height (Auto Probe) |
|
============================================================ --> |
|
<section :id="sections[6].id" class="demo-section"> |
|
<h2 class="section-title">7. {{ sections[6].label }}</h2> |
|
<p class="section-desc"> |
|
Dynamic height mode uses a hidden mini-table to probe actual row and header heights. |
|
<code>debug</code> shows estimated values. No <code>rowHeight</code> or <code>estimatedRowHeight</code> needed — |
|
it measures automatically. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<span class="test-indicator" :class="testResults.dynamicHeight ? 'pass' : 'pending'"> |
|
Height probed: {{ testResults.dynamicHeight ? "✓" : "—" }} |
|
</span> |
|
<el-button size="small" @click="testResize">Trigger ResizeObserver</el-button> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="dynamicData" |
|
:columns="dynamicColumns" |
|
:page="true" |
|
:border="true" |
|
:example="dynamicExample" |
|
:row-key="'id'" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// Dynamic height mode (no rowHeight set) |
|
// Mini-table probe automatically measures row height |
|
// ResizeObserver debounces at 50ms to prevent flicker |
|
|
|
// Fixed height mode (rowHeight set) - disables probe |
|
// <ListTableV2 :row-height="60" ...> // No probe needed |
|
|
|
// Manual estimate mode |
|
// <ListTableV2 :estimated-row-height="50" ...> // Uses this as starting estimate</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 8: Fixed Height Mode |
|
============================================================ --> |
|
<section :id="sections[7].id" class="demo-section"> |
|
<h2 class="section-title">8. {{ sections[7].label }}</h2> |
|
<p class="section-desc"> |
|
Fixed height mode via <code>:row-height="60"</code>. Disables the mini-table probe. Better performance for large |
|
datasets since no measurement needed. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<span class="test-indicator">rowHeight: 60 | No probe active</span> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="fixedHeightData" |
|
:columns="fixedHeightColumns" |
|
:page="false" |
|
:border="true" |
|
:row-key="'id'" |
|
:row-height="60" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// Fixed row height - no mini-table probe needed |
|
// Better performance for large datasets |
|
<ListTableV2 |
|
:data="data" |
|
:columns="columns" |
|
:row-key="'id'" |
|
:row-height="60" |
|
:border="true" |
|
debug |
|
/></code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 9: Column Width Distribution |
|
============================================================ --> |
|
<section :id="sections[8].id" class="demo-section"> |
|
<h2 class="section-title">9. {{ sections[8].label }}</h2> |
|
<p class="section-desc"> |
|
Control column widths with <code>width</code> (fixed) and <code>minWidth</code> (flexible). Columns without |
|
explicit width use auto-derived flex factors greater than <code>1</code> to fill remaining space. |
|
</p> |
|
|
|
<ListTableV2 |
|
:data="widthData" |
|
:columns="widthColumns" |
|
:page="false" |
|
:border="true" |
|
:row-key="'id'" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>const widthColumns = [ |
|
{ key: 'id', name: 'ID', width: 80 }, // Fixed 80px |
|
{ key: 'caseName', name: 'Case Name', minWidth: 200 }, // Min 200px, grows |
|
{ key: 'taskName', name: 'Task Name', minWidth: 150 }, // Min 150px, grows |
|
{ key: 'userId', name: 'User ID', width: 120 }, // Fixed 120px |
|
// Remaining space distributed proportionally |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 10: Width + Height + MaxHeight |
|
============================================================ --> |
|
<section :id="sections[9].id" class="demo-section"> |
|
<h2 class="section-title">10. {{ sections[9].label }}</h2> |
|
<p class="section-desc"> |
|
Control table dimensions with <code>height</code>, <code>maxHeight</code>, and <code>headerHeight</code>. |
|
Auto-resizer handles width automatically. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<el-button size="small" @click="toggleMaxHeight">{{ showMaxHeight ? "Remove" : "Add" }} Max Height</el-button> |
|
<el-button size="small" @click="cycleHeaderHeight">Cycle Header Height</el-button> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="dimensionData" |
|
:columns="dimensionColumns" |
|
:page="false" |
|
:border="true" |
|
:row-key="'id'" |
|
:max-height="showMaxHeight ? 300 : undefined" |
|
:header-height="dimensionHeaderHeight" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// Fixed height with scrolling internally |
|
<ListTableV2 :height="400" ...> |
|
|
|
// Max height - table grows but caps at max |
|
<ListTableV2 :max-height="300" ...> |
|
|
|
// Custom header height (default is auto-probed) |
|
<ListTableV2 :header-height="60" ...></code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 11: Slot-based Custom Cells |
|
============================================================ --> |
|
<section :id="sections[10].id" class="demo-section"> |
|
<h2 class="section-title">11. {{ sections[10].label }}</h2> |
|
<p class="section-desc"> |
|
Custom cell content via Vue template slots. Use <code>slot: true</code> on column and define |
|
<code>#columnKey</code> template in parent. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<span class="test-indicator" :class="testResults.slotCells ? 'pass' : 'pending'"> |
|
Slot cells: {{ testResults.slotCells ? "✓ rendered" : "—" }} |
|
</span> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="slotData" |
|
:columns="slotColumns" |
|
:page="false" |
|
:border="true" |
|
:row-key="'id'" |
|
height="350" |
|
debug |
|
> |
|
<template #status="{ row }"> |
|
<el-tag :type="row.status === 'active' ? 'success' : 'warning'" size="small"> |
|
{{ row.status }} |
|
</el-tag> |
|
</template> |
|
<template #actions="{ row }"> |
|
<el-button link type="primary" size="small" @click="onSlotAction(row)">Edit</el-button> |
|
<el-button link type="danger" size="small" @click="onSlotAction(row)">Delete</el-button> |
|
</template> |
|
</ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// In template |
|
<ListTableV2 :columns="slotColumns" ...> |
|
<template #status="{ row }"> |
|
<el-tag>{row.status}</el-tag> |
|
</template> |
|
<template #actions="{ row }"> |
|
<el-button>Edit</el-button> |
|
</template> |
|
</ListTableV2> |
|
|
|
// In script |
|
const slotColumns = [ |
|
{ key: 'id', name: 'ID', slot: true }, |
|
{ key: 'caseName', name: 'Case Name' }, |
|
{ key: 'status', name: 'Status', slot: true }, |
|
{ key: 'actions', name: 'Actions', slot: true }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 12: i18n Support |
|
============================================================ --> |
|
<section :id="sections[11].id" class="demo-section"> |
|
<h2 class="section-title">12. {{ sections[11].label }}</h2> |
|
<p class="section-desc">Column headers support i18n via <code>i18n</code> key (uses <code>vue3-i18n</code>).</p> |
|
|
|
<ListTableV2 |
|
:data="i18nData" |
|
:columns="i18nColumns" |
|
:page="false" |
|
:border="true" |
|
:row-key="'id'" |
|
height="350" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// The component uses t() from vue3-i18n internally |
|
// Column with i18n key: |
|
const i18nColumns = [ |
|
{ key: 'id', name: 'ID' }, |
|
{ key: 'caseName', i18n: 'table.props.0' }, // Uses t('table.props.0') |
|
{ key: 'taskName', i18n: 'table.props.1' }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 13: Combined Features Test |
|
============================================================ --> |
|
<section :id="sections[12].id" class="demo-section"> |
|
<h2 class="section-title">13. {{ sections[12].label }}</h2> |
|
<p class="section-desc"> |
|
All features working together: fixed columns + custom renderers + pagination + dynamic height probe + events. |
|
This is the stress test. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<el-button size="small" type="primary" @click="runCombinedTest">Run Combined Test</el-button> |
|
<el-button size="small" @click="reloadCombinedData">Reload Data</el-button> |
|
<span v-for="(v, k) in testResults.combined" :key="k" class="test-indicator" :class="v ? 'pass' : 'pending'"> |
|
{{ k }}: {{ v ? "✓" : "✗" }} |
|
</span> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="combinedData" |
|
:columns="combinedColumns" |
|
:page="true" |
|
:border="true" |
|
:example="combinedExample" |
|
:row-key="'id'" |
|
height="350" |
|
@query="handleCombinedQuery" |
|
@row-click="handleCombinedRowClick" |
|
@cell-click="handleCombinedCellClick" |
|
debug |
|
> |
|
<template #combinedStatus="{ row }"> |
|
<el-tag :type="getStatusType(row.status)" size="small">{{ row.status }}</el-tag> |
|
</template> |
|
<template #combinedActions="{ row }"> |
|
<el-button link type="primary" size="small">View</el-button> |
|
<el-button link type="warning" size="small">Edit</el-button> |
|
</template> |
|
</ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// Combined: fixed columns + custom renderers + pagination + i18n |
|
const combinedColumns = [ |
|
{ key: 'id', name: 'ID', fixed: 'left', width: 80 }, |
|
{ key: 'caseName', i18n: 'table.props.0', fixed: 'left' }, |
|
{ key: 'taskName', i18n: 'table.props.1' }, |
|
{ key: 'userId', name: 'User', dict: 'test' }, |
|
{ key: 'createTime', name: 'Time', timestamp: 'unix', minWidth: 180 }, |
|
{ key: 'combinedStatus', name: 'Status', slot: true }, |
|
{ key: 'combinedActions', name: 'Actions', fixed: 'right', slot: true, width: 140 }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
|
|
<!-- ============================================================ |
|
SECTION 14: Custom Renderer + Fixed + Pagination |
|
============================================================ --> |
|
<section :id="sections[13].id" class="demo-section"> |
|
<h2 class="section-title">14. {{ sections[13].label }}</h2> |
|
<p class="section-desc"> |
|
Edge case: custom <code>cellRenderer</code> combined with <code>fixed</code> columns and |
|
<code>pagination</code>. Verifies mini-table probe handles custom renderers correctly across page changes. |
|
</p> |
|
|
|
<div class="demo-toolbar"> |
|
<el-button size="small" @click="changePageRenderer">Change Page</el-button> |
|
<el-button size="small" @click="toggleRendererType">Toggle Renderer Type</el-button> |
|
<span class="test-indicator">Renderer type: {{ rendererType }}</span> |
|
</div> |
|
|
|
<ListTableV2 |
|
:data="edgeData" |
|
:columns="edgeColumns" |
|
:page="true" |
|
:border="true" |
|
:example="edgeExample" |
|
:row-key="'id'" |
|
height="350" |
|
@query="handleEdgeQuery" |
|
debug |
|
></ListTableV2> |
|
|
|
<div class="demo-code"> |
|
<pre><code>// Edge case: cellRenderer + fixed columns + pagination |
|
// Must verify mini-table probe renders custom JSX correctly |
|
const edgeColumns = [ |
|
{ key: 'id', fixed: 'left', cellRenderer: edgeIdRenderer }, |
|
{ key: 'caseName', cellRenderer: edgeNameRenderer }, |
|
{ key: 'priority', cellRenderer: edgePriorityRenderer }, |
|
{ key: 'actions', fixed: 'right', cellRenderer: edgeActionsRenderer }, |
|
];</code></pre> |
|
</div> |
|
</section> |
|
</div> |
|
</template> |
|
|
|
<script lang="tsx" setup> |
|
import { useStore } from "vuex"; |
|
import { reactive, ref, computed, onMounted } from "vue"; |
|
import { ListTableV2, SearchRow, NoobInput, NoobSelect, Infomation } from "noob-mengyxu"; |
|
import { useI18n } from "vue3-i18n"; |
|
import { LoremIpsum } from "lorem-ipsum"; |
|
import { ElTag, ElButton, ElLink } from "element-plus"; |
|
import { |
|
CellRenderer, |
|
CellRendererParams, |
|
HeaderCellRenderer, |
|
HeaderCellRendererParams, |
|
} from "element-plus/es/components/table-v2/src/types.mjs"; |
|
import type { ListTableColumn } from "../../../packages/base/data/list-table-v2.vue"; |
|
|
|
// --- Data Generator --- |
|
const generateRows = (count: number) => { |
|
const rows: Array<Record<string, any>> = []; |
|
const now = Math.floor(Date.now() / 1000); |
|
const lorem = new LoremIpsum({ |
|
sentencesPerParagraph: { min: 1, max: 3 }, |
|
wordsPerSentence: { min: 4, max: 12 }, |
|
}); |
|
for (let i = 1; i <= count; i++) { |
|
rows.push({ |
|
id: i, |
|
caseName: `案件${i}—${lorem.generateWords(3)}`, |
|
taskName: `任务${i}`, |
|
userId: `user${i % 10}`, |
|
content: lorem.generateSentences(1), |
|
createTime: now - i * 60 * ((i % 10) + 1), |
|
fileSize: Math.floor(Math.random() * 10000000), |
|
status: ["active", "pending", "inactive", "completed"][i % 4], |
|
priority: ["high", "medium", "low"][i % 3], |
|
}); |
|
} |
|
return rows; |
|
}; |
|
|
|
const allRows = generateRows(200); |
|
|
|
// --- i18n --- |
|
const { t } = useI18n(); |
|
|
|
// --- Section Navigation --- |
|
const sections = [ |
|
{ id: "s1-basic", label: "Basic Usage" }, |
|
{ id: "s2-styling", label: "Styling" }, |
|
{ id: "s3-fixed", label: "Fixed Columns" }, |
|
{ id: "s4-formatting", label: "Data Formatting" }, |
|
{ id: "s5-cell-renderer", label: "Custom Cell Renderer" }, |
|
{ id: "s6-header-renderer", label: "Custom Header Renderer" }, |
|
{ id: "s7-dynamic-height", label: "Dynamic Height" }, |
|
{ id: "s8-fixed-height", label: "Fixed Height Mode" }, |
|
{ id: "s9-width-dist", label: "Column Width" }, |
|
{ id: "s10-dimensions", label: "Height/MaxHeight" }, |
|
{ id: "s11-slots", label: "Slot Cells" }, |
|
{ id: "s12-i18n", label: "i18n Support" }, |
|
{ id: "s13-combined", label: "Combined Test" }, |
|
{ id: "s14-edge-renderer", label: "Renderer Edge Cases" }, |
|
]; |
|
|
|
// --- Test Results --- |
|
const testResults = reactive<Record<string, any>>({ |
|
basic: { events: false, cellClick: false, query: false }, |
|
fixedScroll: false, |
|
cellRenderer: false, |
|
cellRendererMini: false, |
|
headerRenderer: false, |
|
headerRendererMini: false, |
|
dynamicHeight: false, |
|
slotCells: false, |
|
combined: {}, |
|
}); |
|
|
|
// ================================================================ |
|
// SECTION 1: Basic |
|
// ================================================================ |
|
const basicExample = reactive({ page: 1, size: 10, aaa: "" }); |
|
const basicData = reactive({ data: allRows.slice(0, 10), total: allRows.length }); |
|
const basicColumns = [ |
|
{ key: "id", name: "ID" }, |
|
{ key: "caseName", name: "Case Name" }, |
|
{ key: "taskName", name: "Task Name" }, |
|
{ key: "userId", name: "User ID" }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix" }, |
|
]; |
|
const handleBasicQuery = () => { |
|
testResults.basic.query = true; |
|
const start = (basicExample.page - 1) * basicExample.size; |
|
basicData.data = allRows.slice(start, start + basicExample.size); |
|
}; |
|
const handleBasicRowClick = (row: any) => { |
|
testResults.basic.events = true; |
|
console.log("row-click", row.id); |
|
}; |
|
const handleBasicCellClick = (row: any) => { |
|
testResults.basic.cellClick = true; |
|
console.log("cell-click", row.id); |
|
}; |
|
|
|
// ================================================================ |
|
// SECTION 2: Styling |
|
// ================================================================ |
|
const borderDemo = reactive({ border: true, align: "center" }); |
|
const styleData = allRows.slice(0, 20); |
|
const styleColumns = computed(() => [ |
|
{ key: "id", name: "ID", align: "center" as const }, |
|
{ key: "caseName", name: "Case Name", align: "left" as const }, |
|
{ key: "userId", name: "User ID", align: "right" as const }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix", align: "center" as const }, |
|
]); |
|
const toggleBorder = () => { |
|
borderDemo.border = !borderDemo.border; |
|
}; |
|
|
|
// ================================================================ |
|
// SECTION 3: Fixed Columns |
|
// ================================================================ |
|
const fixedExample = reactive({ page: 1, size: 10 }); |
|
const fixedData = reactive({ data: allRows.slice(0, 10), total: allRows.length }); |
|
const fixedColumns = [ |
|
{ key: "id", name: "ID", fixed: "left" as const, width: 80 }, |
|
{ key: "caseName", name: "Case Name", fixed: "left" as const, width: 200 }, |
|
{ key: "taskName", name: "Task Name" }, |
|
{ key: "userId", name: "User ID" }, |
|
{ key: "content", name: "Content", minWidth: 200 }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix" }, |
|
{ key: "actions", name: "Actions", fixed: "right" as const, width: 120 }, |
|
]; |
|
const handleFixedQuery = () => { |
|
const start = (fixedExample.page - 1) * fixedExample.size; |
|
fixedData.data = allRows.slice(start, start + fixedExample.size); |
|
}; |
|
|
|
// ================================================================ |
|
// SECTION 4: Formatting |
|
// ================================================================ |
|
const formatData = allRows.slice(0, 20); |
|
const formatColumns = [ |
|
{ key: "id", name: "ID" }, |
|
{ key: "caseName", name: "Case Name" }, |
|
{ key: "createTime", name: "Create Time (unix)", timestamp: "unix" as const }, |
|
{ key: "fileSize", name: "File Size", filesize: true as const }, |
|
{ key: "status", name: "Status (dict)", dict: "test" as const }, |
|
]; |
|
|
|
// ================================================================ |
|
// SECTION 5: Custom Cell Renderer |
|
// ================================================================ |
|
const statusRenderer: CellRenderer<T> = ({ cellData }) => { |
|
testResults.cellRenderer = true; |
|
const status = cellData as string; |
|
const type = |
|
status === "active" ? "success" : status === "pending" ? "warning" : status === "inactive" ? "info" : "danger"; |
|
return ( |
|
<ElTag type={type} size="small"> |
|
{status} |
|
</ElTag> |
|
); |
|
}; |
|
|
|
const actionRenderer: CellRenderer<T> = ({ rowData }) => { |
|
return ( |
|
<div style="display: flex; gap: 4px;"> |
|
<ElButton size="small" link type="primary"> |
|
Edit |
|
</ElButton> |
|
<ElButton size="small" link type="danger"> |
|
Del |
|
</ElButton> |
|
</div> |
|
); |
|
}; |
|
|
|
const rendererData = allRows.slice(0, 20); |
|
const rendererColumns: ListTableColumn<T>[] = [ |
|
{ key: "id", name: "ID" }, |
|
{ key: "caseName", name: "Case Name" }, |
|
{ key: "status", name: "Status", cellRenderer: statusRenderer }, |
|
{ |
|
key: "priority", |
|
name: "Priority", |
|
cellRenderer: ({ cellData }) => { |
|
const colors: Record<string, string> = { high: "danger", medium: "warning", low: "info" }; |
|
return ( |
|
<ElTag type={colors[cellData] || "info"} size="small"> |
|
{cellData} |
|
</ElTag> |
|
); |
|
}, |
|
}, |
|
{ key: "actions", name: "Actions", cellRenderer: actionRenderer }, |
|
]; |
|
|
|
const testCellRendererResize = () => { |
|
testResults.cellRendererMini = true; |
|
// Force re-render by toggling data |
|
rendererData = [...allRows.slice(0, 20)]; |
|
}; |
|
|
|
// ================================================================ |
|
// SECTION 6: Custom Header Renderer |
|
// ================================================================ |
|
const headerRenderer: HeaderCellRenderer<T> = ({ column }) => { |
|
testResults.headerRenderer = true; |
|
return ( |
|
<span> |
|
<span style="color: var(--el-color-danger)">★</span> {(column as any).title} |
|
</span> |
|
); |
|
}; |
|
|
|
const headerData = allRows.slice(0, 20); |
|
const headerColumns: ListTableColumn<T>[] = [ |
|
{ key: "id", name: "ID", headerCellRenderer: headerRenderer }, |
|
{ key: "caseName", name: "Case Name", headerCellRenderer: headerRenderer }, |
|
{ key: "taskName", name: "Task Name", headerCellRenderer: headerRenderer }, |
|
{ key: "userId", name: "User ID", headerCellRenderer: headerRenderer }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix", headerCellRenderer: headerRenderer }, |
|
]; |
|
|
|
// ================================================================ |
|
// SECTION 7: Dynamic Height |
|
// ================================================================ |
|
const dynamicExample = reactive({ page: 1, size: 10 }); |
|
const dynamicData = reactive({ data: allRows.slice(0, 10), total: allRows.length }); |
|
const dynamicColumns = [ |
|
{ key: "id", name: "ID" }, |
|
{ key: "caseName", name: "Case Name" }, |
|
{ key: "content", name: "Content (long text to test height probe)" }, |
|
{ key: "taskName", name: "Task Name" }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix" }, |
|
]; |
|
const handleDynamicQuery = () => { |
|
const start = (dynamicExample.page - 1) * dynamicExample.size; |
|
dynamicData.data = allRows.slice(start, start + dynamicExample.size); |
|
}; |
|
|
|
const testResize = () => { |
|
dynamicData.data = [...allRows.slice(0, 10)]; |
|
testResults.dynamicHeight = true; |
|
}; |
|
|
|
// ================================================================ |
|
// SECTION 8: Fixed Height |
|
// ================================================================ |
|
const fixedHeightData = allRows.slice(0, 50); |
|
const fixedHeightColumns = [ |
|
{ key: "id", name: "ID" }, |
|
{ key: "caseName", name: "Case Name" }, |
|
{ key: "taskName", name: "Task Name" }, |
|
{ key: "userId", name: "User ID" }, |
|
]; |
|
|
|
// ================================================================ |
|
// SECTION 9: Width Distribution |
|
// ================================================================ |
|
const widthData = allRows.slice(0, 20); |
|
const widthColumns = [ |
|
{ key: "id", name: "ID", width: 80 }, |
|
{ key: "caseName", name: "Case Name (min 200)", minWidth: 200 }, |
|
{ key: "taskName", name: "Task Name (min 150)", minWidth: 150 }, |
|
{ key: "userId", name: "User ID (fixed 120)", width: 120 }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix" }, |
|
]; |
|
|
|
// ================================================================ |
|
// SECTION 10: Dimensions |
|
// ================================================================ |
|
const showMaxHeight = ref(false); |
|
const toggleMaxHeight = () => { |
|
showMaxHeight.value = !showMaxHeight.value; |
|
}; |
|
const dimensionHeaderHeight = ref<number | undefined>(undefined); |
|
const dimensionData = allRows.slice(0, 30); |
|
const dimensionColumns = [ |
|
{ key: "id", name: "ID" }, |
|
{ key: "caseName", name: "Case Name" }, |
|
{ key: "taskName", name: "Task Name" }, |
|
{ key: "userId", name: "User ID" }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix" }, |
|
]; |
|
const cycleHeaderHeight = () => { |
|
const heights = [undefined, 40, 60, 80]; |
|
const idx = heights.indexOf(dimensionHeaderHeight.value); |
|
dimensionHeaderHeight.value = heights[(idx + 1) % heights.length]; |
|
}; |
|
|
|
// ================================================================ |
|
// SECTION 11: Slot Cells |
|
// ================================================================ |
|
const slotData = allRows.slice(0, 20); |
|
const slotColumns = [ |
|
{ key: "id", name: "ID", slot: true }, |
|
{ key: "caseName", name: "Case Name" }, |
|
{ key: "status", name: "Status", slot: true }, |
|
{ key: "priority", name: "Priority", slot: true }, |
|
{ key: "actions", name: "Actions", slot: true }, |
|
]; |
|
const onSlotAction = (row: any) => { |
|
testResults.slotCells = true; |
|
console.log("Slot action:", row.id); |
|
}; |
|
|
|
// ================================================================ |
|
// SECTION 12: i18n |
|
// ================================================================ |
|
const i18nData = allRows.slice(0, 20); |
|
const i18nColumns = [ |
|
{ key: "id", name: "ID" }, |
|
{ key: "caseName", i18n: "table.props.0" }, |
|
{ key: "taskName", i18n: "table.props.1" }, |
|
{ key: "userId", i18n: "table.props.2" }, |
|
{ key: "content", i18n: "table.props.3" }, |
|
{ key: "createTime", i18n: "table.props.4", timestamp: "unix" }, |
|
]; |
|
|
|
// ================================================================ |
|
// SECTION 13: Combined |
|
// ================================================================ |
|
const combinedExample = reactive({ page: 1, size: 10 }); |
|
const combinedData = reactive({ data: allRows.slice(0, 10), total: allRows.length }); |
|
const combinedColumns = [ |
|
{ key: "id", name: "ID", fixed: "left" as const, width: 80 }, |
|
{ key: "caseName", i18n: "table.props.0", fixed: "left" as const }, |
|
{ key: "taskName", i18n: "table.props.1" }, |
|
{ key: "userId", i18n: "table.props.2", dict: "test" as const }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix" as const, minWidth: 180 }, |
|
{ key: "combinedStatus", name: "Status", slot: true }, |
|
{ key: "combinedActions", name: "Actions", fixed: "right" as const, slot: true, width: 140 }, |
|
]; |
|
const getStatusType = (status: string) => { |
|
const map: Record<string, any> = { active: "success", pending: "warning", inactive: "info", completed: "success" }; |
|
return map[status] || "info"; |
|
}; |
|
const handleCombinedQuery = () => { |
|
const start = (combinedExample.page - 1) * combinedExample.size; |
|
combinedData.data = allRows.slice(start, start + combinedExample.size); |
|
testResults.combined.query = true; |
|
}; |
|
const handleCombinedRowClick = () => { |
|
testResults.combined.rowClick = true; |
|
}; |
|
const handleCombinedCellClick = () => { |
|
testResults.combined.cellClick = true; |
|
}; |
|
const runCombinedTest = () => { |
|
testResults.combined = { query: false, rowClick: false, cellClick: false, renderer: false }; |
|
combinedExample.page = 1; |
|
handleCombinedQuery(); |
|
setTimeout(() => { |
|
combinedData.data = [...allRows.slice(0, 10)]; |
|
testResults.combined.renderer = true; |
|
}, 100); |
|
}; |
|
const reloadCombinedData = () => { |
|
combinedData.data = [...allRows.slice(0, 10)]; |
|
}; |
|
|
|
// ================================================================ |
|
// SECTION 14: Renderer Edge Cases |
|
// ================================================================ |
|
const rendererType = ref<"jsx" | "simple">("jsx"); |
|
const edgeExample = reactive({ page: 1, size: 10 }); |
|
const edgeData = reactive({ data: allRows.slice(0, 10), total: allRows.length }); |
|
|
|
const edgeIdRenderer: CellRenderer<T> = ({ cellData }) => { |
|
return <span style="font-weight: bold; color: var(--el-color-primary)">#{cellData}</span>; |
|
}; |
|
|
|
const edgeNameRenderer: CellRenderer<T> = ({ cellData }) => { |
|
if (rendererType.value === "jsx") { |
|
return <span style="font-style: italic">{cellData}</span>; |
|
} |
|
return <span>{cellData}</span>; |
|
}; |
|
|
|
const edgePriorityRenderer: CellRenderer<T> = ({ cellData }) => { |
|
const colors: Record<string, string> = { high: "danger", medium: "warning", low: "success" }; |
|
return ( |
|
<ElTag type={colors[cellData]} size="small"> |
|
{cellData} |
|
</ElTag> |
|
); |
|
}; |
|
|
|
const edgeActionsRenderer: CellRenderer<T> = () => { |
|
return ( |
|
<div style="display: flex; gap: 4px;"> |
|
<ElButton size="small" type="primary"> |
|
Open |
|
</ElButton> |
|
</div> |
|
); |
|
}; |
|
|
|
const edgeColumns: ListTableColumn<T>[] = [ |
|
{ key: "id", name: "ID", fixed: "left" as const, width: 80, cellRenderer: edgeIdRenderer }, |
|
{ key: "caseName", name: "Case Name", cellRenderer: edgeNameRenderer }, |
|
{ key: "priority", name: "Priority", cellRenderer: edgePriorityRenderer }, |
|
{ key: "userId", name: "User ID" }, |
|
{ key: "createTime", name: "Create Time", timestamp: "unix" }, |
|
{ key: "actions", name: "Actions", fixed: "right" as const, width: 120, cellRenderer: edgeActionsRenderer }, |
|
]; |
|
|
|
const handleEdgeQuery = () => { |
|
const start = (edgeExample.page - 1) * edgeExample.size; |
|
edgeData.data = allRows.slice(start, start + edgeExample.size); |
|
}; |
|
|
|
const changePageRenderer = () => { |
|
edgeExample.page = (edgeExample.page % 5) + 1; |
|
handleEdgeQuery(); |
|
}; |
|
|
|
const toggleRendererType = () => { |
|
rendererType.value = rendererType.value === "jsx" ? "simple" : "jsx"; |
|
edgeData.data = [...allRows.slice(0, 10)]; |
|
}; |
|
|
|
onMounted(() => { |
|
console.log("Table V2 Demo mounted"); |
|
}); |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.table-v2-demo { |
|
padding: 0; |
|
min-height: 0; |
|
flex: 1; |
|
overflow-y: auto; |
|
} |
|
|
|
.demo-nav { |
|
position: sticky; |
|
top: 0; |
|
z-index: 100; |
|
background: var(--el-fill-color-light); |
|
border-bottom: 1px solid var(--el-border-color); |
|
padding: 8px 16px; |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 8px; |
|
max-height: 120px; |
|
overflow-y: auto; |
|
} |
|
|
|
.nav-link { |
|
font-size: 12px; |
|
color: var(--el-color-primary); |
|
text-decoration: none; |
|
padding: 2px 8px; |
|
border-radius: 4px; |
|
white-space: nowrap; |
|
|
|
&:hover { |
|
background: var(--el-color-primary-light-9); |
|
text-decoration: underline; |
|
} |
|
} |
|
|
|
.demo-section { |
|
padding: 24px 16px; |
|
border-bottom: 1px solid var(--el-border-color-lighter); |
|
|
|
&:nth-child(even) { |
|
background: var(--el-fill-color-lighter); |
|
} |
|
} |
|
|
|
.section-title { |
|
font-size: 18px; |
|
font-weight: 600; |
|
margin: 0 0 8px 0; |
|
color: var(--el-text-color-primary); |
|
|
|
code { |
|
font-size: 14px; |
|
background: var(--el-fill-color); |
|
padding: 2px 6px; |
|
border-radius: 4px; |
|
color: var(--el-color-primary); |
|
} |
|
} |
|
|
|
.section-desc { |
|
font-size: 14px; |
|
color: var(--el-text-color-secondary); |
|
margin: 0 0 16px 0; |
|
} |
|
|
|
.demo-toolbar { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 8px; |
|
margin-bottom: 12px; |
|
align-items: center; |
|
} |
|
|
|
.test-indicator { |
|
font-size: 11px; |
|
padding: 2px 8px; |
|
border-radius: 10px; |
|
background: var(--el-fill-color); |
|
color: var(--el-text-color-secondary); |
|
border: 1px solid var(--el-border-color-light); |
|
|
|
&.pass { |
|
background: var(--el-color-success-light-9); |
|
color: var(--el-color-success); |
|
border-color: var(--el-color-success-light-7); |
|
} |
|
|
|
&.fail { |
|
background: var(--el-color-danger-light-9); |
|
color: var(--el-color-danger); |
|
border-color: var(--el-color-danger-light-7); |
|
} |
|
|
|
&.pending { |
|
background: var(--el-fill-color); |
|
color: var(--el-text-color-placeholder); |
|
} |
|
} |
|
|
|
.demo-code { |
|
margin-top: 16px; |
|
background: var(--el-fill-color-dark); |
|
border-radius: 8px; |
|
padding: 12px 16px; |
|
overflow-x: auto; |
|
|
|
pre { |
|
margin: 0; |
|
} |
|
|
|
code { |
|
font-size: 12px; |
|
font-family: "Fira Code", "Consolas", monospace; |
|
color: var(--el-text-color-primary); |
|
white-space: pre; |
|
} |
|
} |
|
</style>
|
|
|