Skip to content

VTable 示例

展示 VTable 组件的各种使用场景和功能特性,每个示例都可以独立运行和复制。

基础用法

最简单的表格用法,展示大规模行列数据渲染。

ID
姓名
列 1
列 2
列 3
列 4
列 5
列 6
列 7
列 8
列 9
列 10
列 11
列 12
列 13
列 14
列 15
列 16
列 17
列 18
当前内容为空
查看代码 在线链接
vue
<template>
  <div style="padding: 20px">
    <div style="margin-bottom: 16px; display: flex; gap: 10px">
      <a-button @click="switchMode('default')">默认 (100×20)</a-button>
      <a-button @click="switchMode('big')">1000 × 50</a-button>
      <a-button @click="switchMode('huge')">10000 × 100</a-button>
    </div>

    <VTable
      :data="data"
      :columns="columns"
      :bordered="true"
      style="height: 400px; overflow: auto"
    />
  </div>
</template>

<script setup lang="ts">
import { shallowRef } from 'vue'
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

defineOptions({ name: 'BasicExample' })

const generateColumns = (total: number): VTableColumn[] => {
  const cols: VTableColumn[] = []
  cols.push(
    { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
    { columnKey: 'name', columnHeader: '姓名', columnWidth: 120 },
  )
  for (let i = 1; i <= total - 2; i++) {
    cols.push({
      columnKey: `col_${i}`,
      columnHeader: `列 ${i}`,
      columnWidth: 120,
    })
  }
  return cols
}

const generateData = (rows: number, cols: number) => {
  const list = Array.from({ length: rows }).map((_, i) => {
    const row: Record<string, any> = {
      id: i,
      name: `用户 ${i}`,
    }

    for (let j = 1; j <= cols - 2; j++) {
      row[`col_${j}`] = `数据 ${i}-${j}`
    }

    return row
  })
  console.log('生成数据条数:', list.length)
  return list
}

const columns = shallowRef<VTableColumn[]>([])
const data = shallowRef<any[]>([])

const switchMode = (mode: 'default' | 'big' | 'huge') => {
  const now = performance.now()
  if (mode === 'default') {
    columns.value = generateColumns(20)
    data.value = generateData(100, 20)
  }

  if (mode === 'big') {
    columns.value = generateColumns(50)
    data.value = generateData(1000, 50)
  }

  if (mode === 'huge') {
    columns.value = generateColumns(100)
    data.value = generateData(10000, 100)
  }

  console.log('耗时(ms):', performance.now() - now)
}

switchMode('default')
</script>

固定表头/表体

表头固定,内容区域可滚动,适合数据量较大的场景。

ID
姓名
年龄
邮箱
部门
我是表尾可以自定义
当前内容为空
查看代码 在线链接
vue
<template>
  <div style="height: 400px">
    <VTable
      style="height: 100%"
      :data="data"
      :columns="columns"
      :fixed-header="true"
      :row-height="48"
      :bordered="true"
      :fixed-footer="true"
    >
      <template #customFooter>
        <div style="width: 100%; background-color: #fafafa; padding: 12px">我是表尾可以自定义</div>
      </template>
    </VTable>
  </div>
</template>

<script setup lang="ts">
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

defineOptions({ name: 'FixedHeaderExample' })

// 生成 100 条数据
const data = ref(
  Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `用户${i + 1}`,
    age: 20 + (i % 30),
    email: `user${i + 1}@example.com`,
    department: ['技术部', '产品部', '运营部'][i % 3],
  })),
)

const columns: VTableColumn[] = [
  { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
  { columnKey: 'name', columnHeader: '姓名', columnWidth: 120 },
  { columnKey: 'age', columnHeader: '年龄', columnWidth: 100, columnAlign: 'center' },
  { columnKey: 'email', columnHeader: '邮箱', columnWidth: 200 },
  { columnKey: 'department', columnHeader: '部门' },
]
</script>

可编辑

支持可编辑行和可编辑单元格功能 (双击点击姓名的 tag 标签可进入单元格编辑)

姓名
部门
薪资
状态
操作
当前内容为空
查看代码 在线链接
vue
<template>
  <VTable ref="tableRef" :data="data" :columns="columns" row-key="id" :loading="loading">
    <template #bodyCell="{ columnKey, row, isEditingMode }">
      <template v-if="columnKey === 'name'">
        <a-tag
          v-if="isEditingMode == null"
          :color="row.level === 'senior' ? 'gold' : 'blue'"
          @dblclick="beginCellEdit(row, columnKey)"
        >
          {{ row.name }}
        </a-tag>
        <a-input
          v-else
          v-model:value="row.name"
          autofocus
          style="width: 120px"
          @blur="saveEdit(row)"
        />
        <a-button
          v-if="isEditingMode === 'cell'"
          size="small"
          type="link"
          style="padding: 0 !important; margin-left: 8px"
          @mousedown.prevent="cancelEdit(row)"
          >取消</a-button
        >
      </template>
      <template v-else-if="columnKey === 'department'">
        <span v-if="!isEditingMode">{{ row.department }}</span>
        <a-select
          v-else
          v-model:value="row.department"
          :options="[
            { label: '技术部', value: '技术部' },
            { label: '产品部', value: '产品部' },
            { label: '运营部', value: '运营部' },
          ]"
        />
      </template>
      <template v-else-if="columnKey === 'status'">
        <a-badge
          v-if="!isEditingMode"
          :status="row.status === 'active' ? 'success' : 'error'"
          :text="row.status === 'active' ? '在职' : '离职'"
        />
        <a-select
          v-else
          v-model:value="row.status"
          :options="[
            { label: '在职', value: 'active' },
            { label: '离职', value: 'inactive' },
          ]"
        />
      </template>
      <template v-else-if="columnKey === 'salary'">
        <span v-if="!isEditingMode" class="font-semibold text-green-600">
          ¥{{ row.salary.toLocaleString() }}
        </span>
        <a-input-number v-else v-model:value="row.salary" />
      </template>
      <template v-else-if="columnKey === 'action'">
        <template v-if="!isEditingMode">
          <a-button type="link" size="small" @click="beginRowEdit(row)">编辑</a-button>
        </template>
        <template v-else>
          <a-button type="primary" size="small" @click="saveEdit(row)">保存</a-button>
          <a-button size="small" style="margin-left: 8px" @click="cancelEdit(row)">取消</a-button>
        </template>
      </template>
    </template>
  </VTable>
</template>

<script setup lang="ts">
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

defineOptions({ name: 'EditStateExample' })

const loading = ref(false)
const dataList = [
  {
    id: 1,
    name: '张三',
    level: 'senior',
    status: 'active',
    salary: 25000,
    department: '技术部',
  },
  {
    id: 2,
    name: '李四',
    level: 'junior',
    status: 'active',
    salary: 15000,
    department: '产品部',
  },
  {
    id: 3,
    name: '王五',
    level: 'senior',
    status: 'inactive',
    salary: 28000,
    department: '技术部',
  },
  {
    id: 4,
    name: '赵六',
    level: 'junior',
    status: 'active',
    salary: 18000,
    department: '运营部',
  },
]

