基于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.
 
 
 
 

1149 lines
40 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&lt;T&gt; = ({ cellData }) => {
const status = cellData as string;
const type = status === 'active' ? 'success' : status === 'pending' ? 'warning' : 'info';
return &lt;ElTag type={type}&gt;{status}&lt;/ElTag&gt;;
};
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&lt;T&gt; = ({ column }) => {
const col: ListTableColumn&lt;T&gt; = column._listTableColumn;
return (
&lt;span&gt;
&lt;span style="color: var(--el-color-primary)"&gt;*&lt;/span&gt; {col.title}
&lt;/span&gt;
);
};
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
// &lt;ListTableV2 :row-height="60" ...&gt; // No probe needed
// Manual estimate mode
// &lt;ListTableV2 :estimated-row-height="50" ...&gt; // 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
&lt;ListTableV2
:data="data"
:columns="columns"
:row-key="'id'"
:row-height="60"
:border="true"
debug
/&gt;</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
&lt;ListTableV2 :height="400" ...&gt;
// Max height - table grows but caps at max
&lt;ListTableV2 :max-height="300" ...&gt;
// Custom header height (default is auto-probed)
&lt;ListTableV2 :header-height="60" ...&gt;</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 }">
<div style="display: flex; flex: 1; justify-content: center; width: 100%; overflow: hidden; padding: 10% 20%">
<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>
</div>
</template>
</ListTableV2>
<div class="demo-code">
<pre><code>// In template
&lt;ListTableV2 :columns="slotColumns" ...&gt;
&lt;template #status="{ row }"&gt;
&lt;el-tag&gt;{row.status}&lt;/el-tag&gt;
&lt;/template&gt;
&lt;template #actions="{ row }"&gt;
&lt;el-button&gt;Edit&lt;/el-button&gt;
&lt;/template&gt;
&lt;/ListTableV2&gt;
// 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";
// --- 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<any> = ({ 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<any> = ({ 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 = ref(allRows.slice(0, 20));
const rendererColumns: ListTableColumn<any>[] = [
{ key: "id", name: "ID" },
{ key: "caseName", name: "Case Name" },
{ key: "status", name: "Status", cellRenderer: statusRenderer },
{
key: "priority",
name: "Priority",
cellRenderer: ({ cellData }) => {
const colors = { high: "danger" as const, medium: "warning" as const, low: "info" as const };
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.value = allRows.slice(0, 20);
};
// ================================================================
// SECTION 6: Custom Header Renderer
// ================================================================
const headerRenderer: HeaderCellRenderer<any> = ({ 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<any>[] = [
{ 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<any> = ({ cellData }) => {
return <span style="font-weight: bold; color: var(--el-color-primary)">#{cellData}</span>;
};
const edgeNameRenderer: CellRenderer<any> = ({ cellData }) => {
if (rendererType.value === "jsx") {
return <span style="font-style: italic">{cellData}</span>;
}
return <span>{cellData}</span>;
};
const edgePriorityRenderer: CellRenderer<any> = ({ cellData }) => {
const colors = { high: "danger" as const, medium: "warning" as const, low: "success" as const };
return (
<ElTag type={colors[cellData]} size="small">
{cellData}
</ElTag>
);
};
const edgeActionsRenderer: CellRenderer<any> = () => {
return (
<div style="display: flex; gap: 4px;">
<ElButton size="small" type="primary">
Open
</ElButton>
</div>
);
};
const edgeColumns: ListTableColumn<any>[] = [
{ 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>