Z-Table 虚拟滚动功能
📚 概述
Z-Table 组件现在支持基于 Element Plus TableV2 的虚拟滚动功能,可以高效处理大量数据,提供流畅的用户体验。
🚀 核心特性
- 高性能渲染: 基于 Element Plus TableV2,支持 10 万+ 数据流畅滚动
- 智能切换: 自动或手动在普通表格和虚拟表格间切换
- 完整功能: 支持排序、固定列、多选等核心表格功能
- 样式一致: 保持与原表格相同的视觉体验
- 极致性能: DOM 节点数量恒定,内存占用稳定
✅ 兼容性说明
- 完全兼容: 排序、筛选、多选、选择功能、展开功能、索引列、固定列、样式主题、列显隐、列提示、水印功能
- 高级功能: 可编辑表格、自定义列渲染、自定义表头、插槽模板
- API 一致: 与普通表格相同的 props 和事件
- 渐进增强: 可在运行时动态开启/关闭
🚀 快速开始
三步启用虚拟滚动
<!-- 步骤1: 添加 virtual 属性 -->
<!-- 步骤2: 设置固定高度 -->
<!-- 步骤3: 配置列信息 -->
<z-table
:data="largeData"
:columns="columns"
:virtual="true"
height="400px"
/>
基础配置示例
<template>
<ZTable
:data="tableData"
:columns="columns"
:virtual="true"
height="600px"
/>
</template>
<script setup>
// 列配置无需指定 width,系统会自动处理
const columns = [
{ prop: 'name', label: '姓名' },
{ prop: 'email', label: '邮箱' },
{ prop: 'department', label: '部门' }
]
</script>
高级配置示例
<template>
<ZTable
:data="tableData"
:columns="columns"
:virtual="virtualConfig"
height="600px"
/>
</template>
<script setup>
const virtualConfig = {
enabled: true,
itemHeight: 48, // 行高度
threshold: 100, // 启用阈值
footerHeight: 60 // Footer 高度
}
</script>
🎯 基础功能
基础表格
基础的虚拟表格功能。支持大量数据的高效渲染。
<!-- eslint-disable no-console -->
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { TableV2FixedDir } from 'element-plus'
// 生成测试数据
function generateData(count: number) {
const data = []
for (let i = 1; i <= count; i++) {
data.push({
id: i,
name: `用户${i}`,
email: `user${i}@example.com`,
phone: `138${String(i).padStart(8, '0')}`,
department: ['技术部', '产品部', '设计部', '运营部'][i % 4],
position: ['工程师', '产品经理', '设计师', '运营专员'][i % 4],
status: ['在职', '离职', '试用期'][i % 3],
createTime: new Date(2024, 0, 1 + (i % 365)).toLocaleDateString(),
})
}
return data
}
const tableData = ref(generateData(5000))
const dataCount = ref(5000)
const columns = ref([
{
prop: 'id',
label: 'ID',
},
{
prop: 'name',
label: '姓名',
fixed: TableV2FixedDir.LEFT
},
{
prop: 'email',
label: '邮箱',
},
{
prop: 'phone',
label: '手机号',
},
{
prop: 'department',
label: '部门',
},
{
prop: 'position',
label: '职位',
},
{
prop: 'status',
label: '状态',
},
{
prop: 'createTime',
label: '创建时间',
fixed: TableV2FixedDir.RIGHT,
},
])
// 虚拟滚动配置
const virtualConfig = computed(() => ({
enabled: true,
itemHeight: 48,
threshold: 100,
}))
function updateData() {
tableData.value = generateData(dataCount.value)
}
function handleRefresh() {
console.log('刷新数据')
updateData()
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<el-space wrap>
<span>数据量:</span>
<el-input-number
v-model="dataCount"
:min="100"
:max="50000"
:step="1000"
controls-position="right"
style="width: 150px;"
/>
<el-button @click="updateData" type="primary">
更新数据
</el-button>
<span style="color: #666;">当前: {{ tableData.length.toLocaleString() }} 条</span>
</el-space>
</div>
<z-table
v-model:data="tableData"
:columns="columns"
:virtual="virtualConfig"
height="500px"
border
stripe
@refresh="handleRefresh"
/>
</div>
</template>
操作按钮
验证操作按钮在虚拟表格中的完整功能,包括点击事件、样式等。
虚拟表格操作按钮演示
测试操作按钮在虚拟表格中的兼容性,包括点击事件、样式等。
<!-- eslint-disable no-console -->
<script lang="ts" setup>
import { computed, h, ref, resolveComponent } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
interface RowData {
id: number
name: string
email: string
department: string
salary: number
status: string
}
// 生成测试数据
function generateData(count: number): RowData[] {
const names = ['张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '郑十', '冯一', '陈二']
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部']
const statuses = ['在职', '离职', '试用期']
const data: RowData[] = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}-${i + 1}`,
email: `user${i + 1}@example.com`,
department: departments[i % departments.length],
salary: Math.floor(Math.random() * 50000) + 8000,
status: statuses[i % statuses.length],
})
}
return data
}
const tableData = ref<RowData[]>(generateData(1000))
const columns = ref([
{
prop: 'id',
label: 'ID',
},
{
prop: 'name',
label: '姓名',
},
{
prop: 'email',
label: '邮箱',
},
{
prop: 'department',
label: '部门',
},
{
prop: 'salary',
label: '薪资',
render: ({ row }: any) => `¥${row.salary.toLocaleString()}`,
},
{
prop: 'status',
label: '状态',
},
{
prop: 'operation',
label: '操作',
render: (scope: any) => [
h(resolveComponent('el-button'), {
type: 'primary',
onClick: () => handleEdit(scope.row)
}, () => '编辑'),
h(resolveComponent('el-button'), {
type: 'danger',
onClick: () => handleDelete(scope.row)
}, () => '删除')
],
},
])
// 虚拟滚动配置
const virtualConfig = computed(() => ({
enabled: true,
itemHeight: 48,
threshold: 100,
}))
function handleEdit(row: RowData) {
console.log('编辑操作:', row)
ElMessage.success(`开始编辑: ${row.name}`)
}
function handleDelete(row: RowData) {
console.log('删除操作:', row)
ElMessageBox.confirm(`确定要删除 ${row.name} 吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
const index = tableData.value.findIndex(item => item.id === row.id)
if (index > -1) {
tableData.value.splice(index, 1)
ElMessage.success('删除成功')
}
}).catch(() => {
ElMessage.info('已取消删除')
})
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<h3>虚拟表格操作按钮演示</h3>
<p style="margin: 8px 0; color: #666;">
测试操作按钮在虚拟表格中的兼容性,包括点击事件、样式等。
</p>
</div>
<z-table
v-model:data="tableData"
:columns="columns"
:virtual="virtualConfig"
height="500px"
border
stripe
/>
</div>
</template>
列类型
测试各种列类型在虚拟表格中的兼容性:选择列、索引列、表单组件等。
虚拟表格列类型演示
测试各种列类型在虚拟表格中的兼容性:选择列、索引列、表单组件等。
已选择: 0 项
<!-- eslint-disable no-console -->
<script lang="ts" setup>
import { computed, h, ref } from 'vue'
interface RowData {
id: number
name: string
email: string
department: string
position: string
salary: number
status: string
}
// 生成测试数据
function generateData(count: number): RowData[] {
const names = ['张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '郑十', '冯一', '陈二']
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部']
const positions = ['工程师', '产品经理', '设计师', '运营专员', '市场专员']
const statuses = ['在职', '离职', '试用期']
const data: RowData[] = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}-${i + 1}`,
email: `user${i + 1}@example.com`,
department: departments[i % departments.length],
position: positions[i % positions.length],
salary: Math.floor(Math.random() * 50000) + 8000,
status: statuses[i % statuses.length],
})
}
return data
}
const tableData = ref<RowData[]>(generateData(1000))
const columns = ref([
{
type: 'selection',
label: '选择',
},
{
type: 'index',
label: '序号',
index: 1,
},
{
prop: 'id',
label: 'ID',
},
{
prop: 'name',
label: '姓名',
},
{
prop: 'email',
label: '邮箱',
},
{
prop: 'department',
label: '部门',
},
{
prop: 'position',
label: '职位',
},
{
prop: 'salary',
label: '薪资',
render: ({ row }: any) => `¥${row.salary.toLocaleString()}`,
},
{
prop: 'status',
label: '状态',
render: ({ row }: any) => {
const type = row.status === '在职' ? 'success' : row.status === '试用期' ? 'warning' : 'danger'
return h('el-tag', { type, size: 'small' }, row.status)
},
},
])
// 虚拟滚动配置
const virtualConfig = computed(() => ({
enabled: true,
itemHeight: 48,
threshold: 100,
}))
const selectedRows = ref<RowData[]>([])
function handleSelectionChange(selection: RowData[]) {
selectedRows.value = selection
console.log('选择变化:', selection)
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<h3>虚拟表格列类型演示</h3>
<p style="margin: 8px 0; color: #666;">
测试各种列类型在虚拟表格中的兼容性:选择列、索引列、表单组件等。
</p>
<p style="margin: 8px 0; color: #666;">已选择: {{ selectedRows.length }} 项</p>
</div>
<z-table
v-model:data="tableData"
:columns="columns"
:virtual="virtualConfig"
height="500px"
border
stripe
@selection-change="handleSelectionChange"
/>
</div>
</template>
列选择
测试 type: 'selection'
列在虚拟表格中的支持情况。
功能特点:
- ✅ 多选支持
- ✅ 全选/取消全选
- ✅ 选中状态回调
- ✅ 禁用状态支持
虚拟表格选择功能演示
虚拟表格完全支持多选功能,支持全选、清空选择、以及各种选择操作。
<!-- eslint-disable no-console -->
<script lang="ts" setup>
import { computed, ref } from 'vue'
interface RowData {
id: number
name: string
email: string
department: string
position: string
salary: number
status: string
}
// 生成测试数据
function generateData(count: number): RowData[] {
const names = ['张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '郑十', '冯一', '陈二']
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部']
const positions = ['工程师', '产品经理', '设计师', '运营专员', '市场专员']
const statuses = ['在职', '离职', '试用期']
const data: RowData[] = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}-${i + 1}`,
email: `user${i + 1}@example.com`,
department: departments[i % departments.length],
position: positions[i % positions.length],
salary: Math.floor(Math.random() * 50000) + 8000,
status: statuses[i % statuses.length],
})
}
return data
}
const tableData = ref<RowData[]>(generateData(2000))
const selectedRows = ref<RowData[]>([])
const columns = ref([
{
type: 'selection',
label: '选择',
},
{
prop: 'id',
label: 'ID',
},
{
prop: 'name',
label: '姓名',
},
{
prop: 'email',
label: '邮箱',
},
{
prop: 'department',
label: '部门',
},
{
prop: 'position',
label: '职位',
},
{
prop: 'salary',
label: '薪资',
render: ({ row }: any) => `¥${row.salary.toLocaleString()}`,
},
{
prop: 'status',
label: '状态',
},
])
// 虚拟滚动配置
const virtualConfig = computed(() => ({
enabled: true,
itemHeight: 48,
threshold: 100,
}))
const tableRef = ref()
function handleSelectionChange(selection: RowData[]) {
console.log('选择变化:', selection)
selectedRows.value = selection
}
function selectAll() {
tableRef.value?.toggleAllSelection()
}
function clearSelection() {
tableRef.value?.clearSelection()
selectedRows.value = []
}
function selectFirst10() {
clearSelection()
for (let i = 0; i < Math.min(10, tableData.value.length); i++) {
tableRef.value?.toggleRowSelection(tableData.value[i], true)
}
}
function selectHighSalary() {
clearSelection()
const highSalaryRows = tableData.value.filter(row => row.salary > 30000)
highSalaryRows.forEach(row => {
tableRef.value?.toggleRowSelection(row, true)
})
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<h3>虚拟表格选择功能演示</h3>
<p style="margin: 8px 0; color: #666;">
虚拟表格完全支持多选功能,支持全选、清空选择、以及各种选择操作。
</p>
<el-space wrap>
<el-button @click="selectAll" type="primary">
全选
</el-button>
<el-button @click="clearSelection" type="warning">
清空选择
</el-button>
<el-button @click="selectFirst10" type="success">
选择前10条
</el-button>
<el-button @click="selectHighSalary" type="info">
选择高薪人员(>3万)
</el-button>
<span style="color: #666;">已选择: {{ selectedRows.length }} 项</span>
</el-space>
</div>
<z-table
ref="tableRef"
v-model:data="tableData"
:columns="columns"
:virtual="virtualConfig"
height="500px"
border
stripe
@selection-change="handleSelectionChange"
/>
<div v-if="selectedRows.length > 0" style="padding: 16px; margin-top: 16px; background: #f5f7fa; border-radius: 4px;">
<h4 style="margin: 0 0 8px; color: #303133;">已选择的数据 ({{ selectedRows.length }}条):</h4>
<div style="max-height: 200px; overflow-y: auto;">
<div
v-for="row in selectedRows"
:key="row.id"
style="
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
margin-bottom: 4px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
"
>
<span><strong>{{ row.name }}</strong> ({{ row.email }})</span>
<span style="color: #67c23a;">¥{{ row.salary.toLocaleString() }}</span>
</div>
</div>
</div>
</div>
</template>
列展开
虚拟表格完全支持展开功能,基于Element Plus TableV2原生能力。
功能特点:
- ✅ 展开列配置:
{ type: 'expand' }
列配置,TableV2自动为有children的行添加展开按钮 - ✅ 双向绑定:
v-model:expanded-row-keys
支持双向绑定展开状态 - ✅ 事件支持:
@expand-change
、@row-expand
事件 (TableV2原生事件) - ✅ 方法支持:
toggleRowExpansion
方法 - ✅ 插槽支持:
#expand
插槽自定义展开内容 - ✅ 数据结构:支持children字段的嵌套数据结构
虚拟表格 - 展开功能
展开状态:
当前展开的行ID:无
展开的行数量:0
<template>
<div>
<h3>虚拟表格 - 展开功能</h3>
<div style="margin-bottom: 16px;">
<el-space>
<el-button @click="expandAll">
展开所有
</el-button>
<el-button @click="collapseAll">
收起所有
</el-button>
<el-button @click="expandFirst5">
展开前5行
</el-button>
</el-space>
</div>
<el-alert
title="✅ 展开功能已完成"
type="success"
:closable="false"
style="margin-bottom: 16px;"
>
<template #default>
<p><strong>✅ 展开列支持</strong>:完全支持 <code>type: 'expand'</code> 列配置,TableV2自动处理有children的行</p>
<p><strong>✅ 双向绑定</strong>:<code>v-model:expanded-row-keys</code> 支持双向绑定展开状态</p>
<p><strong>✅ 事件支持</strong>:<code>@expand-change</code>、<code>@row-expand</code> 事件</p>
<p><strong>✅ 方法支持</strong>:<code>toggleRowExpansion</code> 方法</p>
<p><strong>✅ 插槽支持</strong>:<code>#expand</code> 插槽自定义展开内容</p>
</template>
</el-alert>
<z-table
ref="tableRef"
:data="tableData"
:columns="columns"
v-model:expanded-row-keys="expandedKeys"
:virtual="{ enabled: true, itemHeight: 48, threshold: 100, estimatedRowHeight: 48 }"
height="500px"
border
stripe
row-key="id"
@expanded-rows-change="handleExpandedRowsChange"
@row-expand="handleRowExpand"
>
<template #expand="props">
<Row v-bind="props" />
</template>
</z-table>
<div style="margin-top: 16px;">
<h4>展开状态:</h4>
<p>当前展开的行ID:{{ expandedKeys.join(', ') || '无' }}</p>
<p>展开的行数量:{{ expandedKeys.length }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { h, ref } from 'vue'
const detailedText = `这是详细的展开内容。包含了工作经历、技能特长、项目经验等详细信息。
这个内容比较长,适合在展开区域中显示。支持多行文本和富文本内容。
可以包含各种复杂的UI组件和交互元素。`
// 生成测试数据
function generateData() {
const data = []
for (let i = 1; i <= 2000; i++) {
const rowData: any = {
id: i,
name: `用户 ${i}`,
email: `user${i}@example.com`,
age: 20 + (i % 50),
department: ['技术部', '市场部', '产品部', '运营部'][i % 4],
salary: 5000 + (i % 100) * 100,
createTime: new Date(2020 + (i % 4), (i % 12), (i % 28) + 1).toLocaleDateString(),
status: i % 3 === 0 ? 'active' : 'inactive',
}
// 添加children用于展开内容
rowData.children = [
{
id: `${rowData.id}-detail-content`,
detail: detailedText,
parentData: rowData
}
]
data.push(rowData)
}
return data
}
const tableData = ref(generateData())
const expandedKeys = ref<(string | number)[]>([])
const tableRef = ref()
// Row组件用于渲染行内容
function Row({ cells, rowData }: any) {
if (rowData.detail) {
return h('div', {
class: 'z-table-expand-content',
style: { padding: '20px', backgroundColor: '#f5f7fa' }
}, [
h('h4', {}, `${rowData.parentData?.name} 的详细信息`),
h('div', { style: { marginTop: '16px' } }, [
h('p', {}, [
h('strong', {}, () => '详细描述:'),
rowData.detail
]),
h('p', {}, [
h('strong', {}, () => '创建时间:'),
rowData.parentData?.createTime
]),
h('p', {}, [
h('strong', {}, () => '状态:'),
h('span', {
style: {
color: rowData.parentData?.status === 'active' ? '#67C23A' : '#E6A23C',
fontWeight: 'bold'
}
}, rowData.parentData?.status === 'active' ? '活跃' : '非活跃')
])
])
])
}
return cells
}
Row.inheritAttrs = false
// 列配置
const columns = [
{
type: 'expand',
width: 60
},
{
prop: 'id',
label: 'ID',
},
{
prop: 'name',
label: '姓名',
},
{
prop: 'email',
label: '邮箱',
},
{
prop: 'age',
label: '年龄',
},
{
prop: 'department',
label: '部门',
},
{
prop: 'salary',
label: '薪资',
render: ({ row }: any) => `¥${row.salary.toLocaleString()}`
}
]
// 展开行keys变化事件
function handleExpandedRowsChange(_keys: (string | number)[]) {
// console.log('展开行keys变化 (expanded-rows-change):', _keys)
}
// 行展开事件
function handleRowExpand(_params: any) {
// console.log('行展开事件 (row-expand):', _params)
}
// 展开所有
function expandAll() {
expandedKeys.value = tableData.value.map(row => row.id)
}
// 收起所有
function collapseAll() {
expandedKeys.value = []
}
// 展开前5行
function expandFirst5() {
expandedKeys.value = tableData.value.slice(0, 5).map(row => row.id)
}
</script>
<style scoped>
.z-table-expand-content {
background-color: #f5f7fa;
}
</style>
索引列
虚拟表格完全支持索引列功能。
功能特点:
- ✅ 索引列配置:
{ type: 'index' }
列配置 - ✅ 自定义起始索引:支持数字类型的index属性
- ✅ 函数索引:支持函数类型的index属性自定义显示逻辑
- ✅ 分页兼容:自动处理分页情况下的索引偏移
- ✅ 高性能:虚拟滚动下的索引列渲染优化
虚拟表格索引列演示
虚拟表格完全支持索引列功能,支持数字索引和自定义函数索引。
<!-- eslint-disable no-console -->
<script lang="ts" setup>
import { computed, ref } from 'vue'
interface RowData {
id: number
name: string
email: string
department: string
position: string
salary: number
}
// 生成测试数据
function generateData(count: number): RowData[] {
const names = ['张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '郑十', '冯一', '陈二']
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部']
const positions = ['工程师', '产品经理', '设计师', '运营专员', '市场专员']
const data: RowData[] = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}-${i + 1}`,
email: `user${i + 1}@example.com`,
department: departments[i % departments.length],
position: positions[i % positions.length],
salary: Math.floor(Math.random() * 50000) + 8000,
})
}
return data
}
const tableData = ref<RowData[]>(generateData(2000))
const indexType = ref('number')
const columns = computed(() => [
{
type: 'index',
label: '序号',
index: indexType.value === 'number' ? 1 :
indexType.value === 'function' ? (index: number) => `No.${index + 1}` :
undefined,
},
{
prop: 'id',
label: 'ID',
},
{
prop: 'name',
label: '姓名',
},
{
prop: 'email',
label: '邮箱',
},
{
prop: 'department',
label: '部门',
},
{
prop: 'position',
label: '职位',
},
{
prop: 'salary',
label: '薪资',
render: ({ row }: any) => `¥${row.salary.toLocaleString()}`,
},
])
// 虚拟滚动配置
const virtualConfig = computed(() => ({
enabled: true,
itemHeight: 48,
threshold: 100,
}))
function changeIndexType(type: string) {
indexType.value = type
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<h3>虚拟表格索引列演示</h3>
<p style="margin: 8px 0; color: #666;">
虚拟表格完全支持索引列功能,支持数字索引和自定义函数索引。
</p>
<el-space wrap>
<span>索引类型:</span>
<el-radio-group v-model="indexType" @change="changeIndexType">
<el-radio-button value="number">数字索引</el-radio-button>
<el-radio-button value="function">函数索引</el-radio-button>
<el-radio-button value="default">默认索引</el-radio-button>
</el-radio-group>
</el-space>
</div>
<z-table
v-model:data="tableData"
:columns="columns"
:virtual="virtualConfig"
height="500px"
border
stripe
/>
</div>
</template>
<style scoped>
ul {
padding-left: 20px;
margin: 0;
}
li {
margin: 4px 0;
}
</style>
可编辑表格
测试大数据量下的表格编辑功能,支持多种表单组件。
支持的组件:
- 📝 输入框 (input)
- 🎯 选择器 (select)
- 🔢 数字输入框 (input-number)
- 📅 日期选择器 (date-picker)
可编辑表格功能测试 (800条数据)
💡 提示:
- 支持多种表单组件:输入框、选择器、数字输入框、日期选择器
- 所有修改会实时同步到数据模型
- 虚拟滚动确保大数据量下的编辑性能
<!-- eslint-disable no-console -->
<script lang="ts" setup>
import { ref } from 'vue'
// 生成大量测试数据
function generateLargeData(count: number) {
const names = ['Steven', 'Helen', 'Nancy', 'Jack', 'Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
const genders = ['1', '2']
const data = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}-${i + 1}`,
gender: genders[i % genders.length],
age: 18 + (i % 50),
time: `202${(i % 4) + 0}-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`,
salary: Math.floor(Math.random() * 50000) + 5000,
department: ['技术部', '产品部', '设计部'][i % 3],
})
}
return data
}
const tableData = ref(generateLargeData(800))
const columns = ref([
{
prop: 'id',
label: 'ID',
width: 100,
},
{
component: 'input',
prop: 'name',
label: '姓名',
fieldProps: {
placeholder: '请输入姓名',
},
},
{
component: 'select',
prop: 'gender',
label: '性别',
fieldProps: {
placeholder: '请选择性别',
},
},
{
component: 'el-input-number',
prop: 'age',
label: '年龄',
fieldProps: {
min: 18,
max: 65,
},
},
{
component: 'el-input-number',
prop: 'salary',
label: '薪资',
fieldProps: {
min: 0,
controls: false,
},
},
{
component: 'select',
prop: 'department',
label: '部门',
},
{
component: 'el-date-picker',
prop: 'time',
label: '入职日期',
fieldProps: {
valueFormat: 'YYYY-MM-DD',
placeholder: '选择日期',
},
},
])
const options = {
gender: [
{ label: '男', value: '1' },
{ label: '女', value: '2' },
],
department: [
{ label: '技术部', value: '技术部' },
{ label: '产品部', value: '产品部' },
{ label: '设计部', value: '设计部' },
{ label: '运营部', value: '运营部' },
{ label: '市场部', value: '市场部' },
],
}
function handleSave() {
console.log('保存数据:', tableData.value)
}
function handleReset() {
tableData.value = generateLargeData(800)
console.log('重置数据完成')
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<h4>可编辑表格功能测试 (800条数据)</h4>
<el-space>
<el-button type="primary" @click="handleSave">
保存数据
</el-button>
<el-button @click="handleReset">
重置数据
</el-button>
</el-space>
</div>
<z-table
v-model:data="tableData"
:columns="columns"
:options="options"
:editable="true"
:virtual="{ enabled: true, itemHeight: 48, threshold: 100 }"
height="500px"
border
stripe
/>
<div style="margin-top: 16px; color: #666; font-size: 14px;">
<p>💡 提示:</p>
<ul>
<li>支持多种表单组件:输入框、选择器、数字输入框、日期选择器</li>
<li>所有修改会实时同步到数据模型</li>
<li>虚拟滚动确保大数据量下的编辑性能</li>
</ul>
</div>
</div>
</template>
表单组件支持
虚拟表格完全支持各种表单组件,包括输入框、选择器、单选框、多选框、日期选择器、开关等。
支持的表单组件:
- 📝 输入框 (input):基础文本输入,支持 placeholder、验证等
- 🎯 选择器 (select):单选和多选下拉选择,支持 options 配置
- 🔢 数字输入框 (input-number):数字输入,支持 min/max 限制
- 📊 单选框 (radio):单选组件,支持 options 配置
- ☑️ 多选框 (checkbox):多选组件,支持 options 配置
- 📅 日期选择器 (date-picker):日期选择,支持格式化配置
- 🔘 开关 (switch):开关组件,支持自定义文本
功能特点:
- ✅ 高性能渲染:虚拟滚动下的表单组件渲染优化
- ✅ 完整验证:支持必填、自定义验证规则
- ✅ 选项配置:支持 options 配置选择类组件
- ✅ 事件处理:支持各种表单事件
- ✅ 样式统一:与普通表格表单组件样式一致
虚拟表格表单组件功能测试
表单组件类型说明:
- 姓名:input 输入框
- 性别:select 下拉选择
- 年龄:input-number 数字输入框
- 部门:select 多选下拉
- 评级:radio 单选框
- 技能:checkbox 多选框
- 入职日期:date-picker 日期选择
- 状态:switch 开关
<!-- eslint-disable no-console -->
<template>
<div class="virtual-form-components-demo">
<h2>虚拟表格表单组件功能测试</h2>
<div style="margin-bottom: 16px;">
<ElSpace>
<ElButton @click="generateData(1000)">生成 1000 条数据</ElButton>
<ElButton @click="generateData(5000)">生成 5000 条数据</ElButton>
<ElButton @click="validateData">验证数据</ElButton>
<ElButton @click="showSelected">显示选中数据</ElButton>
</ElSpace>
</div>
<ZTable
ref="tableRef"
:data="tableData"
:columns="columns"
:virtual="true"
:options="formOptions"
height="600px"
@update:data="handleDataUpdate"
@selection-change="handleSelectionChange"
/>
<div style="margin-top: 16px;">
<h3>表单组件类型说明:</h3>
<ul>
<li><strong>姓名</strong>:input 输入框</li>
<li><strong>性别</strong>:select 下拉选择</li>
<li><strong>年龄</strong>:input-number 数字输入框</li>
<li><strong>部门</strong>:select 多选下拉</li>
<li><strong>评级</strong>:radio 单选框</li>
<li><strong>技能</strong>:checkbox 多选框</li>
<li><strong>入职日期</strong>:date-picker 日期选择</li>
<li><strong>状态</strong>:switch 开关</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElButton, ElMessage, ElSpace } from 'element-plus'
const tableRef = ref()
const tableData = ref<any[]>([])
// 表单选项配置
const formOptions = computed(() => ({
gender: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '其他', value: 'other' }
],
department: [
{ label: '技术部', value: 'tech' },
{ label: '产品部', value: 'product' },
{ label: '设计部', value: 'design' },
{ label: '运营部', value: 'operation' },
{ label: '市场部', value: 'marketing' }
],
rating: [
{ label: 'A级', value: 'A' },
{ label: 'B级', value: 'B' },
{ label: 'C级', value: 'C' },
{ label: 'D级', value: 'D' }
],
skills: [
{ label: 'JavaScript', value: 'js' },
{ label: 'Vue', value: 'vue' },
{ label: 'React', value: 'react' },
{ label: 'Node.js', value: 'nodejs' },
{ label: 'Python', value: 'python' }
]
}))
// 表格列配置
const columns = ref([
{
type: 'selection',
label: '选择',
},
{
type: 'index',
label: '序号',
index: 1,
},
{
prop: 'name',
label: '姓名',
component: 'input',
required: true,
fieldProps: {
placeholder: '请输入姓名',
},
},
{
prop: 'gender',
label: '性别',
component: 'select',
fieldProps: {
placeholder: '请选择性别',
},
},
{
prop: 'age',
label: '年龄',
component: 'el-input-number',
fieldProps: {
min: 18,
max: 100,
placeholder: '请输入年龄',
},
},
// {
// prop: 'department',
// label: '部门',
// component: 'select',
// fieldProps: {
// placeholder: '请选择部门',
// multiple: true,
// appendToBody: true,
// },
// },
{
prop: 'rating',
label: '评级',
component: 'radio',
fieldProps: {
placeholder: '请选择评级',
},
},
{
prop: 'skills',
label: '技能',
component: 'checkbox',
fieldProps: {
placeholder: '请选择技能',
},
},
// {
// prop: 'joinDate',
// label: '入职日期',
// component: 'el-date-picker',
// fieldProps: {
// type: 'date',
// placeholder: '请选择入职日期',
// format: 'YYYY-MM-DD',
// valueFormat: 'YYYY-MM-DD',
// },
// },
{
prop: 'status',
label: '状态',
component: 'el-switch',
fieldProps: {
activeText: '在职',
inactiveText: '离职',
},
},
])
// 生成测试数据
function generateData(count: number) {
const names = ['张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '郑十']
const genders = ['male', 'female', 'other']
const departments = ['tech', 'product', 'design', 'operation', 'marketing']
const ratings = ['A', 'B', 'C', 'D']
const skills = ['js', 'vue', 'react', 'nodejs', 'python']
const data = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}_${i + 1}`,
gender: genders[i % genders.length],
age: Math.floor(Math.random() * 40) + 20,
department: [departments[i % departments.length], departments[(i + 1) % departments.length]],
rating: ratings[i % ratings.length],
skills: [skills[i % skills.length], skills[(i + 1) % skills.length]],
joinDate: `2020-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
status: Math.random() > 0.5,
__isEdit: false,
})
}
tableData.value = data
}
// 处理数据更新
function handleDataUpdate(newData: any[]) {
tableData.value = newData
// console.log('数据更新:', newData)
}
// 验证数据
function validateData() {
const invalidData = tableData.value.filter(item =>
!item.name || !item.gender || !item.age || !item.joinDate
)
if (invalidData.length > 0) {
ElMessage.error(`发现 ${invalidData.length} 条无效数据`)
// console.log('无效数据:', invalidData)
} else {
ElMessage.success('所有数据验证通过')
}
}
// 显示选中数据
function showSelected() {
// eslint-disable-next-line no-console
console.log('数据打印', tableData.value)
}
function handleSelectionChange(selection: any[]) {
// eslint-disable-next-line no-console
console.log('选中数据', selection)
}
// 初始化数据
generateData(1000)
</script>
<style scoped>
.virtual-form-components-demo {
padding: 20px;
}
h2 {
margin-bottom: 20px;
color: #333;
}
h3 {
margin-bottom: 10px;
color: #666;
}
ul {
padding-left: 20px;
margin: 0;
}
li {
margin-bottom: 5px;
color: #666;
}
</style>
列显隐功能
测试动态显示和隐藏列的功能,支持函数式和静态配置。
功能特点:
- 🔄 动态显示/隐藏列
- 🎯 函数式配置:
hide: () => boolean
- 🔧 静态配置:
hide: boolean
- 📊 实时响应状态变化
列显隐功能测试 (2000条数据)
<script lang="ts" setup>
import { ref } from 'vue'
const isHide = ref(false)
// 生成大量测试数据
function generateLargeData(count: number) {
const names = ['Steven', 'Helen', 'Nancy', 'Jack', 'Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
const genders = ['male', 'female']
const data = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}-${i + 1}`,
gender: genders[i % genders.length],
age: 18 + (i % 50),
time: `202${(i % 4) + 0}-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`,
})
}
return data
}
const tableData = ref(generateLargeData(2000))
const columns = ref([
{
prop: 'id',
label: 'ID',
width: 200,
},
{
prop: 'name',
label: '姓名',
hide: () => isHide.value,
width: 200
},
{
prop: 'gender',
label: '性别',
hide: true, // 默认隐藏
width: 200
},
{
prop: 'age',
label: '年龄',
width: 200
},
{
prop: 'time',
label: '出生日期',
width: 200
},
])
function changeVisible() {
isHide.value = !isHide.value
}
function toggleGender() {
const genderColumn = columns.value.find(col => col.prop === 'gender')
if (genderColumn) {
genderColumn.hide = !genderColumn.hide
}
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<h4>列显隐功能测试 (2000条数据)</h4>
<el-space>
<el-button @click="changeVisible">
{{ isHide ? '显示' : '隐藏' }}姓名列
</el-button>
<el-button @click="toggleGender">
切换性别列显隐
</el-button>
</el-space>
</div>
<z-table
:data="tableData"
:columns="columns"
:virtual="{ enabled: true, itemHeight: 48, threshold: 100 }"
height="400px"
border
stripe
/>
</div>
</template>
列提示功能
虚拟表格完全支持表头tooltip功能,与z-table普通表格的tooltip实现保持一致。
功能特点:
- ✅ 字符串提示:
tooltip: '提示内容'
- ✅ 函数提示:
tooltip: (scope) => '动态内容'
,支持传递scope参数 - ✅ 对象配置:
tooltip: { content: '内容', placement: 'top', effect: 'dark' }
- ✅ 样式一致:与普通表格的tooltip样式和交互完全一致
- ✅ 完整兼容:支持useTableColumnSlots中的所有tooltip配置选项
- ✅ 性能优化:修复了表头抖动和Vue警告问题
虚拟表格 - 列提示功能
<script lang="ts" setup>
import { ref } from 'vue'
// 生成测试数据
function generateData() {
const departments = ['技术部', '市场部', '产品部', '运营部', '设计部']
const levels = ['初级', '中级', '高级', '专家']
const data = []
for (let i = 1; i <= 2000; i++) {
data.push({
id: i,
name: `用户 ${i}`,
email: `user${i}@example.com`,
age: 20 + (i % 50),
department: departments[i % departments.length],
salary: 5000 + (i % 100) * 100,
level: levels[i % levels.length],
status: i % 3 === 0 ? '在职' : '离职',
joinDate: new Date(2020 + (i % 4), (i % 12), (i % 28) + 1).toLocaleDateString(),
performance: Math.floor(Math.random() * 100),
})
}
return data
}
const tableData = ref(generateData())
// 计算统计数据
const totalUsers = tableData.value.length
const avgSalary = Math.round(tableData.value.reduce((sum, item) => sum + item.salary, 0) / totalUsers)
const departmentCount = new Set(tableData.value.map(item => item.department)).size
// 列配置
const columns = [
{
type: 'index',
label: '序号',
tooltip: '显示数据行的序号,从1开始计数'
},
{
prop: 'name',
label: '用户姓名',
tooltip: '用户的真实姓名,用于标识用户身份'
},
{
prop: 'email',
label: '邮箱地址',
tooltip: {
content: '用户的电子邮箱地址,用于联系和通知',
placement: 'bottom',
effect: 'light'
}
},
{
prop: 'age',
label: '年龄',
tooltip: (scope: any) => {
const minAge = Math.min(...tableData.value.map(item => item.age))
const maxAge = Math.max(...tableData.value.map(item => item.age))
return `年龄范围:${minAge} - ${maxAge} 岁 (列索引: ${scope.$index})`
}
},
{
prop: 'department',
label: '所属部门',
tooltip: {
content: (scope: any) => {
return `共有 ${departmentCount} 个部门 (列: ${scope.column.prop})`
},
placement: 'top-start',
effect: 'dark'
}
},
{
prop: 'salary',
label: '薪资待遇',
tooltip: (scope: any) => {
return `平均薪资:¥${avgSalary.toLocaleString()} (列索引: ${scope.$index})`
}
},
{
prop: 'level',
label: '职级',
tooltip: {
content: '员工的职业等级,分为初级、中级、高级、专家四个级别',
placement: 'right',
showAfter: 500,
hideAfter: 100
}
},
{
prop: 'status',
label: '在职状态',
tooltip: {
content: (scope: any) => {
const activeCount = tableData.value.filter(item => item.status === '在职').length
const inactiveCount = totalUsers - activeCount
return `在职:${activeCount} 人,离职:${inactiveCount} 人 (列: ${scope.column.label})`
},
placement: 'left',
effect: 'light'
}
},
{
prop: 'joinDate',
label: '入职日期',
tooltip: '员工加入公司的日期'
},
{
prop: 'performance',
label: '绩效得分',
tooltip: {
content: (scope: any) => {
const avgPerformance = Math.round(
tableData.value.reduce((sum, item) => sum + item.performance, 0) / totalUsers
)
return `平均绩效得分:${avgPerformance} 分 (列索引: ${scope.$index})`
},
placement: 'bottom-end',
effect: 'dark',
showAfter: 300
}
}
]
</script>
<template>
<div>
<h3>虚拟表格 - 列提示功能</h3>
<el-alert
title="✅ 表头Tooltip功能"
type="success"
:closable="false"
style="margin-bottom: 16px;"
>
<template #default>
<p><strong>✅ 字符串提示</strong>:直接使用字符串作为tooltip内容</p>
<p><strong>✅ 函数提示</strong>:使用函数动态生成tooltip内容,支持scope参数传递</p>
<p><strong>✅ 对象配置</strong>:使用对象配置tooltip的各种属性(位置、主题等)</p>
<p><strong>✅ Scope参数</strong>:函数tooltip正确接收scope.column和scope.$index参数</p>
</template>
</el-alert>
<z-table
:data="tableData"
:columns="columns"
:virtual="{ enabled: true, itemHeight: 48, threshold: 100 }"
height="500px"
border
stripe
row-key="id"
/>
</div>
</template>
<style scoped>
:deep(.el-collapse-item__content) {
padding: 12px !important;
}
</style>
水印功能
虚拟表格完全兼容z-table组件的水印功能。
功能特点:
- ✅ 字符串水印:直接传入字符串作为水印内容
- ✅ 对象水印:传入配置对象,自定义水印的字体、颜色、角度、间距等
- ✅ 动态切换:支持运行时动态开启/关闭/修改水印
- ✅ 虚拟滚动兼容:水印在虚拟滚动表格中正常显示,不影响性能
- ✅ 样式一致:与普通表格的水印效果完全一致
虚拟表格 - 水印功能
功能说明:
- 字符串水印:直接传入字符串作为水印内容
- 对象水印:传入配置对象,自定义水印样式
- 虚拟滚动兼容:水印层级高于表格内容,在虚拟滚动中正常显示
- 性能优化:水印不影响虚拟滚动的性能表现
当前配置:
false
<template>
<div>
<h3>虚拟表格 - 水印功能</h3>
<div style="margin-bottom: 16px;">
<el-space>
<el-button @click="toggleWatermark">
{{ currentWatermarkType === 'none' ? '启用字符串水印' : currentWatermarkType === 'string' ? '切换到对象水印' : '禁用水印' }}
</el-button>
<el-text type="info">当前水印模式: {{ watermarkModeText }}</el-text>
</el-space>
</div>
<el-alert
title="✅ 水印功能已兼容虚拟表格"
type="success"
:closable="false"
style="margin-bottom: 16px;"
>
<template #default>
<p><strong>✅ 字符串水印</strong>:支持简单的字符串水印内容</p>
<p><strong>✅ 对象水印</strong>:支持完整的水印配置,包括字体、颜色、角度等</p>
<p><strong>✅ 动态切换</strong>:支持运行时动态开启/关闭/修改水印</p>
<p><strong>✅ 虚拟滚动兼容</strong>:水印在虚拟滚动表格中正常显示</p>
</template>
</el-alert>
<z-table
:data="tableData"
:columns="columns"
:virtual="{ enabled: true, itemHeight: 48, threshold: 100 }"
:watermark="currentWatermark"
height="500px"
border
stripe
row-key="id"
/>
<div style="margin-top: 16px;">
<h4>功能说明:</h4>
<ul>
<li><strong>字符串水印</strong>:直接传入字符串作为水印内容</li>
<li><strong>对象水印</strong>:传入配置对象,自定义水印样式</li>
<li><strong>虚拟滚动兼容</strong>:水印层级高于表格内容,在虚拟滚动中正常显示</li>
<li><strong>性能优化</strong>:水印不影响虚拟滚动的性能表现</li>
</ul>
</div>
<div style="margin-top: 16px;">
<h4>当前配置:</h4>
<pre style="padding: 12px; font-size: 12px; background: #f5f7fa; border-radius: 4px;">{{ JSON.stringify(currentWatermark, null, 2) }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
// 生成测试数据
function generateLargeData(count: number) {
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十', '郑十一', '王十二']
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部', '销售部', '人力资源部', '财务部']
const data = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}${Math.floor(i / names.length) + 1}`,
email: `user${i + 1}@company.com`,
age: 22 + (i % 40),
department: departments[i % departments.length],
salary: 5000 + Math.floor(Math.random() * 10000),
status: i % 3 === 0 ? 'active' : 'inactive',
joinDate: `2023-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`
})
}
return data
}
const tableData = ref(generateLargeData(2000))
const columns = ref([
{
type: 'selection',
},
{
type: 'index',
label: '序号',
},
{
prop: 'name',
label: '姓名',
},
{
prop: 'email',
label: '邮箱',
},
{
prop: 'age',
label: '年龄',
},
{
prop: 'department',
label: '部门',
},
{
prop: 'salary',
label: '薪资',
render: ({ row }: any) => `¥${row.salary.toLocaleString()}`
},
{
prop: 'status',
label: '状态',
render: ({ row }: any) => row.status === 'active' ? '在职' : '离职'
},
{
prop: 'joinDate',
label: '入职日期',
}
])
// 水印状态管理
const currentWatermarkType = ref<'none' | 'string' | 'object'>('none')
const currentWatermark = computed(() => {
switch (currentWatermarkType.value) {
case 'string':
return 'ideaz-element 虚拟表格水印'
case 'object':
return {
content: 'ideaz-element',
fontColor: 'rgba(0, 0, 0, 0.15)',
rotate: -20,
gap: [100, 100],
offset: [50, 50]
}
default:
return false
}
})
const watermarkModeText = computed(() => {
switch (currentWatermarkType.value) {
case 'string':
return '字符串水印'
case 'object':
return '对象配置水印'
default:
return '无水印'
}
})
function toggleWatermark() {
if (currentWatermarkType.value === 'none') {
currentWatermarkType.value = 'string'
} else if (currentWatermarkType.value === 'string') {
currentWatermarkType.value = 'object'
} else {
currentWatermarkType.value = 'none'
}
}
</script>
Footer 底部功能
虚拟表格完全支持 Element Plus TableV2 的 footer 功能。
功能特点:
- ✅ Footer 插槽:通过
#footer
插槽自定义底部内容 - ✅ 高度配置:通过
virtual.footerHeight
配置 footer 区域高度 - ✅ 动态控制:支持运行时动态显示/隐藏 footer
- ✅ 样式自定义:完全支持 CSS 样式自定义,支持渐变背景等
- ✅ 统计功能:适合展示数据统计、汇总信息等
- ✅ 操作区域:可放置导出、生成报告等操作按钮
虚拟表格 Footer 功能演示
虚拟表格支持 footer 插槽,可以在表格底部显示统计信息等内容。
<!-- eslint-disable no-console -->
<script lang="ts" setup>
import { computed, h, ref } from 'vue'
interface RowData {
id: number
name: string
department: string
salary: number
bonus: number
total: number
status: string
}
// 生成测试数据
function generateData(count: number): RowData[] {
const names = ['张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '郑十', '冯一', '陈二']
const departments = ['技术部', '产品部', '设计部', '运营部', '市场部']
const statuses = ['在职', '离职', '试用期']
const data: RowData[] = []
for (let i = 0; i < count; i++) {
const salary = Math.floor(Math.random() * 30000) + 8000
const bonus = Math.floor(Math.random() * 5000) + 1000
data.push({
id: i + 1,
name: `${names[i % names.length]}-${i + 1}`,
department: departments[i % departments.length],
salary,
bonus,
total: salary + bonus,
status: statuses[i % statuses.length],
})
}
return data
}
const tableData = ref<RowData[]>(generateData(1000))
const footerHeight = ref(80)
const showFooter = ref(true)
const columns = ref([
{
prop: 'id',
label: 'ID',
},
{
prop: 'name',
label: '姓名',
},
{
prop: 'department',
label: '部门',
},
{
prop: 'salary',
label: '基本工资',
render: ({ row }: any) => `¥${row.salary.toLocaleString()}`,
},
{
prop: 'bonus',
label: '奖金',
render: ({ row }: any) => `¥${row.bonus.toLocaleString()}`,
},
{
prop: 'total',
label: '总收入',
render: ({ row }: any) => h('span', {
style: { color: '#67c23a', fontWeight: 'bold' }
}, `¥${row.total.toLocaleString()}`),
},
{
prop: 'status',
label: '状态',
render: ({ row }: any) => h('span', {
style: {
color: row.status === '在职' ? '#67c23a' : row.status === '试用期' ? '#e6a23c' : '#f56c6c',
fontWeight: 'bold'
}
}, row.status),
},
])
// 计算统计数据
const statistics = computed(() => {
const validData = tableData.value.filter(item => item.status !== '离职')
return {
totalEmployees: validData.length,
totalSalary: validData.reduce((sum, item) => sum + item.salary, 0),
totalBonus: validData.reduce((sum, item) => sum + item.bonus, 0),
totalIncome: validData.reduce((sum, item) => sum + item.total, 0),
averageSalary: Math.round(validData.reduce((sum, item) => sum + item.salary, 0) / validData.length),
}
})
function resetData() {
tableData.value = generateData(1000)
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<h3>虚拟表格 Footer 功能演示</h3>
<p style="margin: 8px 0; color: #666;">
虚拟表格支持 footer 插槽,可以在表格底部显示统计信息等内容。
</p>
<el-space wrap>
<el-button @click="showFooter = !showFooter" :type="showFooter ? 'danger' : 'primary'">
{{ showFooter ? '隐藏' : '显示' }} Footer
</el-button>
<el-input-number
v-model="footerHeight"
:min="50"
:max="200"
:step="10"
controls-position="right"
style="width: 150px;"
/>
<span style="color: #666;">Footer 高度: {{ footerHeight }}px</span>
<el-button @click="resetData" type="success">
重新生成数据
</el-button>
</el-space>
</div>
<z-table
v-model:data="tableData"
:columns="columns"
:virtual="{
enabled: true,
itemHeight: 48,
threshold: 50,
footerHeight: showFooter ? footerHeight : 0
}"
height="500px"
border
stripe
>
<!-- Footer 插槽 - 显示统计信息 -->
<template #footer v-if="showFooter">
<div class="table-footer">
<div class="footer-content">
<div class="statistics-section">
<div class="stat-item">
<div class="stat-info">
<div class="stat-value">{{ statistics.totalEmployees }}</div>
<div class="stat-label">在职员工</div>
</div>
</div>
<div class="divider">|</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-value">¥{{ statistics.totalSalary.toLocaleString() }}</div>
<div class="stat-label">总基本工资</div>
</div>
</div>
<div class="divider">|</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-value">¥{{ statistics.totalBonus.toLocaleString() }}</div>
<div class="stat-label">总奖金</div>
</div>
</div>
<div class="divider">|</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-value">¥{{ statistics.totalIncome.toLocaleString() }}</div>
<div class="stat-label">总收入</div>
</div>
</div>
<div class="divider">|</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-value">¥{{ statistics.averageSalary.toLocaleString() }}</div>
<div class="stat-label">平均工资</div>
</div>
</div>
</div>
</div>
</div>
</template>
</z-table>
</div>
</template>
<style scoped>
.table-footer {
display: flex;
align-items: center;
height: 100%;
padding: 0 20px;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.footer-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.statistics-section {
display: flex;
gap: 16px;
align-items: center;
}
.stat-item {
display: flex;
gap: 8px;
align-items: center;
}
.divider {
margin: 0 8px;
font-size: 16px;
color: rgba(255, 255, 255, 60%);
}
.stat-info {
text-align: left;
}
.stat-value {
font-size: 16px;
font-weight: bold;
line-height: 1;
}
.stat-label {
margin-top: 2px;
font-size: 12px;
opacity: 80%;
}
.action-section {
display: flex;
gap: 8px;
}
</style>
自定义列渲染
验证列内容的自定义渲染,支持render函数和slot插槽两种方式。
支持方式:
- 🎯 Render 函数:灵活的渲染函数支持
- 🎰 Slot 插槽:Vue 原生插槽支持
- 🎨 复杂组件:支持嵌套复杂组件
- 🖱️ 交互功能:支持点击、悬浮等交互
虚拟表格 - 列自定义
<template>
<div>
<h3>虚拟表格 - 列自定义</h3>
<div style="margin-bottom: 16px;">
<el-space>
<el-button @click="toggleCustomMode">
{{ useSlotMode ? '切换到Render模式' : '切换到Slot模式' }}
</el-button>
<el-text type="info">当前模式: {{ useSlotMode ? 'Slot插槽' : 'Render函数' }}</el-text>
</el-space>
</div>
<el-alert
title="✅ 列自定义功能"
type="success"
:closable="false"
style="margin-bottom: 16px;"
>
<template #default>
<p><strong>✅ Render函数</strong>:使用render函数进行列内容自定义</p>
<p><strong>✅ Slot插槽</strong>:使用slot插槽进行列内容自定义</p>
<p><strong>✅ 参数传递</strong>:完整的scope参数,包括row、column、cellData、$index等</p>
<p><strong>✅ 事件支持</strong>:自定义内容中的事件绑定和交互</p>
<p><strong>✅ 组件支持</strong>:支持各种Element Plus组件</p>
</template>
</el-alert>
<z-table
ref="tableRef"
:data="tableData"
:columns="columns"
:virtual="{ enabled: true, itemHeight: 48, threshold: 100 }"
height="500px"
border
stripe
row-key="id"
>
<!-- Slot方式的列自定义 -->
<template #avatar-slot="scope">
<div style="display: flex; gap: 8px; align-items: center;">
<el-avatar :size="32" :src="scope.row.avatar" />
<div>
<div style="font-weight: bold;">{{ scope.row.name }}</div>
<div style="font-size: 12px; color: #909399;">{{ scope.row.email }}</div>
</div>
</div>
</template>
<template #salary-slot="scope">
<div @click="handleSalaryClick(scope)" style="cursor: pointer;">
<el-tag
:type="scope.row.salary > 8000 ? 'success' : scope.row.salary > 6000 ? 'warning' : 'danger'"
effect="dark"
>
<el-icon style="margin-right: 4px;"><Money /></el-icon>
¥{{ scope.row.salary.toLocaleString() }}
</el-tag>
</div>
</template>
<template #status-slot="scope">
<el-switch
v-model="scope.row.status"
active-value="active"
inactive-value="inactive"
active-text="在职"
inactive-text="离职"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
@change="handleStatusChange(scope)"
/>
</template>
<template #progress-slot="scope">
<div style="display: flex; gap: 8px; align-items: center;">
<el-progress
:percentage="scope.row.progress"
:stroke-width="8"
:show-text="false"
style="flex: 1;"
/>
<span style="min-width: 35px; font-size: 12px; color: #606266;">
{{ scope.row.progress }}%
</span>
</div>
</template>
<template #actions-slot="scope">
<el-space size="small">
<el-button
size="small"
type="primary"
:icon="Edit"
@click="handleEdit(scope)"
>
编辑
</el-button>
<el-button
size="small"
type="success"
:icon="View"
@click="handleView(scope)"
>
查看
</el-button>
<el-popconfirm
title="确定要删除吗?"
@confirm="handleDelete(scope)"
>
<template #reference>
<el-button
size="small"
type="danger"
:icon="Delete"
>
删除
</el-button>
</template>
</el-popconfirm>
</el-space>
</template>
</z-table>
<!-- 参数打印区域 -->
<div style="margin-top: 16px;">
<el-collapse v-model="activeCollapse">
<el-collapse-item title="最近的参数传递日志" name="1">
<div style="max-height: 200px; padding: 12px; border-radius: 4px; background: #f5f7fa; overflow-y: auto;">
<div v-for="(log, index) in paramLogs" :key="index" style="margin-bottom: 8px; font-family: monospace; font-size: 12px;">
<span style="color: #909399;">[{{ log.timestamp }}]</span>
<span style="font-weight: bold; color: #409eff;">{{ log.type }}:</span>
<span style="color: #606266;">{{ log.message }}</span>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, h, ref } from 'vue'
import { Delete, Edit, Money, View } from '@element-plus/icons-vue'
// 生成测试数据
function generateData() {
const departments = ['技术部', '市场部', '产品部', '运营部', '设计部']
const data = []
for (let i = 1; i <= 2000; i++) {
data.push({
id: i,
name: `用户 ${i}`,
email: `user${i}@example.com`,
age: 20 + (i % 50),
department: departments[i % departments.length],
salary: 5000 + (i % 100) * 100,
status: i % 3 === 0 ? 'active' : 'inactive',
progress: Math.floor(Math.random() * 100),
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=user${i}`,
})
}
return data
}
const tableData = ref(generateData())
const tableRef = ref()
const useSlotMode = ref(true)
const activeCollapse = ref(['1'])
const paramLogs = ref<any[]>([])
// 添加日志
function addLog(type: string, message: string, params?: any) {
const timestamp = new Date().toLocaleTimeString()
paramLogs.value.unshift({
timestamp,
type,
message,
params
})
// 只保留最近20条日志
if (paramLogs.value.length > 20) {
paramLogs.value = paramLogs.value.slice(0, 20)
}
// eslint-disable-next-line no-console
console.log(`[${type}]`, message, params)
}
// 列配置
const columns = computed(() => [
{
type: 'index',
label: '序号',
},
{
prop: 'name',
label: '用户信息',
// 根据模式选择render或slot
...(useSlotMode.value ? {
slot: 'avatar-slot'
} : {
render: (scope: any) => {
addLog('列Render', `用户信息列渲染 - 行${scope.$index}`, scope)
return h('div', {
style: { display: 'flex', gap: '8px', alignItems: 'center' }
}, [
h('el-avatar', {
size: 32,
src: scope.row.avatar
}),
h('div', {}, [
h('div', { style: { fontWeight: 'bold' } }, scope.row.name),
h('div', {
style: { fontSize: '12px', color: '#909399' }
}, scope.row.email)
])
])
}
})
},
{
prop: 'department',
label: '部门',
},
{
prop: 'age',
label: '年龄',
},
{
prop: 'salary',
label: '薪资',
...(useSlotMode.value ? {
slot: 'salary-slot'
} : {
render: (scope: any) => {
addLog('列Render', `薪资列渲染 - 行${scope.$index}, 薪资: ${scope.row.salary}`, scope)
const getType = (salary: number) => {
if (salary > 8000) return 'success'
if (salary > 6000) return 'warning'
return 'danger'
}
return h('div', {
onClick: () => handleSalaryClick(scope),
style: { cursor: 'pointer' }
}, [
h('el-tag', {
type: getType(scope.row.salary),
effect: 'dark'
}, [
h('el-icon', { style: { marginRight: '4px' } }, [h(Money)]),
`¥${scope.row.salary.toLocaleString()}`
])
])
}
})
},
{
prop: 'status',
label: '状态',
...(useSlotMode.value ? {
slot: 'status-slot'
} : {
render: (scope: any) => {
addLog('列Render', `状态列渲染 - 行${scope.$index}, 状态: ${scope.row.status}`, scope)
return h('el-switch', {
modelValue: scope.row.status,
activeValue: 'active',
inactiveValue: 'inactive',
activeText: '在职',
inactiveText: '离职',
style: '--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949',
'onUpdate:modelValue': (value: string) => {
scope.row.status = value
handleStatusChange(scope)
}
})
}
})
},
{
prop: 'progress',
label: '项目进度',
...(useSlotMode.value ? {
slot: 'progress-slot'
} : {
render: (scope: any) => {
addLog('列Render', `进度列渲染 - 行${scope.$index}, 进度: ${scope.row.progress}%`, scope)
return h('div', {
style: { display: 'flex', gap: '8px', alignItems: 'center' }
}, [
h('el-progress', {
percentage: scope.row.progress,
strokeWidth: 8,
showText: false,
style: { flex: 1 }
}),
h('span', {
style: { minWidth: '35px', fontSize: '12px', color: '#606266' }
}, `${scope.row.progress}%`)
])
}
})
},
{
prop: 'actions',
label: '操作',
fixed: 'right',
...(useSlotMode.value ? {
slot: 'actions-slot'
} : {
render: (scope: any) => {
addLog('列Render', `操作列渲染 - 行${scope.$index}`, scope)
return h('el-space', { size: 'small' }, [
h('el-button', {
size: 'small',
type: 'primary',
icon: Edit,
onClick: () => handleEdit(scope)
}, '编辑'),
h('el-button', {
size: 'small',
type: 'success',
icon: View,
onClick: () => handleView(scope)
}, '查看'),
h('el-popconfirm', {
title: '确定要删除吗?',
onConfirm: () => handleDelete(scope)
}, {
reference: () => h('el-button', {
size: 'small',
type: 'danger',
icon: Delete
}, '删除')
})
])
}
})
}
])
// 事件处理函数
function handleSalaryClick(scope: any) {
addLog('Slot事件', `点击薪资 - 行${scope.$index}, 用户: ${scope.row.name}, 薪资: ${scope.row.salary}`, scope)
}
function handleStatusChange(scope: any) {
addLog('Slot事件', `状态变更 - 行${scope.$index}, 用户: ${scope.row.name}, 新状态: ${scope.row.status}`, scope)
}
function handleEdit(scope: any) {
addLog('Slot事件', `编辑用户 - 行${scope.$index}, 用户: ${scope.row.name}`, scope)
}
function handleView(scope: any) {
addLog('Slot事件', `查看用户 - 行${scope.$index}, 用户: ${scope.row.name}`, scope)
}
function handleDelete(scope: any) {
addLog('Slot事件', `删除用户 - 行${scope.$index}, 用户: ${scope.row.name}`, scope)
}
function toggleCustomMode() {
useSlotMode.value = !useSlotMode.value
addLog('模式切换', `切换到${useSlotMode.value ? 'Slot' : 'Render'}模式`)
}
</script>
<style scoped>
:deep(.el-collapse-item__content) {
padding: 12px !important;
}
</style>
自定义表头
测试表头的自定义功能,包括图标、徽章、下拉菜单等交互元素。
支持功能:
- 🎯 图标支持:表头图标展示
- 🏷️ 徽章支持:动态徽章展示
- 📝 提示信息:表头提示支持
- 🎛️ 下拉菜单:表头操作菜单
- 🎨 样式自定义:完整的样式定制能力
虚拟表格 - 表头自定义
<template>
<div>
<h3>虚拟表格 - 表头自定义</h3>
<div style="margin-bottom: 16px;">
<el-space>
<el-button @click="toggleCustomMode">
{{ useSlotMode ? '切换到Render模式' : '切换到Slot模式' }}
</el-button>
<el-text type="info">当前模式: {{ useSlotMode ? 'Slot插槽' : 'Render函数' }}</el-text>
</el-space>
</div>
<el-alert
title="✅ 表头自定义功能"
type="success"
:closable="false"
style="margin-bottom: 16px;"
>
<template #default>
<p><strong>✅ Render函数</strong>:使用render函数进行表头自定义</p>
<p><strong>✅ Slot插槽</strong>:使用slot插槽进行表头自定义</p>
<p><strong>✅ 图标支持</strong>:支持图标、提示、徽章等复杂内容</p>
<p><strong>✅ 交互支持</strong>:表头中的点击事件和交互元素</p>
<p><strong>✅ 样式定制</strong>:支持自定义样式和布局</p>
</template>
</el-alert>
<z-table
ref="tableRef"
:data="tableData"
:columns="columns"
:virtual="{ enabled: true, itemHeight: 48, threshold: 100 }"
height="500px"
border
stripe
row-key="id"
>
<!-- Slot方式的表头自定义 -->
<template #user-header>
<div style="display: flex; gap: 6px; align-items: center;">
<el-icon color="#409eff" size="16"><User /></el-icon>
<span>用户信息</span>
<el-tooltip content="包含用户姓名、邮箱等基本信息" placement="top">
<el-icon color="#909399" size="14" style="cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>
<template #department-header>
<div style="display: flex; gap: 6px; align-items: center;">
<el-icon color="#67c23a" size="16"><OfficeBuilding /></el-icon>
<span>部门信息</span>
<el-badge :value="departmentCount" type="primary" :max="99">
<span></span>
</el-badge>
</div>
</template>
<template #salary-header>
<div
style="display: flex; gap: 6px; align-items: center; cursor: pointer;"
@click="handleSalaryHeaderClick"
>
<el-icon color="#f56c6c" size="16"><Money /></el-icon>
<span>薪资待遇</span>
<el-tag size="small" type="warning">¥{{ avgSalary }}</el-tag>
</div>
</template>
<template #status-header>
<div style="display: flex; gap: 6px; align-items: center;">
<el-icon color="#909399" size="16"><SwitchButton /></el-icon>
<span>在职状态</span>
<div style="display: flex; gap: 4px;">
<el-tag size="small" type="success">{{ activeCount }}在职</el-tag>
<el-tag size="small" type="info">{{ inactiveCount }}离职</el-tag>
</div>
</div>
</template>
<template #actions-header>
<div style="display: flex; gap: 6px; align-items: center;">
<el-icon color="#e6a23c" size="16"><Setting /></el-icon>
<span>操作</span>
<el-dropdown @command="handleHeaderCommand">
<el-icon style="cursor: pointer;" color="#409eff"><More /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="export">导出数据</el-dropdown-item>
<el-dropdown-item command="import">导入数据</el-dropdown-item>
<el-dropdown-item command="settings">列设置</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</z-table>
<!-- 参数打印区域 -->
<div style="margin-top: 16px;">
<el-collapse v-model="activeCollapse">
<el-collapse-item title="最近的参数传递日志" name="1">
<div style="max-height: 200px; padding: 12px; overflow-y: auto; background: #f5f7fa; border-radius: 4px;">
<div v-for="(log, index) in paramLogs" :key="index" style="margin-bottom: 8px; font-family: monospace; font-size: 12px;">
<span style="color: #909399;">[{{ log.timestamp }}]</span>
<span style="font-weight: bold; color: #409eff;">{{ log.type }}:</span>
<span style="color: #606266;">{{ log.message }}</span>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, h, ref } from 'vue'
import {
Money,
More,
OfficeBuilding,
QuestionFilled,
Setting,
SwitchButton,
User
} from '@element-plus/icons-vue'
// 生成测试数据
function generateData() {
const departments = ['技术部', '市场部', '产品部', '运营部', '设计部']
const data = []
for (let i = 1; i <= 2000; i++) {
data.push({
id: i,
name: `用户 ${i}`,
email: `user${i}@example.com`,
age: 20 + (i % 50),
department: departments[i % departments.length],
salary: 5000 + (i % 100) * 100,
status: i % 3 === 0 ? 'active' : 'inactive',
})
}
return data
}
const tableData = ref(generateData())
const tableRef = ref()
const useSlotMode = ref(true)
const activeCollapse = ref(['1'])
const paramLogs = ref<any[]>([])
// 计算统计数据
const departmentCount = computed(() => {
const depts = new Set(tableData.value.map(item => item.department))
return depts.size
})
const avgSalary = computed(() => {
const total = tableData.value.reduce((sum, item) => sum + item.salary, 0)
return Math.round(total / tableData.value.length)
})
const activeCount = computed(() => {
return tableData.value.filter(item => item.status === 'active').length
})
const inactiveCount = computed(() => {
return tableData.value.filter(item => item.status === 'inactive').length
})
// 添加日志
function addLog(type: string, message: string, params?: any) {
const timestamp = new Date().toLocaleTimeString()
paramLogs.value.unshift({
timestamp,
type,
message,
params
})
// 只保留最近20条日志
if (paramLogs.value.length > 20) {
paramLogs.value = paramLogs.value.slice(0, 20)
}
// eslint-disable-next-line no-console
console.log(`[${type}]`, message, params)
}
// 列配置
const columns = computed(() => [
{
type: 'index',
label: '序号',
},
{
prop: 'name',
// 根据模式选择不同的表头配置
...(useSlotMode.value ? {
label: 'user-header'
} : {
label: (scope: any) => {
addLog('表头Render', `用户信息表头渲染`, scope)
return h('div', {
style: { display: 'flex', gap: '6px', alignItems: 'center' }
}, [
h('el-icon', { color: '#409eff', size: 16 }, [h(User)]),
h('span', {}, () => '用户信息'),
h('el-tooltip', {
content: '包含用户姓名、邮箱等基本信息',
placement: 'top'
}, {
default: () => h('el-icon', {
color: '#909399',
size: 14,
style: { cursor: 'help' }
}, [h(QuestionFilled)])
})
])
}
})
},
{
prop: 'email',
label: '邮箱',
},
{
prop: 'age',
label: '年龄',
},
{
prop: 'department',
...(useSlotMode.value ? {
label: 'department-header'
} : {
label: (scope: any) => {
addLog('表头Render', `部门信息表头渲染`, scope)
return h('div', {
style: { display: 'flex', gap: '6px', alignItems: 'center' }
}, [
h('el-icon', { color: '#67c23a', size: 16 }, [h(OfficeBuilding)]),
h('span', {}, () => '部门信息'),
h('el-badge', {
value: departmentCount.value,
type: 'primary',
max: 99
}, {
default: () => h('span')
})
])
}
})
},
{
prop: 'salary',
...(useSlotMode.value ? {
label: 'salary-header'
} : {
label: (scope: any) => {
addLog('表头Render', `薪资表头渲染`, scope)
return h('div', {
style: { display: 'flex', gap: '6px', alignItems: 'center', cursor: 'pointer' },
onClick: handleSalaryHeaderClick
}, [
h('el-icon', { color: '#f56c6c', size: 16 }, [h(Money)]),
h('span', {}, () => '薪资待遇'),
h('el-tag', {
size: 'small',
type: 'warning'
}, `¥${avgSalary.value}`)
])
}
})
},
{
prop: 'status',
...(useSlotMode.value ? {
label: 'status-header'
} : {
label: (scope: any) => {
addLog('表头Render', `状态表头渲染`, scope)
return h('div', {
style: { display: 'flex', gap: '6px', alignItems: 'center' }
}, [
h('el-icon', { color: '#909399', size: 16 }, [h(SwitchButton)]),
h('span', {}, () => '在职状态'),
h('div', {
style: { display: 'flex', gap: '4px' }
}, [
h('el-tag', {
size: 'small',
type: 'success'
}, `${activeCount.value}在职`),
h('el-tag', {
size: 'small',
type: 'info'
}, `${inactiveCount.value}离职`)
])
])
}
})
},
{
prop: 'actions',
fixed: 'right',
...(useSlotMode.value ? {
label: 'actions-header'
} : {
label: (scope: any) => {
addLog('表头Render', `操作表头渲染`, scope)
return h('div', {
style: { display: 'flex', gap: '6px', alignItems: 'center' }
}, [
h('el-icon', { color: '#e6a23c', size: 16 }, [h(Setting)]),
h('span', {}, () => '操作'),
h('el-dropdown', {
onCommand: handleHeaderCommand
}, {
default: () => h('el-icon', {
style: { cursor: 'pointer' },
color: '#409eff'
}, [h(More)]),
dropdown: () => h('el-dropdown-menu', {}, [
h('el-dropdown-item', { command: 'export' }, () => '导出数据'),
h('el-dropdown-item', { command: 'import' }, () => '导入数据'),
h('el-dropdown-item', { command: 'settings' }, () => '列设置')
])
})
])
}
})
}
])
// 事件处理函数
function handleSalaryHeaderClick() {
addLog('表头事件', `点击薪资表头 - 平均薪资: ${avgSalary.value}`)
}
function handleHeaderCommand(command: string) {
addLog('表头事件', `操作表头下拉菜单 - 命令: ${command}`)
}
function toggleCustomMode() {
useSlotMode.value = !useSlotMode.value
addLog('模式切换', `切换到${useSlotMode.value ? 'Slot' : 'Render'}模式`)
}
</script>
<style scoped>
:deep(.el-collapse-item__content) {
padding: 12px !important;
}
</style>
方法和事件
测试表格的各种方法调用和事件处理。
测试内容:
- 📞 方法调用:包括虚拟滚动专用的
scrollToRow
方法 - 📡 事件处理:完整的事件处理测试
- 🎯 滚动定位:精确的滚动定位功能
- 🔄 状态管理:表格状态管理测试
虚拟表格方法测试
虚拟滚动专用方法测试
<!-- eslint-disable no-console -->
<script lang="ts" setup>
import { ref } from 'vue'
interface RowData {
id: number
name: string
gender: string
age: number
time: string
}
// 生成大量测试数据
function generateLargeData(count: number): RowData[] {
const names = ['Steven', 'Helen', 'Nancy', 'Jack', 'Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
const genders = ['male', 'female']
const data: RowData[] = []
for (let i = 0; i < count; i++) {
data.push({
id: i + 1,
name: `${names[i % names.length]}-${i + 1}`,
gender: genders[i % genders.length],
age: 18 + (i % 50),
time: `202${(i % 4) + 0}-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`,
})
}
return data
}
const tableData = ref<RowData[]>(generateLargeData(1500))
const tableRef = ref()
const columns = ref([
{
type: 'selection',
},
{
type: 'index',
},
{
prop: 'id',
label: 'ID',
},
{
prop: 'name',
label: '姓名',
},
{
prop: 'gender',
label: '性别',
},
{
prop: 'age',
label: '年龄',
sortable: true,
},
{
prop: 'time',
label: '出生日期',
},
])
// 表格方法测试
function setCurrentRow() {
const row = tableData.value[10]
tableRef.value?.setCurrentRow(row)
console.log('设置当前行:', row)
}
function toggleRowSelection() {
const row = tableData.value[5]
tableRef.value?.toggleRowSelection(row, true)
console.log('切换行选择:', row)
}
function clearSelection() {
tableRef.value?.clearSelection()
console.log('清空选择')
}
function toggleAllSelection() {
tableRef.value?.toggleAllSelection()
console.log('切换全选')
}
function clearSort() {
tableRef.value?.clearSort()
console.log('清空排序')
}
function scrollToTop() {
tableRef.value?.scrollTo({ scrollTop: 0 })
console.log('滚动到顶部')
}
function scrollToBottom() {
tableRef.value?.scrollTo({ scrollTop: 999999 })
console.log('滚动到底部')
}
function scrollToRow() {
const index = 500
tableRef.value?.scrollToRow(index)
console.log(`滚动到第 ${index + 1} 行`)
}
// 事件处理
function handleCurrentChange(currentRow: RowData, oldCurrentRow: RowData) {
console.log('当前行改变:', currentRow, oldCurrentRow)
}
function handleSelectionChange(selection: RowData[]) {
console.log('选择改变:', selection.length, '行')
}
function handleSortChange({ column, prop, order }: any) {
console.log('排序改变:', { column, prop, order })
}
</script>
<template>
<div>
<div style="margin-bottom: 16px;">
<h4>虚拟表格方法测试</h4>
<el-button @click="setCurrentRow">设置当前行(第11行)</el-button>
<el-button @click="toggleRowSelection">选择第6行</el-button>
<el-button @click="clearSelection">清空选择</el-button>
<el-button @click="toggleAllSelection">切换全选</el-button>
<el-button @click="clearSort">清空排序</el-button>
</div>
<div style="margin-bottom: 16px;">
<h4>虚拟滚动专用方法测试</h4>
<el-button @click="scrollToTop">滚动到顶部</el-button>
<el-button @click="scrollToBottom">滚动到底部</el-button>
<el-button @click="scrollToRow">滚动到第501行</el-button>
</div>
<z-table
ref="tableRef"
v-model:data="tableData"
:columns="columns"
:virtual="{ enabled: true, itemHeight: 48, threshold: 100 }"
height="500px"
row-key="id"
highlight-current-row
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
/>
</div>
</template>
📖 配置参考ß
配置选项
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
enabled | boolean | false | 是否启用虚拟滚动 |
itemHeight | number | 48 | 每行固定高度(px) |
estimatedRowHeight | number | 48 | 动态高度时的预估行高 |
threshold | number | 100 | 数据量超过此值自动启用 |
footerHeight | number | 50 | Footer 区域高度(px) |
API 方法
虚拟滚动专用方法
// 滚动到指定位置
tableRef.value.scrollTo({
scrollTop: 1000,
scrollLeft: 200
})
// 滚动到指定行
tableRef.value.scrollToRow(500, 'center')
通用表格方法
所有原有的表格方法在虚拟模式下依然可用:
// 设置当前行
tableRef.value.setCurrentRow(row)
// 切换行选择
tableRef.value.toggleRowSelection(row, true)
// 清空选择
tableRef.value.clearSelection()
// 排序
tableRef.value.sort('column', 'ascending')
性能对比
数据量 | 普通表格 | 虚拟表格 | 性能提升 |
---|---|---|---|
1,000 行 | 正常 | 正常 | - |
10,000 行 | 较慢 | 流畅 | 10x |
50,000 行 | 卡顿 | 流畅 | 50x |
100,000 行 | 不可用 | 流畅 | 100x+ |
使用建议
🎯 何时使用虚拟滚动
- ✅ 数据量 > 1000 条
- ✅ 需要一次性展示所有数据
- ✅ 用户需要快速浏览和定位
- ✅ 性能敏感的应用场景
⚠️ 注意事项
固定高度要求:
- ✅ 必须设置
height
属性 - ❌ 不支持动态行高
- ✅ 必须设置
数据结构要求:
- ✅ 支持普通对象数组
- ✅ 支持嵌套属性访问
- ❌ 不支持复杂树形数据结构
功能兼容性:
- ✅ 完全兼容: 排序、分页、操作按钮、列显隐、列提示、可编辑表格、选择功能、展开功能、索引列、水印功能、Footer 底部区域
- ✅ 自定义功能: 列内容渲染、表头渲染、插槽模板
- ⚠️ 部分兼容: 复杂的嵌套组件、动态行高
- ❌ 不支持: 行合并、复杂树形数据
性能优化建议:
- 💡 数据量 > 1000 条时启用
- 💡 避免在列中使用过于复杂的组件
- 💡 列配置无需手动指定 width,系统自动处理ß