const data = ref(dataList)

const columns: VTableColumn[] = [
  {
    columnKey: 'name',
    columnHeader: '姓名',
    columnWidth: 200,
    columnEnableFilter: true,
  },
  {
    columnKey: 'department',
    columnHeader: '部门',
    columnWidth: 120,
  },
  {
    columnKey: 'salary',
    columnHeader: '薪资',
    columnWidth: 150,
    columnAlign: 'right',
  },
  {
    columnKey: 'status',
    columnHeader: '状态',
    columnWidth: 120,
    columnAlign: 'center',
  },
  {
    columnKey: 'action',
    columnHeader: '操作',
    columnAlign: 'center',
    columnWidth: 140,
  },
]

const tableRef = useTemplateRef('tableRef')

// 快照,用于撤回
const snapshot = new Map<number, Record<string, any>>()

const clearTableEditState = () => {
  tableRef.value?.setEditingState(null)
}

/** 整行编辑 */
const beginRowEdit = (row: Record<string, any> & { id: number }) => {
  snapshot.set(row.id, { ...row })
  tableRef.value?.setEditingState(row.id)
}

/** 单元格编辑 */
const beginCellEdit = (row: Record<string, any> & { id: number }, columnKey: string) => {
  snapshot.set(row.id, { ...row })
  tableRef.value?.setEditingState(row.id, columnKey)
}

const cancelEdit = (row: Record<string, any> & { id: number }) => {
  const original = snapshot.get(row.id)
  if (original) {
    Object.assign(row, original)
    snapshot.delete(row.id)
  }
  clearTableEditState()
}

const saveEdit = (row: Record<string, any> & { id: number }) => {
  snapshot.delete(row.id)
  clearTableEditState()
  console.log(data.value, 'data')
}
</script>

右键菜单

支持自定义右键菜单(插入行、列等行为)。

ID
姓名
部门
职位
薪资
邮箱
状态
当前内容为空
查看代码 在线链接
vue
<template>
  <VTable
    :data="tableData"
    :columns="columns"
    style="max-height: 400px"
    :row-key="(row) => row.id"
    :context-menu-config="{ enableHeaderMenu: true, enableCellMenu: true }"
    :bordered="false"
    :enable-row-hover="true"
  >
    <template #customContextMenu="{ context, close }">
      <!-- 表头右键菜单 -->
      <div v-if="context.type === 'header'" class="context-menu">
        <div class="context-menu-item" @click="handleSortAsc(context, close)">升序排序</div>
        <div class="context-menu-item" @click="handleSortDesc(context, close)">降序排序</div>
        <div class="context-menu-divider" />
        <div class="context-menu-item" @click="handleHideColumn(context, close)">隐藏列</div>
        <div class="context-menu-item" @click="handleInsertColumnLeft(context, close)">
          在左侧插入列
        </div>
        <div class="context-menu-item" @click="handleInsertColumnRight(context, close)">
          在右侧插入列
        </div>
        <div class="context-menu-item" @click="handleDeleteColumn(context, close)">删除当前列</div>
      </div>

      <!-- 单元格右键菜单 -->
      <div v-else-if="context.type === 'cell'" class="context-menu">
        <div class="context-menu-item" @click="handleInsertRowAbove(context, close)">
          在上方插入行
        </div>
        <div class="context-menu-item" @click="handleInsertRowBelow(context, close)">
          在下方插入行
        </div>
        <div class="context-menu-item" @click="handleDeleteRow(context, close)">删除当前行</div>
        <div class="context-menu-divider" />
        <div class="context-menu-item" @click="handleCopyCell(context, close)">复制单元格</div>
        <div class="context-menu-item" @click="handleCopyRow(context, close)">复制整行</div>
        <div class="context-menu-divider" />
        <div class="context-menu-item" @click="handleDuplicateRow(context, close)">复制当前行</div>
        <div
          v-if="context.row?.status === 'active'"
          class="context-menu-item"
          @click="handleSetInactive(context, close)"
        >
          设为离职
        </div>
        <div v-else class="context-menu-item" @click="handleSetActive(context, close)">
          设为在职
        </div>
        <template v-if="context.column.columnKey === 'salary'">
          <div class="context-menu-divider" />
          <div class="context-menu-item" @click="handleIncreaseSalary(context, close)">
            涨薪 10%
          </div>
          <div class="context-menu-item" @click="handleDecreaseSalary(context, close)">
            降薪 10%
          </div>
        </template>
      </div>
    </template>
  </VTable>
</template>

<script setup lang="ts">
import { h, ref } from 'vue'
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

defineOptions({ name: 'ContextMenuExample' })

// 数据类型定义
interface Employee {
  id: number
  name: string
  department: string
  position: string
  salary: number
  email: string
  status: 'active' | 'inactive'
}

// 表格数据
const tableData = ref<Employee[]>([
  {
    id: 1,
    name: '张三',
    department: '技术部',
    position: '前端工程师',
    salary: 15000,
    email: 'zhangsan@company.com',
    status: 'active',
  },
  {
    id: 2,
    name: '李四',
    department: '产品部',
    position: '产品经理',
    salary: 18000,
    email: 'lisi@company.com',
    status: 'active',
  },
  {
    id: 3,
    name: '王五',
    department: '技术部',
    position: '后端工程师',
    salary: 16000,
    email: 'wangwu@company.com',
    status: 'active',
  },
  {
    id: 4,
    name: '赵六',
    department: '设计部',
    position: 'UI 设计师',
    salary: 14000,
    email: 'zhaoliu@company.com',
    status: 'inactive',
  },
  {
    id: 5,
    name: '钱七',
    department: '市场部',
    position: '市场专员',
    salary: 12000,
    email: 'qianqi@company.com',
    status: 'active',
  },
])

// 列配置
const columns = ref<VTableColumn[]>([
  {
    columnKey: 'id',
    columnHeader: 'ID',
    columnWidth: 80,
    columnAlign: 'center',
  },
  {
    columnKey: 'name',
    columnHeader: '姓名',
    columnWidth: 120,
  },
  {
    columnKey: 'department',
    columnHeader: '部门',
    columnWidth: 120,
  },
  {
    columnKey: 'position',
    columnHeader: '职位',
    columnWidth: 150,
  },
  {
    columnKey: 'salary',
    columnHeader: '薪资',
    columnWidth: 120,
    columnAlign: 'right',
    columnCell: (props) => {
      const value = props.getValue()
      return h('span', { style: { color: '#1890ff', fontWeight: 600 } }, `¥${value}`)
    },
  },
  {
    columnKey: 'email',
    columnHeader: '邮箱',
    columnWidth: 200,
  },
  {
    columnKey: 'status',
    columnHeader: '状态',
    columnWidth: 100,
    columnAlign: 'center',
    columnCell: (props) => {
      const value = props.getValue()
      const isActive = value === 'active'
      return h(
        'span',
        {
          style: {
            padding: '4px 12px',
            borderRadius: '4px',
            fontSize: '12px',
            backgroundColor: isActive ? '#f6ffed' : '#fff1f0',
            color: isActive ? '#52c41a' : '#ff4d4f',
            border: `1px solid ${isActive ? '#b7eb8f' : '#ffccc7'}`,
          },
        },
        isActive ? '在职' : '离职',
      )
    },
  },
])

