Skip to content

TablePro 表格组件

基于 TDesign Vue Next 的增强表格组件,提供搜索表单、分页、排序、过滤等高级功能。

功能特性

  • 🔍 智能搜索表单 - 自动根据列配置生成搜索表单
  • 📄 分页支持 - 内置分页功能,支持前端和后端分页
  • 🔄 数据请求 - 支持异步数据请求和自动加载状态
  • 📊 排序过滤 - 支持列排序和数据过滤
  • 🎨 插槽扩展 - 丰富的插槽支持,灵活定制表格头部和内容
  • 🔧 类型安全 - 完整的 TypeScript 类型定义
  • 📱 响应式布局 - 搜索表单自适应容器宽度

基础使用

Center Header
Left Header
Right Header
ID
Name
Age
Address
Email
Phone
Actions
1John Doe30123 Main St---
2Jane Smith25456 Elm St---
3Alice Johnson28789 Maple Ave---
4Bob Brown35101 Pine Rd---
5Charlie White22202 Oak St---
6Diana Green27303 Birch Blvd---
7Ethan Blue32404 Cedar Ct---
8Fiona Black29505 Spruce Dr---
9George Gray31606 Willow Way---
10Hannah Yellow26707 Cherry Ln---
11Ian Purple34808 Ash St---
12Julia Orange23909 Palm Pl---
13Kevin Pink301010 Magnolia Ave---
14Laura Cyan281111 Peach St---
15Mike Red331212 Maple St---
16Nina Teal241313 Cedar St---
17Oscar Silver361414 Oak St---
18Paula Gold211515 Pine St---
19Quinn Bronze291616 Birch St---
20Rita Copper321717 Spruce St---
共 0 条数据
50 条/页
  • 1
查看代码
vue
<template>
  <div class="base-table-container">
    <TablePro :columns="columns" row-key="id" :data="data">
      <template #tableHeaderCenter>
        <div>Center Header</div>
      </template>
      <template #tableHeaderLeft>
        <div>Left Header</div>
      </template>
      <template #tableHeaderRight>
        <div>Right Header</div>
      </template>
    </TablePro>
  </div>
</template>

<script setup lang="ts">
import type { ProTableCol } from 't-design-pro'
import { TablePro } from 't-design-pro'
import { useData } from 'vitepress'
import { ref, watch } from 'vue'

const { isDark } = useData()

watch(
  isDark,
  (newVal) => {
    document.documentElement.setAttribute(
      'theme-mode',
      newVal ? 'dark' : 'light'
    )
  },
  {
    immediate: true
  }
)

const columns = ref<ProTableCol[]>([
  {
    title: 'ID',
    colKey: 'id',
    width: 100,
    search: true,
    fixed: 'left'
  },
  {
    title: 'Name',
    colKey: 'name',
    width: 200,
    search: true
  },
  {
    title: 'Age',
    colKey: 'age',
    width: 100,
    search: true
  },
  {
    title: 'Address',
    colKey: 'address',
    width: 300,
    search: true
  },
  {
    title: 'Email',
    colKey: 'email',
    width: 200,
    search: true
  },
  {
    title: 'Phone',
    colKey: 'phone',
    width: 150,
    search: true
  },
  {
    title: 'Actions',
    colKey: 'operation',
    width: 150,
    fixed: 'right'
  }
])

const data = ref([
  { id: 1, name: 'John Doe', age: 30, address: '123 Main St' },
  { id: 2, name: 'Jane Smith', age: 25, address: '456 Elm St' },
  { id: 3, name: 'Alice Johnson', age: 28, address: '789 Maple Ave' },
  { id: 4, name: 'Bob Brown', age: 35, address: '101 Pine Rd' },
  { id: 5, name: 'Charlie White', age: 22, address: '202 Oak St' },
  { id: 6, name: 'Diana Green', age: 27, address: '303 Birch Blvd' },
  { id: 7, name: 'Ethan Blue', age: 32, address: '404 Cedar Ct' },
  { id: 8, name: 'Fiona Black', age: 29, address: '505 Spruce Dr' },
  { id: 9, name: 'George Gray', age: 31, address: '606 Willow Way' },
  { id: 10, name: 'Hannah Yellow', age: 26, address: '707 Cherry Ln' },
  { id: 11, name: 'Ian Purple', age: 34, address: '808 Ash St' },
  { id: 12, name: 'Julia Orange', age: 23, address: '909 Palm Pl' },
  { id: 13, name: 'Kevin Pink', age: 30, address: '1010 Magnolia Ave' },
  { id: 14, name: 'Laura Cyan', age: 28, address: '1111 Peach St' },
  { id: 15, name: 'Mike Red', age: 33, address: '1212 Maple St' },
  { id: 16, name: 'Nina Teal', age: 24, address: '1313 Cedar St' },
  { id: 17, name: 'Oscar Silver', age: 36, address: '1414 Oak St' },
  { id: 18, name: 'Paula Gold', age: 21, address: '1515 Pine St' },
  { id: 19, name: 'Quinn Bronze', age: 29, address: '1616 Birch St' },
  { id: 20, name: 'Rita Copper', age: 32, address: '1717 Spruce St' }
])
</script>

