Skip to content

表格 Hooks

DVHA Pro 提供了表格相关的 Hooks,包括数据管理和列渲染功能。

useTable - 表格数据管理

useTable 是 DVHA Pro 提供的表格数据管理 Hook,基于 useNaiveTableuseExtendList 构建,提供完整的表格功能支持,包括数据获取、分页、排序、过滤、选择、导入导出等功能。

特性

  • 自动数据获取: 基于 useExtendList 的数据获取和分页
  • 列管理: 支持列的显示/隐藏控制
  • 行选择: 支持单选和多选功能
  • 排序过滤: 支持表格排序和过滤
  • 导入导出: 内置导入导出功能和进度提示
  • 国际化支持: 内置多语言提示信息

基础用法

导入

js
import { useTable } from '@duxweb/dvha-pro'

基本使用

typescript
const table = useTable({
  path: '/api/users',
  columns: [
    { title: 'ID', key: 'id', width: 80 },
    { title: '用户名', key: 'username' },
    { title: '邮箱', key: 'email' },
    { title: '状态', key: 'status' }
  ]
})

// 获取表格数据和状态
const {
  list, // 表格数据
  isLoading, // 加载状态
  table, // 表格属性
  tablePagination // 分页属性
} = table

配置选项

UseTableProps

继承自 UseExtendListProps,并扩展了表格特有的属性:

typescript
interface UseTableProps extends Omit<UseExtendListProps, 'key'> {
  key?: TableColumnKey
  columns: TableColumn[]
}

列配置 (columns)

表格列配置,支持 Naive UI DataTable 的所有列类型:

typescript
const columns = [
  {
    title: 'ID',
    key: 'id',
    width: 80,
    show: true, // 控制列显示/隐藏
    sorter: true // 启用排序
  },
  {
    title: '用户名',
    key: 'username',
    render: row => h('span', { class: 'font-semibold' }, row.username)
  },
  {
    title: '状态',
    key: 'status',
    filterOptions: [
      { label: '启用', value: 'active' },
      { label: '禁用', value: 'inactive' }
    ],
    filter: true
  }
]

完整示例

基础表格示例

vue
<script setup>
import { useTable } from '@duxweb/dvha-pro'
import { NDataTable, NTag } from 'naive-ui'
import { h } from 'vue'

const table = useTable({
  path: '/api/users',
  columns: [
    {
      title: 'ID',
      key: 'id',
      width: 80,
      sorter: true
    },
    {
      title: '用户名',
      key: 'username',
      sorter: true
    },
    {
      title: '邮箱',
      key: 'email'
    },
    {
      title: '状态',
      key: 'status',
      render: row => h(NTag, {
        type: row.status === 'active' ? 'success' : 'error'
      }, row.status === 'active' ? '启用' : '禁用'),
      filterOptions: [
        { label: '启用', value: 'active' },
        { label: '禁用', value: 'inactive' }
      ],
      filter: true
    }
  ],
  filters: {
    // 默认过滤条件
    status: 'active'
  }
})

const { table: tableProps, tablePagination } = table
</script>

<template>
  <div>
    <NDataTable
      v-bind="tableProps"
      :pagination="tablePagination"
    />
  </div>
</template>

带列管理的表格

vue
<script setup>
import { useTable } from '@duxweb/dvha-pro'
import { NButton, NDataTable, NPopselect } from 'naive-ui'

const table = useTable({
  path: '/api/users',
  columns: [
    { title: 'ID', key: 'id', width: 80 },
    { title: '用户名', key: 'username' },
    { title: '邮箱', key: 'email' },
    { title: '手机号', key: 'phone' },
    { title: '部门', key: 'department' },
    { title: '创建时间', key: 'created_at' }
  ]
})

const {
  table: tableProps,
  tablePagination,
  columnSelected,
  onUpdateColumnSelected,
  columns
} = table

// 列选择选项
const columnOptions = computed(() => {
  return columns.value
    .filter(col => 'title' in col && 'key' in col)
    .map(col => ({
      label: col.title,
      value: col.key
    }))
})
</script>

<template>
  <div>
    <!-- 列管理 -->
    <div class="mb-4 flex justify-end">
      <NPopselect
        v-model:value="columnSelected"
        :options="columnOptions"
        multiple
        @update:value="onUpdateColumnSelected"
      >
        <NButton>
          <template #icon>
            <i class="i-tabler:columns" />
          </template>
          列管理
        </NButton>
      </NPopselect>
    </div>

    <!-- 表格 -->
    <NDataTable
      v-bind="tableProps"
      :pagination="tablePagination"
    />
  </div>