// 生成新 ID
let nextId = 6
const generateId = () => nextId++

// 右键菜单操作
const handleSortAsc = (context: any, close: () => void) => {
  console.log('升序排序:', context.column.columnKey)
  alert(`对 "${context.column.columnKey}" 列进行升序排序`)
  close()
}

const handleSortDesc = (context: any, close: () => void) => {
  console.log('降序排序:', context.column.columnKey)
  alert(`对 "${context.column.columnKey}" 列进行降序排序`)
  close()
}

const handleHideColumn = (context: any, close: () => void) => {
  console.log('隐藏列:', context.column.columnKey)
  alert(`隐藏 "${context.column.columnKey}" 列`)
  close()
}

const handleInsertColumnLeft = (context: any, close: () => void) => {
  const newColumnKey = `new_column_${Date.now()}`
  const newColumn: VTableColumn = {
    columnKey: newColumnKey,
    columnHeader: '新列',
    columnWidth: 120,
  }
  columns.value.splice(context.columnIndex, 0, newColumn)
  // 为所有行添加新列的默认值
  tableData.value.forEach((row: any) => {
    row[newColumnKey] = '-'
  })
  close()
}

const handleInsertColumnRight = (context: any, close: () => void) => {
  const newColumnKey = `new_column_${Date.now()}`
  const newColumn: VTableColumn = {
    columnKey: newColumnKey,
    columnHeader: '新列',
    columnWidth: 120,
  }
  columns.value.splice(context.columnIndex + 1, 0, newColumn)
  // 为所有行添加新列的默认值
  tableData.value.forEach((row: any) => {
    row[newColumnKey] = '-'
  })
  close()
}

const handleDeleteColumn = (context: any, close: () => void) => {
  if (columns.value.length <= 1) {
    alert('至少需要保留一列!')
    close()
    return
  }
  if (confirm(`确定要删除 "${context.column.columnKey}" 列吗?`)) {
    columns.value.splice(context.columnIndex, 1)
    // 从所有行中删除该列数据
    tableData.value.forEach((row: any) => {
      delete row[context.column.columnKey]
    })
  }
  close()
}

const handleInsertRowAbove = (context: any, close: () => void) => {
  const newRow: Employee = {
    id: generateId(),
    name: '新员工',
    department: '未分配',
    position: '待定',
    salary: 10000,
    email: `employee${nextId}@company.com`,
    status: 'active',
  }
  tableData.value.splice(context.rowIndex, 0, newRow)
  close()
}

const handleInsertRowBelow = (context: any, close: () => void) => {
  const newRow: Employee = {
    id: generateId(),
    name: '新员工',
    department: '未分配',
    position: '待定',
    salary: 10000,
    email: `employee${nextId}@company.com`,
    status: 'active',
  }
  tableData.value.splice(context.rowIndex + 1, 0, newRow)
  close()
}

const handleDeleteRow = (context: any, close: () => void) => {
  if (confirm(`确定要删除员工 "${context.row.name}" 的记录吗?`)) {
    tableData.value.splice(context.rowIndex, 1)
  }
  close()
}

const handleCopyCell = (context: any, close: () => void) => {
  const value = String(context.cellValue || '')
  navigator.clipboard.writeText(value).then(() => {
    alert(`已复制: ${value}`)
  })
  close()
}

const handleCopyRow = (context: any, close: () => void) => {
  const rowText = JSON.stringify(context.row, null, 2)
  navigator.clipboard.writeText(rowText).then(() => {
    alert('已复制整行数据到剪贴板')
  })
  close()
}

const handleDuplicateRow = (context: any, close: () => void) => {
  const newRow: Employee = {
    ...context.row,
    id: generateId(),
    name: `${context.row.name} (副本)`,
  }
  tableData.value.splice(context.rowIndex + 1, 0, newRow)
  close()
}

const handleSetInactive = (context: any, close: () => void) => {
  context.row.status = 'inactive'
  close()
}

const handleSetActive = (context: any, close: () => void) => {
  context.row.status = 'active'
  close()
}

const handleIncreaseSalary = (context: any, close: () => void) => {
  context.row.salary = Math.round(context.row.salary * 1.1)
  close()
}

const handleDecreaseSalary = (context: any, close: () => void) => {
  context.row.salary = Math.round(context.row.salary * 0.9)
  close()
}
</script>

<style scoped>
/* 右键菜单样式 */
.context-menu {
  background: #ffffff;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  padding: 4px 0;
  min-width: 160px;
}

.context-menu-item {
  padding: 8px 16px;
  cursor: pointer;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.85);
  transition: background-color 0.2s;
}

.context-menu-item:hover {
  background-color: #f5f5f5;
}

.context-menu-divider {
  height: 1px;
  background-color: #e8e8e8;
  margin: 4px 0;
}
</style>

行选择

支持多选、单选、禁用选择等功能。

已选择: 0 条
ID
姓名
年龄
状态
当前内容为空
查看代码 在线链接
vue
<template>
  <div>
    <div style="margin-bottom: 16px">
      <a-tag color="blue">已选择: {{ selectedRows.length }} 条</a-tag>
    </div>
    <VTable
      :data="data"
      :columns="columns"
      row-key="id"
      :row-selection-config="{
        enabled: true,
        getRowCheckDisabled: (row) => row.disabled ?? false,
        onChange: handleSelectionChange,
      }"
    />
  </div>
</template>

<script setup lang="ts">
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

defineOptions({ name: 'RowSelectionExample' })

interface DataRow {
  id: number
  name: string
  age: number
  status: string
  disabled?: boolean
}

const selectedRows = ref<DataRow[]>([])

const data = ref<DataRow[]>([
  { id: 1, name: '张三', age: 28, status: '正常' },
  { id: 2, name: '李四', age: 32, status: '正常' },
  { id: 3, name: '王五', age: 25, status: '已禁用', disabled: true },
  { id: 4, name: '赵六', age: 35, status: '正常' },
  { id: 5, name: '钱七', age: 29, status: '已禁用', disabled: true },
])