<style scoped>
.base-table-container {
  height: 700px;
}
</style>

动态字段搜索

动态字段搜索示例
ID
姓名
手机号
邮箱
状态
创建时间
操作
综合搜索
暂无数据
共 0 条数据
50 条/页
  • 1
查看代码
vue
<template>
  <div class="dynamic-fields-container">
    <TablePro :columns="columns" :request="fetchData" row-key="id">
      <template #tableHeaderCenter>
        <div>动态字段搜索示例</div>
      </template>
    </TablePro>
  </div>
</template>

<script setup lang="ts">
import type { ProTableCol } from 't-design-pro'
import { TablePro } from 't-design-pro'
import { useData } from 'vitepress'
import { ref, watch } from 'vue'

const { isDark } = useData()

watch(
  isDark,
  (newVal) => {
    document.documentElement.setAttribute(
      'theme-mode',
      newVal ? 'dark' : 'light'
    )
  },
  {
    immediate: true
  }
)

const columns = ref<ProTableCol[]>([
  {
    title: 'ID',
    colKey: 'id',
    width: 100,
    fixed: 'left'
  },
  {
    title: '姓名',
    colKey: 'name',
    width: 150
  },
  {
    title: '手机号',
    colKey: 'phone',
    width: 150
  },
  {
    title: '邮箱',
    colKey: 'email',
    width: 200
  },
  {
    title: '状态',
    colKey: 'status',
    width: 100,
    cellContentEnum: {
      '1': '启用',
      '0': '禁用'
    }
  },
  {
    title: '创建时间',
    colKey: 'createTime',
    width: 180
  },
  {
    title: '操作',
    colKey: 'operation',
    width: 120,
    fixed: 'right'
  },
  {
    title: '综合搜索',
    colKey: 'dynamicSearch',
    search: {
      defaultFieldKey: 'phone',
      dynamicFields: [
        {
          key: 'name',
          label: '姓名',
          valueType: 't-input',
          fieldProps: {
            placeholder: '请输入姓名',
            clearable: true
          }
        },
        {
          key: 'phone',
          label: '手机号',
          valueType: 't-input',
          fieldProps: {
            placeholder: '请输入手机号',
            maxlength: 11
          }
        },
        {
          key: 'email',
          label: '邮箱',
          valueType: 't-input',
          fieldProps: {
            placeholder: '请输入邮箱'
          }
        },
        {
          key: 'status',
          label: '状态',
          valueType: 't-select',
          valueEnum: {
            '1': '启用',
            '0': '禁用'
          },
          fieldProps: {
            placeholder: '请选择状态'
          }
        }
      ]
    }
  }
])

// 模拟数据
const mockData = [
  { id: 1, name: '张三', phone: '13800138000', email: 'zhangsan@example.com', status: '1', createTime: '2024-01-15 10:30:00' },
  { id: 2, name: '李四', phone: '13900139000', email: 'lisi@example.com', status: '0', createTime: '2024-01-16 14:20:00' },
  { id: 3, name: '王五', phone: '13700137000', email: 'wangwu@example.com', status: '1', createTime: '2024-01-17 09:15:00' },
  { id: 4, name: '赵六', phone: '13600136000', email: 'zhaoliu@example.com', status: '1', createTime: '2024-01-18 16:45:00' },
  { id: 5, name: '钱七', phone: '13500135000', email: 'qianqi@example.com', status: '0', createTime: '2024-01-19 11:30:00' },
  { id: 6, name: '孙八', phone: '13400134000', email: 'sunba@example.com', status: '1', createTime: '2024-01-20 13:20:00' },
  { id: 7, name: '周九', phone: '13300133000', email: 'zhoujiu@example.com', status: '1', createTime: '2024-01-21 15:10:00' },
  { id: 8, name: '吴十', phone: '13200132000', email: 'wushi@example.com', status: '0', createTime: '2024-01-22 10:00:00' }
]