</template>

带导入导出的表格

vue
<script setup>
import { useTable } from '@duxweb/dvha-pro'
import { NButton, NDataTable, NSpace } from 'naive-ui'

const table = useTable({
  path: '/api/users',
  columns: [
    { title: 'ID', key: 'id' },
    { title: '用户名', key: 'username' },
    { title: '邮箱', key: 'email' }
  ]
})

const {
  table: tableProps,
  tablePagination,
  isExporting,
  isImporting,
  onExport,
  onImport
} = table

// 导出数据
function handleExport() {
  onExport()
}

// 导入数据
function handleImport(file) {
  onImport(file)
}
</script>

<template>
  <div>
    <!-- 工具栏 -->
    <div class="mb-4">
      <NSpace>
        <NButton
          :loading="isExporting"
          @click="handleExport"
        >
          <template #icon>
            <i class="i-tabler:download" />
          </template>
          导出数据
        </NButton>

        <NButton
          :loading="isImporting"
          @click="handleImport"
        >
          <template #icon>
            <i class="i-tabler:upload" />
          </template>
          导入数据
        </NButton>
      </NSpace>
    </div>

    <!-- 表格 -->
    <NDataTable
      v-bind="tableProps"
      :pagination="tablePagination"
    />
  </div>
</template>

高级用法

自定义过滤器

typescript
const table = useTable({
  path: '/api/users',
  columns: [...],
  filters: {
    // 默认过滤条件
    status: 'active',
    department: 'tech'
  },
  // 自定义过滤器处理
  onUpdateFilters: (filters) => {
    console.log('过滤器更新:', filters)
  }
})

自定义排序

typescript
const table = useTable({
  path: '/api/users',
  columns: [...],
  sorters: {
    // 默认排序
    created_at: 'desc'
  },
  // 自定义排序处理
  onUpdateSorters: (sorters) => {
    console.log('排序更新:', sorters)
  }
})

行选择处理

typescript
const table = useTable({
  path: '/api/users',
  columns: [...],
  // 启用行选择
  checkable: true
})

const { checkeds, onUpdateCheckeds } = table

// 监听选择变化
watch(checkeds, (selectedKeys) => {
  console.log('选中的行:', selectedKeys)
})

// 批量操作
function batchDelete() {
  const selectedIds = checkeds.value
  if (selectedIds.length === 0) {
    message.warning('请选择要删除的数据')
    return
  }
  // 执行批量删除
}

API 参考

UseTableProps

继承 UseExtendListProps 的所有属性,并添加:

属性类型默认值说明
keyTableColumnKey-行键字段
columnsTableColumn[][]表格列配置

UseTableResult

继承 useExtendList 的所有返回值,并添加:

字段类型说明
tablePaginationComputedRef<PaginationProps>分页属性
tableComputedRef<DataTableProps>表格属性
columnsRef<TableColumn[]>表格列配置
columnSelectedComputedRef<string[]>选中的列
onUpdateColumnSelected(v: string[]) => void更新列选择

继承的 useExtendList 属性

字段类型说明
listRef<any[]>数据列表
metaRef<any>元数据信息
isLoadingRef<boolean>加载状态
pageRef<number>当前页码
pageSizeRef<number>每页大小
pageCountRef<number>总页数
checkedsRef<any[]>选中的数据
isExportingRef<boolean>导出状态
isImportingRef<boolean>导入状态
onUpdatePageFunction更新页码
onUpdatePageSizeFunction更新每页大小
onExportFunction导出数据
onImportFunction导入数据
refetchFunction重新获取数据

导入导出功能

导出进度提示

Hook 内置了导出进度提示功能:

  • 导出开始: 显示圆形进度条和页面计数
  • 导出成功: 显示成功通知和导出数量
  • 导出失败: 显示错误信息

导入进度提示

Hook 内置了导入进度提示功能:

  • 导入开始: 显示圆形进度条和处理进度
  • 导入成功: 显示成功通知和处理数量
  • 导入失败: 显示错误信息

自定义导入导出

