Skip to content

Z-Table 虚拟滚动功能

📚 概述

Z-Table 组件现在支持基于 Element Plus TableV2 的虚拟滚动功能,可以高效处理大量数据,提供流畅的用户体验。

🚀 核心特性

  • 高性能渲染: 基于 Element Plus TableV2,支持 10 万+ 数据流畅滚动
  • 智能切换: 自动或手动在普通表格和虚拟表格间切换
  • 完整功能: 支持排序、固定列、多选等核心表格功能
  • 样式一致: 保持与原表格相同的视觉体验
  • 极致性能: DOM 节点数量恒定,内存占用稳定

✅ 兼容性说明

  • 完全兼容: 排序、筛选、多选、选择功能、展开功能、索引列、固定列、样式主题、列显隐、列提示、水印功能
  • 高级功能: 可编辑表格、自定义列渲染、自定义表头、插槽模板
  • API 一致: 与普通表格相同的 props 和事件
  • 渐进增强: 可在运行时动态开启/关闭

🚀 快速开始

三步启用虚拟滚动

vue
<!-- 步骤1: 添加 virtual 属性 -->
<!-- 步骤2: 设置固定高度 -->
<!-- 步骤3: 配置列信息 -->
<z-table
  :data="largeData"
  :columns="columns"
  :virtual="true"
  height="400px"
/>

基础配置示例

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

高级配置示例

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

虚拟表格完全支持 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>

📖 配置参考ß

配置选项

参数类型默认值说明
enabledbooleanfalse是否启用虚拟滚动
itemHeightnumber48每行固定高度(px)
estimatedRowHeightnumber48动态高度时的预估行高
thresholdnumber100数据量超过此值自动启用
footerHeightnumber50Footer 区域高度(px)

API 方法

虚拟滚动专用方法

javascript
// 滚动到指定位置
tableRef.value.scrollTo({
  scrollTop: 1000,
  scrollLeft: 200
})

// 滚动到指定行
tableRef.value.scrollToRow(500, 'center')

通用表格方法

所有原有的表格方法在虚拟模式下依然可用:

javascript
// 设置当前行
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 条
  • ✅ 需要一次性展示所有数据
  • ✅ 用户需要快速浏览和定位
  • ✅ 性能敏感的应用场景

⚠️ 注意事项

  1. 固定高度要求:

    • ✅ 必须设置 height 属性
    • ❌ 不支持动态行高
  2. 数据结构要求:

    • ✅ 支持普通对象数组
    • ✅ 支持嵌套属性访问
    • ❌ 不支持复杂树形数据结构
  3. 功能兼容性:

    • ✅ 完全兼容: 排序、分页、操作按钮、列显隐、列提示、可编辑表格、选择功能、展开功能、索引列、水印功能、Footer 底部区域
    • ✅ 自定义功能: 列内容渲染、表头渲染、插槽模板
    • ⚠️ 部分兼容: 复杂的嵌套组件、动态行高
    • ❌ 不支持: 行合并、复杂树形数据
  4. 性能优化建议:

    • 💡 数据量 > 1000 条时启用
    • 💡 避免在列中使用过于复杂的组件
    • 💡 列配置无需手动指定 width,系统自动处理ß

Released under the MIT License.