const fetchData = async (params: any) => {
  // 模拟异步请求
  await new Promise(resolve => setTimeout(resolve, 500))

  const { current, pageSize, ...searchParams } = params

  // 过滤数据
  let filteredData = [...mockData]

  // 处理动态字段搜索
  Object.keys(searchParams).forEach(key => {
    const value = searchParams[key]
    if (value) {
      const [fieldKey, subKey] = key.split('.')
      if (fieldKey === 'dynamicSearch') {
        if (subKey === 'name') {
          filteredData = filteredData.filter(item => item.name.includes(value))
        } else if (subKey === 'phone') {
          filteredData = filteredData.filter(item => item.phone.includes(value))
        } else if (subKey === 'email') {
          filteredData = filteredData.filter(item => item.email.includes(value))
        } else if (subKey === 'status') {
          filteredData = filteredData.filter(item => item.status === value)
        }
      }
    }
  })

  // 分页处理
  const start = (current - 1) * pageSize
  const end = start + pageSize
  const pageData = filteredData.slice(start, end)

  return {
    data: pageData,
    total: filteredData.length
  }
}
</script>

<style scoped>
.dynamic-fields-container {
  height: 700px;
}
</style>

动态请求数据

Name
Age
Address
暂无数据
共 0 条数据
50 条/页
  • 1
查看代码
vue
<template>
  <div>
    <TablePro
      ref="tableRef"
      :columns="columns"
      row-key="id"
      :loading="loading"
      :request="request"
    >
      <template #tableHeaderLeft>
        <TButton @click="refresh">刷新</TButton>
      </template>
    </TablePro>
  </div>
</template>

<script setup lang="ts">
import { TablePro } from 't-design-pro'
import { Button as TButton } from 'tdesign-vue-next'
import { useData } from 'vitepress'
import { ref, watch } from 'vue'

import type { ProTableCol } from '@/components/Table/types'

const { isDark } = useData()

watch(
  isDark,
  (newVal) => {
    document.documentElement.setAttribute(
      'theme-mode',
      newVal ? 'dark' : 'light'
    )
  },
  {
    immediate: true
  }
)

const columns = ref<ProTableCol[]>([
  {
    title: 'Name',
    colKey: 'name',
    width: 200
  },
  {
    title: 'Age',
    colKey: 'age',
    width: 100
  },
  {
    title: 'Address',
    colKey: 'address',
    width: 300
  }
])

const data = [
  { id: 1, name: 'John Doe', age: 30, address: '123 Main St' },
  { id: 2, name: 'Jane Smith', age: 25, address: '456 Elm St' },
  { id: 3, name: 'Alice Johnson', age: 28, address: '789 Maple Ave' },
  { id: 4, name: 'Bob Brown', age: 35, address: '101 Pine Rd' }
]

const getData = (params: { name: string }) => {
  console.log('params', params)

  return new Promise<{ data: any[]; total: number }>((resolve) => {
    setTimeout(() => {
      resolve({
        data,
        total: data.length
      })
    }, 1500)
  })
}

const loading = ref(false)
const request = async (params) => {
  try {
    loading.value = true
    const result = await getData(params)

    return {
      data: result.data,
      total: result.total
    }
  } catch {
    return null
  } finally {
    loading.value = false
  }
}

const tableRef = ref<InstanceType<typeof TablePro> | null>(null)

const refresh = () => {
  tableRef.value?.refresh()
}
</script>

<style></style>

API

Props

参数说明类型默认值
request数据请求方法ProTableRequestMethod-
extendParams请求扩展参数ExtendParams-
columns表格列配置ProTableCol[][]
data静态数据Array<any>-
rowKey行数据的唯一标识字段名string'id'
cellEmptyContent空单元格显示内容string'-'
pagination分页配置PaginationProps | null{ defaultCurrent: 1, defaultPageSize: 50 }
resetType表单重置类型'initial' | 'empty''initial'
tableProps透传给 EnhancedTable 的属性ProTablePropsOmitKey{}