typescript
const table = useTable({
  path: '/api/users',
  columns: [...],
  // 自定义导出成功回调
  onExportSuccess: (data) => {
    console.log('导出成功:', data)
  },
  // 自定义导出错误回调
  onExportError: (error) => {
    console.error('导出失败:', error)
  },
  // 自定义导入成功回调
  onImportSuccess: (progress) => {
    console.log('导入成功:', progress)
  },
  // 自定义导入错误回调
  onImportError: (error) => {
    console.error('导入失败:', error)
  }
})

最佳实践

1. 合理设计列结构

typescript
// ✅ 推荐:合理设置列宽和属性
const columns = [
  { title: 'ID', key: 'id', width: 80 }, // 固定宽度
  { title: '用户名', key: 'username', minWidth: 120 }, // 最小宽度
  { title: '邮箱', key: 'email', ellipsis: true }, // 文本省略
  { title: '操作', key: 'actions', width: 150 } // 操作列固定宽度
]

2. 使用列管理功能

typescript
// ✅ 推荐:为用户提供列管理功能
const { columnSelected, onUpdateColumnSelected } = table

// 在模板中提供列选择器
<NPopselect
  v-model:value="columnSelected"
  :options="columnOptions"
  multiple
  @update:value="onUpdateColumnSelected"
>
  列管理
</NPopselect>

3. 合理使用过滤和排序

typescript
// ✅ 推荐:设置合理的默认过滤和排序
const table = useTable({
  filters: {
    status: 'active' // 默认只显示启用的数据
  },
  sorters: {
    created_at: 'desc' // 默认按创建时间倒序
  }
})

4. 处理大数据量

typescript
// ✅ 推荐:设置合适的分页大小
const table = useTable({
  pagination: {
    pageSize: 20, // 适中的分页大小
    pageSizes: [10, 20, 50, 100]
  }
})

常见问题

Q: 如何自定义表格的加载状态?

A: 可以通过 isLoading 状态自定义加载显示:

vue
<template>
  <div>
    <div v-if="isLoading" class="text-center p-4">
      自定义加载中...
    </div>
    <NDataTable v-else v-bind="tableProps" />
  </div>
</template>

Q: 如何处理表格的错误状态?

A: 可以监听 useExtendList 的错误状态:

typescript
const { error, isError } = table

watch(isError, (hasError) => {
  if (hasError) {
    message.error('数据加载失败')
  }
})

Q: 如何实现表格数据的实时更新?

A: 可以使用 refetch 方法或设置轮询:

typescript
// 手动刷新
const { refetch } = table
refetch()

// 设置轮询
const table = useTable({
  polling: true,
  pollingInterval: 5000
})

Q: 如何自定义导入导出的文件格式?

A: 可以通过回调函数处理:

typescript
const table = useTable({
  onExportSuccess: (data) => {
    // 自定义导出数据处理
    const csvData = convertToCSV(data)
    downloadFile(csvData, 'users.csv')
  }
})

useTableColumn - 表格列渲染器

useTableColumn 是 DVHA Pro 提供的表格列渲染器 Hook,提供了8种预设的列渲染功能,用于简化表格列的显示和交互逻辑。

特性

  • 8种渲染器: 提供媒体、图片、开关、状态、映射、输入、复制、隐藏等渲染器
  • 统一管理: 集中管理所有列渲染器的配置和路径
  • 类型安全: 完整的 TypeScript 类型定义
  • 业务集成: 内置与后端 API 的交互逻辑

基础用法

导入

typescript
import { useTableColumn } from '@duxweb/dvha-pro'

基本使用

typescript
const column = useTableColumn({
  path: '/api/users', // API 路径
  rowKey: 'id' // 行键字段
})

const {
  renderMedia, // 媒体渲染器
  renderImage, // 图片渲染器
  renderSwitch, // 开关渲染器
  renderStatus, // 状态渲染器
  renderMap, // 映射渲染器
  renderInput, // 输入渲染器
  renderCopy, // 复制渲染器
  renderHidden // 隐藏渲染器
} = column

渲染器详解

renderMedia - 媒体渲染器

用于渲染媒体信息(头像、标题、描述):

typescript
const columns = [
  {
    title: '用户信息',
    key: 'user',
    render: renderMedia({
      title: 'name', // 标题字段
      desc: 'email', // 描述字段
      image: 'avatar', // 图片字段
      avatar: true, // 是否显示为头像
      imageWidth: 40, // 图片宽度
      imageHeight: 40 // 图片高度
    })
  }
]