const columns: VTableColumn[] = [
  { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
  { columnKey: 'name', columnHeader: '姓名', columnWidth: 120 },
  { columnKey: 'age', columnHeader: '年龄', columnWidth: 100, columnAlign: 'center' },
  { columnKey: 'status', columnHeader: '状态' },
]

const handleSelectionChange = (rows: DataRow[]) => {
  selectedRows.value = rows
}
</script>

排序和筛选

支持列排序状态与自定义筛选交互,通常结合 table-change 在外部处理数据。

ID
姓名
年龄
部门
当前内容为空
查看代码 在线链接
vue
<template>
  <VTable
    v-model:default-sort="sortState"
    v-model:default-filter="filterState"
    :data="data"
    :columns="columns"
    @table-change="handleTableChange"
  >
    <template #customFilterDropdown="{ confirm, reset, setFilterValue, filterModelValue }">
      <div style="padding: 8px">
        <a-input
          :value="filterModelValue"
          placeholder="输入关键词"
          style="width: 200px; margin-bottom: 8px"
          @input="(e: any) => setFilterValue(e.target.value)"
          @press-enter="confirm"
        />
        <div style="display: flex; gap: 8px">
          <a-button type="primary" size="small" @click="confirm">确定</a-button>
          <a-button size="small" @click="reset">重置</a-button>
        </div>
      </div>
    </template>
  </VTable>
</template>

<script setup lang="ts">
import VTable, {
  type VTableChangeState,
  type VTableColumn,
  type VTableColumnFiltersState,
  type VTableSortingState,
} from '@aimerthyr/virtual-table'
import { message } from 'ant-design-vue'

defineOptions({ name: 'SortFilterExample' })

const sortState = ref<VTableSortingState>([])
const filterState = ref<VTableColumnFiltersState>([])

const data = ref([
  { id: 1, name: '张三', age: 28, department: '技术部' },
  { id: 2, name: '李四', age: 32, department: '产品部' },
  { id: 3, name: '王五', age: 25, department: '技术部' },
  { id: 4, name: '赵六', age: 35, department: '运营部' },
  { id: 5, name: '钱七', age: 29, department: '产品部' },
])

const columns: VTableColumn[] = [
  { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
  {
    columnKey: 'name',
    columnHeader: '姓名',
    columnWidth: 120,
    columnEnableFilter: true,
  },
  {
    columnKey: 'age',
    columnHeader: '年龄',
    columnWidth: 100,
    columnAlign: 'center',
    columnEnableSort: true,
  },
  {
    columnKey: 'department',
    columnHeader: '部门',
    columnEnableFilter: true,
  },
]

const handleTableChange = (changeState: VTableChangeState) => {
  // 拿到对应筛选的值,然后触发后端筛选
  console.log('changeState', changeState)
  message.info('请求后端数据,手动实现')
}
</script>

分页

支持分页功能,可以设置分页器的位置、模式等。

总数据: 200 条当前页: 1
ID
姓名
年龄
邮箱
部门
职位
当前内容为空
查看代码 在线链接
vue
<template>
  <div>
    <div
      style="
        margin-bottom: 16px;
        display: flex;
        align-items: center;
        justify-content: space-between;
      "
    >
      <div>
        <a-tag color="blue">总数据: {{ totalCount }} 条</a-tag>
        <a-tag color="green">当前页: {{ pagination.pageIndex }}</a-tag>
      </div>
      <a-radio-group v-model:value="paginationPlacement" button-style="solid">
        <a-radio-button value="left">左对齐</a-radio-button>
        <a-radio-button value="center">居中</a-radio-button>
        <a-radio-button value="right">右对齐</a-radio-button>
      </a-radio-group>
    </div>

    <VTable
      v-model:default-pagination="pagination"
      :data="tableData"
      :columns="columns"
      :loading="loading"
      :pagination-config="{
        enabled: true,
        placement: paginationPlacement,
        total: totalCount,
      }"
      @table-change="handleTableChange"
    />
  </div>
</template>

<script setup lang="ts">
import VTable, {
  type VTableChangeState,
  type VTableColumn,
  type VTablePaginationState,
} from '@aimerthyr/virtual-table'

defineOptions({ name: 'PaginationExample' })

const loading = ref(false)
const totalCount = ref(200)
const paginationPlacement = ref<'left' | 'center' | 'right'>('right')

const pagination = ref<VTablePaginationState>({
  pageIndex: 1,
  pageSize: 10,
})

const tableData = ref<any[]>([])

const columns: VTableColumn[] = [
  { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
  { columnKey: 'name', columnHeader: '姓名', columnWidth: 120 },
  { columnKey: 'age', columnHeader: '年龄', columnWidth: 100, columnAlign: 'center' },
  { columnKey: 'email', columnHeader: '邮箱', columnWidth: 200 },
  { columnKey: 'department', columnHeader: '部门', columnWidth: 120 },
  { columnKey: 'position', columnHeader: '职位' },
]

// 模拟后端分页数据
const fetchData = async (page: number, pageSize: number) => {
  loading.value = true

  // 模拟网络延迟
  await new Promise((resolve) => setTimeout(resolve, 500))

  const start = (page - 1) * pageSize
  const data = Array.from({ length: pageSize }, (_, i) => {
    const id = start + i + 1
    return {
      id,
      name: `员工${id}`,
      age: 20 + (id % 30),
      email: `user${id}@example.com`,
      department: ['技术部', '产品部', '运营部', '市场部'][id % 4],
      position: ['工程师', '经理', '专员', '主管'][id % 4],
    }
  })

  tableData.value = data
  loading.value = false
}

// 表格状态变化处理
const handleTableChange = (state: VTableChangeState) => {
  fetchData(state.pagination.pageIndex, state.pagination.pageSize)
}

// 初始化加载
onMounted(() => {
  fetchData(pagination.value.pageIndex, pagination.value.pageSize)
})
</script>

树形数据

支持树形数据展示

部门名称
人数
当前内容为空
查看代码 在线链接
vue
<template>
  <VTable
    :data="data"
    :columns="columns"
    :bordered="true"
    row-key="id"
    :tree-config="{
      enabled: true,
      childrenKey: 'children',
    }"
    @expand="handleExpand"
  />
</template>

<script setup lang="ts">
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

defineOptions({ name: 'TreeDataExample' })

const data = ref([
  {
    id: 1,
    name: '技术部',
    count: 15,
    children: [
      { id: 11, name: '前端组', count: 8 },
      { id: 12, name: '后端组', count: 7 },
    ],
  },
  {
    id: 2,
    name: '产品部',
    count: 10,
    children: [
      { id: 21, name: '产品组', count: 6 },
      { id: 22, name: '设计组', count: 4 },
    ],
  },
  {
    id: 3,
    name: '运营部',
    count: 8,
    children: [],
  },
])

const columns: VTableColumn[] = [
  { columnKey: 'name', columnHeader: '部门名称', columnWidth: 200 },
  { columnKey: 'count', columnHeader: '人数', columnAlign: 'center' },
]

const handleExpand = (expanded: boolean, row: any) => {
  if (row.id === 3 && expanded) {
    row.children = [
      { id: 211, name: '运营组1', count: 6 },
      { id: 212, name: '运营组2', count: 4 },
    ]
  }
}
</script>

可展开行

支持可展开行功能,可以设置展开行的折叠状态。

ID
姓名
年龄
当前内容为空
查看代码 在线链接
vue
<template>
  <VTable :data="data" :columns="columns" row-key="id" :enable-expand-row="true">
    <template #expandedRowRender="{ row }">
      <div style="background-color: #f0f0f0; padding: 16px">
        <p><strong>详细信息:</strong></p>
        <p>邮箱:{{ row.email }}</p>
        <p>电话:{{ row.phone }}</p>
        <p>地址:{{ row.address }}</p>
      </div>
    </template>
  </VTable>
</template>

<script setup lang="ts">
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

defineOptions({ name: 'ExpandableExample' })

const data = ref([
  {
    id: 1,
    name: '张三',
    age: 28,
    email: 'zhangsan@example.com',
    phone: '13800138000',
    address: '北京市朝阳区',
  },
  {
    id: 2,
    name: '李四',
    age: 32,
    email: 'lisi@example.com',
    phone: '13800138001',
    address: '上海市浦东新区',
  },
  {
    id: 3,
    name: '王五',
    age: 25,
    email: 'wangwu@example.com',
    phone: '13800138002',
    address: '广州市天河区',
  },
])

const columns: VTableColumn[] = [
  { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
  { columnKey: 'name', columnHeader: '姓名', columnWidth: 120 },
  { columnKey: 'age', columnHeader: '年龄', columnAlign: 'center' },
]
</script>

固定列

支持固定列功能,可以设置固定列的位置、模式等。

ID
姓名
年龄
邮箱
电话
地址
状态
当前内容为空
查看代码 在线链接
vue
<template>
  <div>
    <div style="margin-bottom: 16px">
      <a-button @click="handleFixColumns">固定左右列</a-button>
      <a-button style="margin-left: 8px" @click="handleClearFixed">取消固定</a-button>
    </div>
    <VTable
      v-model:default-column-pinning="columnPinning"
      :data="data"
      :columns="columns"
      row-key="id"
    />
  </div>
</template>

<script setup lang="ts">
import VTable, { type VTableColumn, type VTableColumnPinningState } from '@aimerthyr/virtual-table'

defineOptions({ name: 'FixedColumnsExample' })

const columnPinning = ref<VTableColumnPinningState>({})

const data = ref([
  {
    id: 1,
    name: '张三',
    age: 28,
    email: 'zhangsan@example.com',
    phone: '13800138000',
    address: '北京市朝阳区',
    status: '正常',
  },
  {
    id: 2,
    name: '李四',
    age: 32,
    email: 'lisi@example.com',
    phone: '13800138001',
    address: '上海市浦东新区',
    status: '正常',
  },
  {
    id: 3,
    name: '王五',
    age: 25,
    email: 'wangwu@example.com',
    phone: '13800138002',
    address: '广州市天河区',
    status: '正常',
  },
  {
    id: 4,
    name: '赵六',
    age: 35,
    email: 'zhaoliu@example.com',
    phone: '13800138003',
    address: '深圳市南山区',
    status: '正常',
  },
])

const columns: VTableColumn[] = [
  { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
  { columnKey: 'name', columnHeader: '姓名', columnWidth: '30%' },
  { columnKey: 'age', columnHeader: '年龄', columnWidth: '20%', columnAlign: 'center' },
  { columnKey: 'email', columnHeader: '邮箱', columnWidth: 200 },
  { columnKey: 'phone', columnHeader: '电话', columnWidth: 150 },
  { columnKey: 'address', columnHeader: '地址', columnWidth: 200 },
  { columnKey: 'status', columnHeader: '状态', columnWidth: 100 },
]

const handleFixColumns = () => {
  columnPinning.value = {
    left: ['name'],
    right: ['status'],
  }
}

const handleClearFixed = () => {
  columnPinning.value = {}
}
</script>

列宽调整

支持列宽调整功能,可以设置列宽的调整模式等。

调整模式:
ID
姓名
年龄
邮箱
电话
地址
部门
当前内容为空
查看代码 在线链接
vue
<template>
  <div>
    <div style="margin-bottom: 16px; display: flex; flex-direction: column; gap: 16px">
      <div>
        <span style="margin-right: 8px">调整模式:</span>
        <a-radio-group v-model:value="resizeMode" @change="handleResizeModeChange">
          <a-radio value="onChange">实时调整 (onChange)</a-radio>
          <a-radio value="onEnd">结束后调整 (onEnd)</a-radio>
        </a-radio-group>
      </div>
    </div>

    <div style="margin-bottom: 16px">
      <a-space>
        <a-button @click="handleResetWidth">重置列宽</a-button>
        <a-button @click="handleShowSizing">查看当前列宽</a-button>
      </a-space>
    </div>

    <VTable
      :key="resizeMode"
      v-model:default-column-sizing="columnSizing"
      :data="data"
      :columns="columns"
      :column-resize-mode="resizeMode"
      @column-sizing-change="handleColumnSizingChange"
    />

    <!-- 列宽信息展示 -->
    <a-modal v-model:open="sizingModalVisible" title="当前列宽信息" :footer="null" width="600px">
      <a-descriptions bordered :column="1">
        <a-descriptions-item
          v-for="(width, key) in columnSizing"
          :key="key"
          :label="getColumnLabel(key)"
        >
          {{ width }}px
        </a-descriptions-item>
      </a-descriptions>
    </a-modal>
  </div>
</template>

<script setup lang="ts">
import VTable, { type VTableColumn, type VTableColumnSizingState } from '@aimerthyr/virtual-table'
import { message } from 'ant-design-vue'

defineOptions({ name: 'ColumnResizeExample' })

const resizeMode = ref<'onChange' | 'onEnd'>('onEnd')
const columnSizing = ref<VTableColumnSizingState>({})
const sizingModalVisible = ref(false)

const data = ref([
  {
    id: 1,
    name: '张三',
    age: 28,
    email: 'zhangsan@example.com',
    phone: '13800138000',
    address: '北京市朝阳区某某街道',
    department: '技术部',
  },
  {
    id: 2,
    name: '李四',
    age: 32,
    email: 'lisi@example.com',
    phone: '13800138001',
    address: '上海市浦东新区某某路',
    department: '产品部',
  },
  {
    id: 3,
    name: '王五',
    age: 25,
    email: 'wangwu@example.com',
    phone: '13800138002',
    address: '广州市天河区某某大道',
    department: '运营部',
  },
  {
    id: 4,
    name: '赵六',
    age: 35,
    email: 'zhaoliu@example.com',
    phone: '13800138003',
    address: '深圳市南山区某某中心',
    department: '市场部',
  },
])

const columns: VTableColumn[] = [
  {
    columnKey: 'id',
    columnHeader: 'ID',
    columnWidth: 80,
    columnEnableResize: true,
    columnMinWidth: 60,
    columnMaxWidth: 150,
  },
  {
    columnKey: 'name',
    columnHeader: '姓名',
    columnWidth: 120,
    columnEnableResize: true,
    columnMinWidth: 80,
    columnMaxWidth: 200,
  },
  {
    columnKey: 'age',
    columnHeader: '年龄',
    columnWidth: 100,
    columnAlign: 'center',
    columnEnableResize: true,
    columnMinWidth: 80,
  },
  {
    columnKey: 'email',
    columnHeader: '邮箱',
    columnWidth: 200,
    columnEnableResize: true,
    columnMinWidth: 150,
    columnMaxWidth: 300,
  },
  {
    columnKey: 'phone',
    columnHeader: '电话',
    columnWidth: 150,
    columnEnableResize: true,
  },
  {
    columnKey: 'address',
    columnHeader: '地址',
    columnWidth: 250,
    columnEnableResize: true,
    columnMinWidth: 150,
  },
  {
    columnKey: 'department',
    columnHeader: '部门',
    columnWidth: 120,
    columnEnableResize: true,
  },
]

// 列宽变化回调
const handleColumnSizingChange = (sizing: VTableColumnSizingState) => {
  console.log('列宽已调整:', sizing)
}

// 重置列宽
const handleResetWidth = () => {
  columnSizing.value = {}
  message.success('列宽已重置')
}

// 显示当前列宽
const handleShowSizing = () => {
  if (Object.keys(columnSizing.value).length === 0) {
    message.info('当前使用默认列宽,未进行调整')
    return
  }
  sizingModalVisible.value = true
}

// 获取列名称
const getColumnLabel = (key: string) => {
  const column = columns.find((col) => col.columnKey === key)
  return column?.columnHeader || key
}

// 切换调整模式
const handleResizeModeChange = () => {
  const modeName = resizeMode.value === 'onChange' ? '实时调整' : '结束后调整'
  message.success(`已切换到 ${modeName} 模式,请尝试拖拽列宽观察区别`)
}
</script>

单元格合并

支持单元格合并功能,可以设置单元格的合并模式等。

ID
姓名
部门
职位
当前内容为空
查看代码 在线链接
vue
<template>
  <VTable :data="data" :columns="columns" :custom-cell-attributes="customCellHandler" />
</template>

<script setup lang="ts">
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

defineOptions({ name: 'CellMergeExample' })

const data = ref([
  { id: 1, name: '张三', department: '技术部', position: '前端工程师' },
  { id: 2, name: '李四', department: '技术部', position: '后端工程师' },
  { id: 3, name: '王五', department: '技术部', position: '测试工程师' },
  { id: 4, name: '赵六', department: '产品部', position: '产品经理' },
  { id: 5, name: '钱七', department: '产品部', position: 'UI设计师' },
  { id: 6, name: '孙八', department: '运营部', position: '运营专员' },
])

const columns: VTableColumn[] = [
  { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
  { columnKey: 'name', columnHeader: '姓名', columnWidth: 120 },
  { columnKey: 'department', columnHeader: '部门', columnWidth: 150 },
  { columnKey: 'position', columnHeader: '职位' },
]

/**
 * 单元格合并处理
 * 相同部门的行,部门列进行纵向合并
 */
const customCellHandler = (row: any, column: VTableColumn | undefined, rowIndex: number) => {
  if (!column || column.columnKey !== 'department') {
    return null
  }

  // 查找当前行之前有多少个相同部门的连续行
  let prevSameCount = 0
  for (let i = rowIndex - 1; i >= 0; i--) {
    if (data.value[i]?.department === row.department) {
      prevSameCount++
    } else {
      break
    }
  }

  // 如果前面有相同部门,当前单元格不渲染
  if (prevSameCount > 0) {
    return { rowspan: 0 }
  }

  // 查找当前行之后有多少个相同部门的连续行
  let nextSameCount = 0
  for (let i = rowIndex + 1; i < data.value.length; i++) {
    if (data.value[i]?.department === row.department) {
      nextSameCount++
    } else {
      break
    }
  }

  // 返回合并的行数
  return {
    rowspan: 1 + nextSameCount,
    style: {
      background: '#fafafa',
      fontWeight: 'bold',
    },
  }
}
</script>

汇总行

支持汇总行功能,可以对数据进行统计计算(求和、平均值、计数、最大值、最小值等)。

ID
产品名称
分类
单价
库存
销量
评分
总价值
总计
50
498.71
12002
25105
4.09
¥5814702.99
当前内容为空
查看代码 在线链接
vue
<template>
  <div>
    <div style="display: flex; margin-bottom: 8px">
      <a-button @click="summaryFixed = !summaryFixed">
        {{ summaryFixed ? '取消固定' : '固定汇总行' }}
      </a-button>
    </div>

    <VTable
      :data="tableData"
      :columns="columns"
      :fixed-header="true"
      :summary-config="{
        enabled: true,
        fixed: summaryFixed,
      }"
      :style="{ height: '500px' }"
    >
      <template #summaryCell="{ columnKey, summaryValue }">
        <div v-if="columnKey === 'id'" style="color: blue">总计</div>
        <div v-else style="color: red">
          {{ summaryValue }}
        </div>
      </template>
    </VTable>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'

interface Product {
  id: number
  name: string
  category: string
  price: number
  quantity: number
  sales: number
  rating: number
}

const summaryFixed = ref(false)

const columns: VTableColumn<Product>[] = [
  {
    columnKey: 'id',
    columnHeader: 'ID',
    columnWidth: 80,
  },
  {
    columnKey: 'name',
    columnHeader: '产品名称',
    columnWidth: 150,
    columnSummary: 'count',
  },
  {
    columnKey: 'category',
    columnHeader: '分类',
    columnWidth: 120,
    columnSummary: 'count',
  },
  {
    columnKey: 'price',
    columnHeader: '单价',
    columnWidth: 100,
    columnAlign: 'right',
    columnCell: ({ row }) => `¥${row.original.price.toFixed(2)}`,
    columnSummary: 'avg',
  },
  {
    columnKey: 'quantity',
    columnHeader: '库存',
    columnWidth: 100,
    columnAlign: 'right',
    columnSummary: 'sum',
  },
  {
    columnKey: 'sales',
    columnHeader: '销量',
    columnWidth: 100,
    columnAlign: 'right',
    columnSummary: 'sum',
  },
  {
    columnKey: 'rating',
    columnHeader: '评分',
    columnWidth: 100,
    columnAlign: 'center',
    columnSummary: 'avg',
  },
  {
    columnKey: 'total',
    columnHeader: '总价值',
    columnWidth: 120,
    columnAlign: 'right',
    columnCell: ({ row }) => `¥${(row.original.price * row.original.quantity).toFixed(2)}`,
    // 自定义汇总计算
    columnSummary: (data) => {
      const total = data.reduce((sum, item) => sum + item.price * item.quantity, 0)
      return `¥${total.toFixed(2)}`
    },
  },
]

const tableData = ref<Product[]>(
  Array.from({ length: 50 }, (_, i) => ({
    id: i + 1,
    name: `产品 ${i + 1}`,
    category: ['电子产品', '服装', '食品', '图书', '家居'][i % 5] as string,
    price: Math.random() * 1000 + 50,
    quantity: Math.floor(Math.random() * 500) + 10,
    sales: Math.floor(Math.random() * 1000),
    rating: Math.random() * 2 + 3, // 3-5分
  })),
)
</script>

自定义 Slot

支持自定义 Slot 功能,可以设置自定义 Slot 的渲染方式等。

姓名
部门
薪资
状态
操作

暂无数据

查看代码 在线链接
vue
<template>
  <div style="margin-bottom: 8px; display: flex; align-items: center; gap: 8px">
    <a-button @click="handleLoading">触发 loading</a-button>
    <a-button @click="handleEmpty">触发空状态</a-button>
    <a-button @click="data = dataList">重置</a-button>
  </div>
  <VTable
    :data="data"
    :columns="columns"
    :bordered="true"
    row-key="id"
    :loading="loading"
    :row-selection-config="{
      enabled: true,
    }"
    :default-checkbox-column-width="100"
    :adaptive-column-width="180"
  >
    <!-- 自定义表头单元格 -->
    <template #headerCell="{ column }">
      <span v-if="column.columnKey === 'name'" class="flex items-center gap-2">
        <UserOutlined />
        <span>{{ column.columnHeader }}</span>
      </span>
    </template>

    <!-- 自定义数据单元格 -->
    <template #bodyCell="{ columnKey, row }">
      <template v-if="columnKey === 'name'">
        <a-tag :color="row.level === 'senior' ? 'gold' : 'blue'">
          {{ row.name }}
        </a-tag>
      </template>
      <template v-else-if="columnKey === 'status'">
        <a-badge
          :status="row.status === 'active' ? 'success' : 'error'"
          :text="row.status === 'active' ? '在职' : '离职'"
        />
      </template>
      <template v-else-if="columnKey === 'salary'">
        <span class="font-semibold text-green-600"> ¥{{ row.salary.toLocaleString() }} </span>
      </template>
      <template v-else-if="columnKey === 'action'">
        <a-space>
          <a-button type="link" size="small" @click="handleEdit(row)">
            <EditOutlined />
            编辑
          </a-button>
          <a-button type="link" size="small" danger @click="handleDelete(row)">
            <DeleteOutlined />
            删除
          </a-button>
        </a-space>
      </template>
    </template>

    <!-- 自定义复选框 -->
    <template #customCheckbox="{ checked, indeterminate, disabled, onCheckedChange }">
      <a-checkbox
        :checked="checked"
        :indeterminate="indeterminate"
        :disabled="disabled"
        @change="onCheckedChange"
      >
        <span v-if="!checked && !indeterminate" class="text-xs text-gray-400">选择</span>
      </a-checkbox>
    </template>

    <!-- 自定义筛选图标 -->
    <template #customFilterIcon="{ filtered }">
      <FilterOutlined :style="{ color: filtered ? '#1890ff' : undefined }" />
    </template>

    <!-- 自定义筛选下拉 -->
    <template #customFilterDropdown="{ confirm, reset, setFilterValue, filterModelValue }">
      <div class="w-[250px] p-3">
        <h3 class="mb-[8px]">我可以自定义筛选下拉</h3>
        <a-input
          :value="filterModelValue"
          placeholder="输入姓名搜索"
          class="mb-2"
          @input="(e: any) => setFilterValue(e.target.value)"
          @press-enter="confirm"
        >
          <template #prefix>
            <SearchOutlined />
          </template>
        </a-input>
        <div class="flex justify-end gap-2">
          <a-button size="small" @click="reset">重置</a-button>
          <a-button type="primary" size="small" @click="confirm">确定</a-button>
        </div>
      </div>
    </template>

    <!-- 自定义加载图标 -->
    <template #customLoadingIcon>
      <LoadingOutlined style="font-size: 24px" spin />
    </template>

    <!-- 自定义空状态 -->
    <template #customEmpty>
      <div class="py-12 text-center">
        <InboxOutlined style="font-size: 48px; color: #d9d9d9" />
        <p class="mt-4 text-gray-500">暂无数据</p>
        <a-button type="primary" class="mt-2" @click="handleAddData">
          <PlusOutlined />
          添加数据
        </a-button>
      </div>
    </template>
  </VTable>
</template>

<script setup lang="ts">
import VTable, { type VTableColumn } from '@aimerthyr/virtual-table'
import {
  DeleteOutlined,
  EditOutlined,
  FilterOutlined,
  InboxOutlined,
  LoadingOutlined,
  PlusOutlined,
  SearchOutlined,
  UserOutlined,
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'

defineOptions({ name: 'CustomSlotExample' })

const loading = ref(false)
const dataList = [
  {
    id: 1,
    name: '张三',
    level: 'senior',
    status: 'active',
    salary: 25000,
    department: '技术部',
  },
  {
    id: 2,
    name: '李四',
    level: 'junior',
    status: 'active',
    salary: 15000,
    department: '产品部',
  },
  {
    id: 3,
    name: '王五',
    level: 'senior',
    status: 'inactive',
    salary: 28000,
    department: '技术部',
  },
  {
    id: 4,
    name: '赵六',
    level: 'junior',
    status: 'active',
    salary: 18000,
    department: '运营部',
  },
]

const data = ref(dataList)

const columns: VTableColumn[] = [
  {
    columnKey: 'name',
    columnHeader: '姓名',
    columnWidth: 150,
    columnEnableFilter: true,
  },
  {
    columnKey: 'department',
    columnHeader: '部门',
    columnWidth: 120,
  },
  {
    columnKey: 'salary',
    columnHeader: '薪资',
    columnWidth: 150,
    columnAlign: 'right',
  },
  {
    columnKey: 'status',
    columnHeader: '状态',
    columnWidth: 120,
    columnAlign: 'center',
  },
  {
    columnKey: 'action',
    columnHeader: '操作',
    columnAlign: 'center',
  },
]

const handleEdit = (row: any) => {
  message.info(`编辑: ${row.name}`)
}

const handleDelete = (row: any) => {
  message.warning(`删除: ${row.name}`)
}

const handleAddData = () => {
  data.value.push({
    id: data.value.length + 1,
    name: `新员工${data.value.length + 1}`,
    level: 'junior',
    status: 'active',
    salary: 12000,
    department: '技术部',
  })
  message.success('添加成功')
}

const handleLoading = () => {
  loading.value = true
  setTimeout(() => {
    loading.value = false
  }, 1000)
}

const handleEmpty = () => {
  data.value = []
}
</script>

自定义主题

支持自定义主题功能,可以设置主题的样式等。

ID
姓名
年龄
邮箱
部门
状态
当前内容为空
查看代码 在线链接
vue
<template>
  <div class="custom-theme-example">
    <div class="theme-selector">
      <a-radio-group v-model:value="currentTheme" button-style="solid">
        <a-radio-button value="default">默认主题</a-radio-button>
        <a-radio-button value="dark">暗黑主题</a-radio-button>
        <a-radio-button value="blue">蓝色主题</a-radio-button>
        <a-radio-button value="green">绿色主题</a-radio-button>
      </a-radio-group>
    </div>

    <VTable
      :key="currentTheme"
      v-model:default-pagination="pagination"
      v-model:default-filter="filterState"
      :data="displayData"
      :columns="columns"
      :theme-config="themeConfig"
      :enable-row-hover="true"
      :fixed-header="true"
      :pagination-config="{
        enabled: true,
        mode: 'client',
        placement: 'right',
        total: data.length,
      }"
      class="h-[500px]"
      @table-change="handleTableChange"
    >
      <template #customFilterDropdown="{ confirm, reset, setFilterValue, filterModelValue }">
        <div class="filter-dropdown">
          <a-input
            :value="filterModelValue"
            placeholder="输入关键词筛选"
            class="filter-input"
            @input="(e: any) => setFilterValue(e.target.value)"
            @press-enter="confirm"
          />
          <div class="filter-actions">
            <a-button type="primary" size="small" @click="confirm">确定</a-button>
            <a-button size="small" @click="reset">重置</a-button>
          </div>
        </div>
      </template>
    </VTable>
  </div>
</template>

<script setup lang="ts">
import VTable, {
  type VTableChangeState,
  type VTableColumn,
  type VTableColumnFiltersState,
  type VTablePaginationState,
  type VTableThemeConfig,
} from '@aimerthyr/virtual-table'

defineOptions({ name: 'CustomTheme' })

const currentTheme = ref<'default' | 'dark' | 'blue' | 'green'>('default')
const pagination = ref<VTablePaginationState>({ pageIndex: 1, pageSize: 10 })
const filterState = ref<VTableColumnFiltersState>([])

// 生成更多数据用于分页展示
const data = ref(
  Array.from({ length: 50 }, (_, i) => ({
    id: i + 1,
    name: `用户${i + 1}`,
    age: 20 + (i % 30),
    email: `user${i + 1}@example.com`,
    department: ['技术部', '产品部', '运营部'][i % 3],
    status: ['在职', '离职', '休假'][i % 3],
  })),
)

// 客户端筛选逻辑
const displayData = computed(() => {
  let result = [...data.value]

  // 应用筛选
  filterState.value.forEach((filter) => {
    const filterValue = String(filter.value).toLowerCase()
    if (filterValue) {
      result = result.filter((row) => {
        const cellValue = String(row[filter.id as keyof typeof row]).toLowerCase()
        return cellValue.includes(filterValue)
      })
    }
  })

  return result
})

const columns: VTableColumn[] = [
  { columnKey: 'id', columnHeader: 'ID', columnWidth: 80 },
  {
    columnKey: 'name',
    columnHeader: '姓名',
    columnWidth: 120,
    columnEnableFilter: true,
  },
  { columnKey: 'age', columnHeader: '年龄', columnWidth: 100, columnAlign: 'center' },
  {
    columnKey: 'email',
    columnHeader: '邮箱',
    columnWidth: 200,
    columnEnableFilter: true,
  },
  {
    columnKey: 'department',
    columnHeader: '部门',
    columnWidth: 120,
    columnEnableFilter: true,
  },
  {
    columnKey: 'status',
    columnHeader: '状态',
    columnEnableFilter: true,
  },
]

const handleTableChange = (changeState: VTableChangeState) => {
  console.log('表格状态变化:', changeState)
}

// 主题配置映射
const themeConfigs: Record<string, VTableThemeConfig> = {
  default: {
    primaryColor: '#1890ff',
    header: {
      color: 'rgba(0, 0, 0, 0.88)',
      backgroundColor: '#fafafa',
      borderRadius: 0,
      splitColor: 'rgba(0, 0, 0, 0.06)',
      headerIconColor: 'rgba(0, 0, 0, 0.45)',
      padding: 16,
    },
    body: {
      color: 'rgba(0, 0, 0, 0.88)',
      backgroundColor: '#ffffff',
      padding: 16,
    },
    border: {
      borderStyle: 'solid',
      borderColor: '#f0f0f0',
    },
    rowHoverColor: '#fafafa',
    zIndex: {
      pinnedColumn: 2,
      fixedHeader: 3,
      fixedFooter: 3,
    },
  },
  dark: {
    primaryColor: '#177ddc',
    header: {
      color: 'rgba(255, 255, 255, 0.85)',
      backgroundColor: '#1f1f1f',
      borderRadius: 0,
      splitColor: 'rgba(255, 255, 255, 0.12)',
      headerIconColor: 'rgba(255, 255, 255, 0.45)',
      padding: 16,
    },
    body: {
      color: 'rgba(255, 255, 255, 0.85)',
      backgroundColor: '#141414',
      padding: 16,
    },
    border: {
      borderStyle: 'solid',
      borderColor: '#303030',
    },
    rowHoverColor: '#262626',
    zIndex: {
      pinnedColumn: 2,
      fixedHeader: 3,
      fixedFooter: 3,
    },
  },
  blue: {
    primaryColor: '#1890ff',
    header: {
      color: '#ffffff',
      backgroundColor: '#1890ff',
      borderRadius: 8,
      splitColor: 'rgba(255, 255, 255, 0.2)',
      headerIconColor: 'rgba(255, 255, 255, 0.85)',
      padding: 16,
    },
    body: {
      color: 'rgba(0, 0, 0, 0.88)',
      backgroundColor: '#ffffff',
      padding: 16,
    },
    border: {
      borderStyle: 'solid',
      borderColor: '#e6f7ff',
    },
    rowHoverColor: '#e6f7ff',
    zIndex: {
      pinnedColumn: 2,
      fixedHeader: 3,
      fixedFooter: 3,
    },
  },
  green: {
    primaryColor: '#52c41a',
    header: {
      color: '#ffffff',
      backgroundColor: '#52c41a',
      borderRadius: 8,
      splitColor: 'rgba(255, 255, 255, 0.2)',
      headerIconColor: 'rgba(255, 255, 255, 0.85)',
      padding: 16,
    },
    body: {
      color: 'rgba(0, 0, 0, 0.88)',
      backgroundColor: '#ffffff',
      padding: 16,
    },
    border: {
      borderStyle: 'solid',
      borderColor: '#f6ffed',
    },
    rowHoverColor: '#f6ffed',
    zIndex: {
      pinnedColumn: 2,
      fixedHeader: 3,
      fixedFooter: 3,
    },
  },
}

const themeConfig = computed(() => themeConfigs[currentTheme.value])
</script>

<style scoped lang="less">
.custom-theme-example {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.theme-selector {
  display: flex;
  justify-content: center;
  padding: 16px;
  background: #fafafa;
  border-radius: 4px;
}

.filter-dropdown {
  padding: 8px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  border-radius: 4px;
}

.filter-input {
  width: 200px;
  margin-bottom: 8px;
}

.filter-actions {
  display: flex;
  gap: 8px;
}
</style>