ProTableCol 列配置

参数说明类型默认值
colKey列标识符string-
title列标题string | TNode-
width列宽度number | string-
fixed固定列位置'left' | 'right'-
hidden是否隐藏列booleanfalse
search搜索配置ProTableColSearchType | boolean-
cellContentEnum单元格内容枚举映射Record<string, any>-
cell自定义单元格渲染TNode-

ProTableColSearchType 搜索配置

参数说明类型默认值
key搜索字段名(默认使用 colKey)string-
label搜索表单标签string-
valueType搜索组件类型SearchValueType't-input'
valueEnum选择器选项枚举Record<string, any>-
fieldProps组件属性Record<string, any>-
render自定义渲染搜索组件() => VNode-
dynamicFields动态字段配置DynamicFieldItem[]-
defaultFieldKey默认选中的动态字段keystring-

DynamicFieldItem 动态字段配置

参数说明类型默认值
key字段标识符string-
label字段显示名称string-
valueType该字段的输入类型SearchValueType't-input'
fieldProps该字段的输入属性Record<string, any>-
valueEnum该字段的枚举值Record<string, any>-

SearchValueType 搜索组件类型

支持的搜索组件类型:

  • 't-input' - 输入框
  • 't-select' - 选择器
  • 't-date-picker' - 日期选择器
  • 't-date-range-picker' - 日期范围选择器
  • VNode - 自定义组件

方法

方法名说明参数返回值
refresh刷新表格数据(不重置分页)--
reset重置表格(重置分页到第一页)--
restSearchForm重置搜索表单FormResetParams-
appendTo新增数据到指定位置(key: string, newData: any)-

插槽

插槽名说明参数
tableHeaderCenter表格头部中心区域-
tableHeaderLeft表格头部左侧区域-
tableHeaderRight表格头部右侧区域-
empty空数据状态-

事件

继承 TDesign EnhancedTable 的所有事件,主要包括:

  • change - 分页、排序、过滤变化时触发
  • select-change - 选择变化时触发
  • expand-change - 展开变化时触发

高级用法

搜索表单配置

vue
<template>
  <TablePro :columns="columns" :request="fetchData" />
</template>

<script setup>
const columns = [
  {
    title: '用户名',
    colKey: 'username',
    search: true // 简单搜索配置
  },
  {
    title: '状态',
    colKey: 'status',
    search: {
      valueType: 't-select',
      valueEnum: {
        active: '活跃',
        inactive: '非活跃'
      }
    }
  },
  {
    title: '创建时间',
    colKey: 'createTime',
    search: {
      valueType: 't-date-range-picker',
      fieldProps: {
        enableTimePicker: true
      }
    }
  }
]
</script>

自定义搜索组件

vue
<script setup>
const columns = [
  {
    title: '自定义搜索',
    colKey: 'custom',
    search: {
      render: () =>
        h(CustomComponent, {
          placeholder: '请输入关键词'
        })
    }
  }
]
</script>

动态字段搜索

支持在一个搜索项中动态切换不同的字段进行搜索,适用于需要灵活搜索的场景。

vue
<script setup>
const columns = [
  {
    title: '动态搜索',
    colKey: 'dynamicSearch',
    search: {
      dynamicFields: [
        { key: 'name', label: '姓名', valueType: 't-input' },
        { key: 'phone', label: '手机号', valueType: 't-input' },
        { key: 'email', label: '邮箱', valueType: 't-input' },
        { key: 'status', label: '状态', valueType: 't-select', valueEnum: { '1': '启用', '0': '禁用' } }
      ]
    }
  }
]

// 搜索时,参数格式为:{ 'dynamicSearch.name': '张三' }
const fetchData = async (params) => {
  // params 包含:{ 'dynamicSearch.name': '张三' } 或 { 'dynamicSearch.phone': '13800138000' }
  const response = await api.search({
    ...params
  })
  
  return {
    data: response.list,
    total: response.total
  }
}
</script>

说明

  • 左侧为字段选择器(select),右侧为对应的输入框
  • 默认选中 dynamicFields 中的第一个字段
  • 搜索时,参数格式为 { 'colKey.fieldKey': value },例如 { 'dynamicSearch.name': '张三' }
  • 切换字段时,输入框的值会自动清空