renderImage - 图片渲染器

用于渲染图片列:

typescript
const columns = [
  {
    title: '头像',
    key: 'avatar',
    render: renderImage({
      key: 'avatar', // 图片字段
      width: 50, // 图片宽度
      height: 50, // 图片高度
      preview: true // 是否支持预览
    })
  }
]

renderSwitch - 开关渲染器

用于渲染可切换的开关控件:

typescript
const columns = [
  {
    title: '状态',
    key: 'status',
    render: renderSwitch({
      key: 'status', // 状态字段
      onChange: (value, rowData) => {
        console.log('状态切换:', value, rowData)
      }
    })
  }
]

renderStatus - 状态渲染器

用于渲染状态标签:

typescript
const columns = [
  {
    title: '状态',
    key: 'status',
    render: renderStatus({
      map: {
        active: { label: '启用', type: 'success' },
        inactive: { label: '禁用', type: 'error' },
        pending: { label: '待审核', type: 'warning' }
      }
    })
  }
]

renderMap - 映射渲染器

用于渲染多个映射字段:

typescript
const columns = [
  {
    title: '详细信息',
    key: 'details',
    render: renderMap({
      maps: [
        {
          key: 'email',
          label: '邮箱',
          icon: 'i-tabler:mail'
        },
        {
          key: 'phone',
          label: '电话',
          icon: 'i-tabler:phone'
        },
        {
          key: 'address',
          label: '地址',
          icon: 'i-tabler:map-pin',
          render: value => value || '未填写'
        }
      ]
    })
  }
]

renderInput - 输入渲染器

用于渲染可编辑的输入框:

typescript
const columns = [
  {
    title: '备注',
    key: 'remark',
    render: renderInput({
      key: 'remark', // 字段名
      tag: NInput, // 输入组件
      onChange: (value, rowData) => {
        // 处理输入变化
      }
    })
  }
]

renderCopy - 复制渲染器

用于渲染带复制功能的文本:

typescript
const columns = [
  {
    title: 'ID',
    key: 'id',
    render: renderCopy({
      key: 'id', // 复制的字段
      format: value => `#${value}` // 格式化显示
    })
  }
]

renderHidden - 隐藏渲染器

用于渲染可切换显示的隐藏文本:

typescript
const columns = [
  {
    title: '密码',
    key: 'password',
    render: renderHidden({
      key: 'password', // 隐藏的字段
      percent: 30 // 显示的字符百分比
    })
  }
]

完整示例

用户管理表格

vue
<script setup>
import { useTable, useTableColumn } from '@duxweb/dvha-pro'
import { NDataTable } from 'naive-ui'

const column = useTableColumn({
  path: '/api/users',
  rowKey: 'id'
})

const {
  renderMedia,
  renderSwitch,
  renderStatus,
  renderMap,
  renderCopy,
  renderHidden
} = column

const table = useTable({
  path: '/api/users',
  columns: [
    {
      title: 'ID',
      key: 'id',
      width: 80,
      render: renderCopy({ key: 'id' })
    },
    {
      title: '用户信息',
      key: 'user',
      render: renderMedia({
        title: 'name',
        desc: 'email',
        image: 'avatar',
        avatar: true
      })
    },
    {
      title: '联系方式',
      key: 'contact',
      render: renderMap({
        maps: [
          {
            key: 'phone',
            label: '电话',
            icon: 'i-tabler:phone'
          },
          {
            key: 'address',
            label: '地址',
            icon: 'i-tabler:map-pin'
          }
        ]
      })
    },
    {
      title: '状态',
      key: 'status',
      render: renderStatus({
        map: {
          active: { label: '启用', type: 'success' },
          inactive: { label: '禁用', type: 'error' }
        }
      })
    },
    {
      title: '启用',
      key: 'enabled',
      render: renderSwitch({ key: 'enabled' })
    },
    {
      title: '密码',
      key: 'password',
      render: renderHidden({
        key: 'password',
        percent: 20
      })
    }
  ]
})

const { table: tableProps, tablePagination } = table
</script>

<template>
  <NDataTable
    v-bind="tableProps"
    :pagination="tablePagination"
  />
</template>

商品管理表格

vue
<script setup>
import { useTable, useTableColumn } from '@duxweb/dvha-pro'
import { NDataTable, NInputNumber } from 'naive-ui'

const column = useTableColumn({
  path: '/api/products',
  rowKey: 'id'
})

const {
  renderImage,
  renderInput,
  renderStatus,
  renderSwitch
} = column

const table = useTable({
  path: '/api/products',
  columns: [
    {
      title: '商品图片',
      key: 'image',
      width: 80,
      render: renderImage({
        key: 'image',
        width: 60,
        height: 60,
        preview: true
      })
    },
    {
      title: '商品名称',
      key: 'name'
    },
    {
      title: '价格',
      key: 'price',
      render: renderInput({
        key: 'price',
        tag: NInputNumber,
        precision: 2,
        min: 0
      })
    },
    {
      title: '状态',
      key: 'status',
      render: renderStatus({
        map: {
          published: { label: '已发布', type: 'success' },
          draft: { label: '草稿', type: 'warning' },
          archived: { label: '已归档', type: 'error' }
        }
      })
    },
    {
      title: '上架',
      key: 'is_active',
      render: renderSwitch({ key: 'is_active' })
    }
  ]
})

const { table: tableProps, tablePagination } = table
</script>

<template>
  <NDataTable
    v-bind="tableProps"
    :pagination="tablePagination"
  />
</template>

API 参考

UseTableColumnProps

属性类型默认值说明
rowKeystring'id'行键字段
pathstring((rowData: Record<string, any>) => string)-

UseTableColumnResult

方法类型说明
renderMediaFunction媒体渲染器
renderImageFunction图片渲染器
renderSwitchFunction开关渲染器
renderStatusFunction状态渲染器
renderMapFunction映射渲染器
renderInputFunction输入渲染器
renderCopyFunction复制渲染器
renderHiddenFunction隐藏渲染器

最佳实践

1. 合理选择渲染器

typescript
// ✅ 推荐:根据数据类型选择合适的渲染器
const columns = [
  // 用户信息使用媒体渲染器
  { title: '用户', render: renderMedia({ title: 'name', image: 'avatar' }) },

  // 状态字段使用状态渲染器
  { title: '状态', render: renderStatus({ map: statusMap }) },

  // 开关字段使用开关渲染器
  { title: '启用', render: renderSwitch({ key: 'enabled' }) },

  // 敏感信息使用隐藏渲染器
  { title: '密码', render: renderHidden({ key: 'password' }) }
]

2. 统一配置管理

typescript
// ✅ 推荐:统一配置渲染器
const column = useTableColumn({
  path: '/api/users',
  rowKey: 'id'
})

// 复用渲染器配置
const statusRenderer = renderStatus({
  map: {
    active: { label: '启用', type: 'success' },
    inactive: { label: '禁用', type: 'error' }
  }
})

3. 处理数据更新

typescript
// ✅ 推荐:正确处理数据更新
const switchRenderer = renderSwitch({
  key: 'enabled',
  onChange: async (value, rowData) => {
    try {
      await updateUserStatus(rowData.id, value)
      message.success('状态更新成功')
      // 刷新表格数据
      table.refetch()
    }
    catch (error) {
      message.error('状态更新失败')
      // 恢复原状态
      rowData.enabled = !value
    }
  }
})

常见问题

Q: 如何自定义渲染器的样式?

A: 可以通过 CSS 类或内联样式自定义:

typescript
const customRenderer = renderStatus({
  map: {
    active: {
      label: '启用',
      type: 'success',
      class: 'custom-status-active'
    }
  }
})

Q: 如何处理渲染器的异步操作?

A: 在回调函数中使用 async/await:

typescript
const switchRenderer = renderSwitch({
  key: 'enabled',
  onChange: async (value, rowData) => {
    const loading = message.loading('更新中...', 0)
    try {
      await api.updateStatus(rowData.id, value)
      message.success('更新成功')
    }
    catch (error) {
      message.error('更新失败')
      rowData.enabled = !value // 恢复状态
    }
    finally {
      loading.destroy()
    }
  }
})

Q: 如何扩展自定义渲染器?

A: 可以创建自定义的渲染函数:

typescript
function useCustomRenderer() {
  const renderCustom = (props) => {
    return (rowData, rowIndex) => {
      return h('div', {
        class: 'custom-renderer'
      }, props.format(rowData[props.key]))
    }
  }

  return { renderCustom }
}