动态字段搜索(带默认值)

vue
<script setup>
const columns = [
  {
    title: '高级搜索',
    colKey: 'advancedSearch',
    search: {
      // 设置默认选中的字段
      defaultFieldKey: 'phone',
      dynamicFields: [
        {
          key: 'name',
          label: '姓名',
          valueType: 't-input',
          fieldProps: {
            placeholder: '请输入姓名',
            clearable: true
          }
        },
        {
          key: 'phone',
          label: '手机号',
          valueType: 't-input',
          fieldProps: {
            placeholder: '请输入手机号',
            maxlength: 11
          }
        },
        {
          key: 'email',
          label: '邮箱',
          valueType: 't-input',
          fieldProps: {
            placeholder: '请输入邮箱'
          }
        },
        {
          key: 'status',
          label: '状态',
          valueType: 't-select',
          valueEnum: {
            '1': '启用',
            '0': '禁用'
          },
          fieldProps: {
            placeholder: '请选择状态'
          }
        }
      ]
    }
  }
]

const fetchData = async (params) => {
  // params 包含:{ 'advancedSearch.phone': '13800138000' }
  const response = await api.search({
    ...params
  })

  return {
    data: response.list,
    total: response.total
  }
}
</script>

说明

  • 使用 defaultFieldKey 指定默认选中的字段(示例中默认选中"手机号")
  • 可以为每个字段配置独立的 fieldProps,如 placeholdermaxlength
  • 字段切换时,输入框的值会自动清空

动态字段搜索(复杂场景)

vue
<script setup>
const columns = [
  {
    title: '综合搜索',
    colKey: 'complexSearch',
    search: {
      dynamicFields: [
        {
          key: 'dateRange',
          label: '时间范围',
          valueType: 't-date-range-picker',
          fieldProps: {
            enableTimePicker: true,
            format: 'YYYY-MM-DD HH:mm:ss'
          }
        },
        {
          key: 'amount',
          label: '金额范围',
          valueType: 't-input',
          fieldProps: {
            placeholder: '请输入金额',
            type: 'number'
          }
        },
        {
          key: 'category',
          label: '分类',
          valueType: 't-select',
          valueEnum: {
            '1': '产品A',
            '2': '产品B',
            '3': '产品C'
          }
        },
        {
          key: 'region',
          label: '地区',
          valueType: 't-select',
          valueEnum: {
            'north': '华北',
            'south': '华南',
            'east': '华东',
            'west': '西南'
          }
        }
      ]
    }
  }
]

const fetchData = async (params) => {
  // 处理不同字段类型的参数
  const { current, pageSize, ...searchParams } = params

  // 示例:处理日期范围
  if (searchParams['complexSearch.dateRange']) {
    const [startTime, endTime] = searchParams['complexSearch.dateRange']
    searchParams.startTime = startTime
    searchParams.endTime = endTime
    delete searchParams['complexSearch.dateRange']
  }

  const response = await api.search({
    page: current,
    size: pageSize,
    ...searchParams
  })

  return {
    data: response.list,
    total: response.total
  }
}
</script>

说明

  • 支持多种输入类型:日期范围、数字输入、选择器等
  • 可以根据业务需求自定义参数处理逻辑
  • 适用于需要灵活组合多种搜索条件的复杂场景

数据请求方法

javascript
const fetchData = async (params) => {
  const { current, pageSize, ...searchParams } = params

  const response = await api.getTableData({
    page: current,
    size: pageSize,
    ...searchParams
  })

  return {
    data: response.list,
    total: response.total
  }
}

表格头部插槽

vue
<template>
  <TablePro :columns="columns">
    <template #tableHeaderLeft>
      <t-button theme="primary">新增</t-button>
    </template>
    <template #tableHeaderRight>
      <t-button theme="default">导出</t-button>
      <t-button theme="default">设置</t-button>
    </template>
  </TablePro>
</template>

注意事项

  1. 数据请求方法request 方法需要返回 { data: Array, total: number } 格式的数据
  2. 列标识符:每列必须设置唯一的 colKey
  3. 搜索表单:只有设置了 search 属性的列才会出现在搜索表单中
  4. 分页处理:组件会自动处理分页参数传递和页码管理
  5. 响应式布局:搜索表单会根据容器宽度自动调整列数和布局