Skip to content

BaseTable 表格组件

来源:@jhlc/common-core 远程组件

📝 作者
朱祥
谢飞
朱祥高级开发工程师谢飞高级开发工程师
👤 工号:025877 · 026789研发部

BaseTable 是一个功能强大的表格组件,支持数据展示、排序、筛选、选择、操作按钮、可编辑单元格等功能。支持 DataTable 和 AGGrid 两种渲染模式。

📦 导入方式

typescript
// 全局注册(已在项目中配置)
// 直接使用 <BaseTable /> 即可

// 类型导入
import type { TableColumnDesc } from "@jhlc/common-core/src/components/table/base-table/type";

🚀 基本用法

vue
<template>
  <BaseTable
    ref="tableRef"
    :data="list"
    :columns="columns"
    showToolbar
    border
  />
</template>

<script setup lang="ts">
import { ref, computed } from "vue";

const tableRef = ref();
const list = ref([
  { id: 1, name: "张三", age: 25, status: "在职" },
  { id: 2, name: "李四", age: 30, status: "离职" },
]);

const columns = computed(() => [
  { type: "selection" },
  { type: "index" },
  { label: "姓名", name: "name" },
  { label: "年龄", name: "age" },
  { label: "状态", name: "status" },
]);
</script>

📋 Props 属性

基础属性

属性名类型默认值说明
dataArray[]表格数据源
columnsTableColumnDesc[][]列配置数组
renderType'' | 'dataTable' | 'agGrid'''渲染类型
rowIdstring'_id'行数据唯一标识字段
rowKeystring'_id'行数据的键(树形表格必需)
childrenKeystring'children'子节点字段名

外观属性

属性名类型默认值说明
borderbooleantrue是否显示边框
stripebooleantrue是否显示斑马纹
size'small' | 'default' | 'large'-表格尺寸
heightnumber-表格高度
maxHeightnumber-表格最大高度
autoHeightbooleanfalse自动调整高度
showHeaderbooleantrue是否显示表头
showToolbarbooleanfalse是否显示工具栏
fixedboolean-是否固定表头

选择相关

属性名类型默认值说明
selectMethod'dblclick' | 'click' | ''''选中方式
selectedRowstring | number-单选选中行
selectedValuestring | number-单选选中值
selectionRowsArray[]多选选中的行
selectionTypenumber2多选类型 2:全选 1:半选和全选
persistSelectbooleanfalse持久化选择
autoCheckedChildrenbooleantrue自动勾选子节点
highlightCurrentRowbooleanfalse高亮当前行
currentRowSelectMethod'dblclick' | 'click'-当前行选中方式

功能相关

属性名类型默认值说明
disabledbooleanfalse禁用输入框
showSummarybooleanfalse显示汇总行
summaryMethodFunction-自定义汇总方法
defaultExpandAllbooleanfalse默认展开所有行
virtualbooleantrue虚拟滚动
frontSortbooleanfalse前端排序
frontFilteringbooleanfalse前端过滤
manualFilteringbooleanfalse手动过滤
manualSortingbooleanfalse手动排序
groupingbooleanfalse分组
groupFieldsstring[]-分组字段
columnVisibilitybooleanfalse列可见性控制
showEditIconbooleantrue显示编辑图标

AGGrid 专用

属性名类型默认值说明
treebooleanfalse树形结构
quickEditCursorbooleantrue快速编辑光标
selectionArray-选中项
isRowSelectableFunction-行是否可选
isGroupOpenByDefaultFunction-分组默认展开
rowGroupPanelShow'never' | 'onlyWhenGrouping' | 'always''onlyWhenGrouping'行分组面板显示
grandTotalRowstring-总计行位置
pinnedTopRowDataArray-固定顶部行数据
pinnedBottomRowDataArray-固定底部行数据
isRowPinnedFunction-行是否固定
suppressRowClickSelectionbooleanfalse禁止行点击选择
rowClassFunction-行样式类
rowStyleFunction-行样式
rowClassRulesObject-行样式规则
stopEditingWhenCellsLoseFocusbooleanfalse失焦停止编辑
isRowValidDropPositionFunction-行是否为有效拖放位置

📋 Events 事件

事件名参数说明
selectionChangeselection: any[]多选项发生变化
selectselection, row, status, params选择某一行
selectAllselection, status全选/取消全选
currentChangecurrentRow当前行变化
row-clickrow, column, event行点击事件
row-dblclickrow, column, event行双击事件
expand-changerow, expanded展开/收起变化
filterModified-过滤条件修改
filterChanged-过滤条件变化
update:selectedRowrow单选行更新
update:selectedValuevalue单选值更新
mounted{ parentEl }组件挂载完成

📋 Expose 方法 (通过 ref 调用)

选择相关

方法名参数返回值说明
getSelectioncheck?: 1|2, firstLevel?: booleanany[]获取选中行 (2:全选,1:半选+全选)
getVisibleSelectioncheck?: 1|2, firstLevel?: booleanany[]获取可见选中行
getRootSelectioncheck?: 1|2any[]获取根节点选中行
getAllSelectioncheck?: 1|2any[]获取所有选中行
getTreeSelectiontype?: 'flat'|'tree'any[]获取树形选中行
getSingleSelectRow-object获取单选行
getSelectionRows-any[]获取选中的行
toggleRowSelectionrow, selected: boolean-切换行选中状态
toggleAllSelection--切换全选状态
setCurrentRowrow-设置当前高亮行
clearSelection--清空选中
setSelectrow, status: boolean-设置行选中状态
setSelection--设置选中
selectAll--全选
deselectAll--取消全选

验证相关

方法名参数返回值说明
validatecallback: (valid: boolean) => void-验证整个表格
validateRow--验证单行
validateRowsrows, cb: (valid: boolean) => void-验证多行
clearValidation--清除验证
clearRowValidation--清除行验证

数据操作

方法名参数返回值说明
setRowValuerow, name, index, value-设置行字段值
setDataValuerow, key, value-设置数据值
updateRowdata-更新行数据
getTableData-any[]获取表格数据
getParentRowrowobject获取父行
isTableReadyrowboolean表格是否就绪

编辑状态

方法名参数返回值说明
resetRowEditState--重置行编辑状态
resetEditState--重置编辑状态

刷新相关

方法名参数返回值说明
refresh--刷新表格
forceRefresh--强制刷新
forceRefreshTable--强制刷新表格
forceRefreshNodenode-强制刷新节点
refreshColumnVisible--刷新列可见性
refreshHeader--刷新表头

展开/折叠

方法名参数返回值说明
expandAll--展开所有行
collapseAll--折叠所有行
expandRowrowList-展开指定行
collRowrow-折叠指定行

滚动/定位

方法名参数返回值说明
goToRowindex: number-滚动到指定行
goToColumnname: string-滚动到指定列

导出

方法名参数返回值说明
exportExceloption: ExportTableExcelOption-导出 Excel

其他

方法名参数返回值说明
loadingtime?: number-显示加载状态(默认6000ms)
closeLoading--关闭加载状态
clearFilters--清除过滤
setSuppressRowDragflag: boolean-设置是否可拖拽
getRowKey-string获取行键
currentRow-object当前高亮行 (computed)
data-any[]表格数据 (computed)

📋 列配置 TableColumnDesc

基础属性

typescript
interface TableColumnDesc<T = any> {
  // 列名(字段名)⚠️ 重要:使用 name 而非 prop
  name?: string;
  // 列标题
  label?: string;
  // 列类型
  type?: "index" | "selection" | "radio" | "expand";
  // 列宽
  width?: number;
  // 最小列宽
  minWidth?: number;
  // 弹性宽度
  flex?: number;
  // 对齐方式
  align?: "left" | "center" | "right";
  // 表头对齐
  headerAlign?: "left" | "center" | "right";
  // 固定列
  fixed?: "left" | "right";
  // 列宽可调整
  resizable?: boolean;
  // 是否可排序
  sortable?: boolean;
  // 超出省略号
  showOverflowTooltip?: boolean;
  // 允许换行
  wrapText?: boolean;
  // 自动调整行高
  autoHeight?: boolean;
  // 是否可过滤
  filterable?: boolean;
}

自定义渲染 (⚠️ 重要)

BaseTable 支持多种自定义渲染方式:

1. defaultSlot - 默认插槽 (推荐)

typescript
import { h } from "vue";

{
  name: "title",
  label: "标题",
  // ✅ 正确方式:使用 defaultSlot + h 函数
  defaultSlot: ({ row, $index }) => {
    return h(
      "span",
      { style: "color: #409eff; cursor: pointer;" },
      row.title
    );
  }
}

2. defaultNode - 低代码插槽

typescript
{
  name: "status",
  label: "状态",
  defaultNode: ({ row, $index, params }) => {
    return {
      tag: "el-tag",
      props: { type: row.status === 1 ? "success" : "danger" },
      children: row.status === 1 ? "启用" : "禁用"
    };
  }
}

3. formatter - 文本格式化

typescript
{
  name: "amount",
  label: "金额",
  // ⚠️ formatter 返回纯文本,不支持 HTML
  formatter: (row, params) => {
    return `¥${Number(row.amount).toLocaleString()}`;
  }
}

4. headerSlot - 表头插槽

typescript
import { h } from "vue";

{
  name: "price",
  label: "价格",
  headerSlot: () => {
    return h("span", { class: "custom-header" }, [
      h("span", "价格"),
      h("el-tooltip", { content: "含税价格" }, h("i", { class: "el-icon-info" }))
    ]);
  }
}

可编辑列

typescript
{
  name: "quantity",
  label: "数量",
  // 开启编辑
  editable: true,
  // 或条件编辑
  editable: (row) => row.status !== "locked",
  // 单击编辑
  singleClickEdit: true,
  // 始终显示编辑框
  alwaysEditable: true,
  // 自动聚焦
  autoFocusInput: true,
  // 校验规则
  rules: [{ required: true, message: "请输入数量" }],
  required: true,
  // 生效校验规则
  effectRule: (row) => row.needValidate,
  // 自定义编辑组件
  editComponent: (row, value, params) => ({
    tag: "el-input-number",
    props: { min: 0, max: 100 }
  })
}

操作列

typescript
{
  label: "操作",
  width: 200,
  fixed: "right",
  operations: [
    {
      name: "edit",
      label: "编辑",
      type: "primary",
      // 权限控制
      permission: ["user:edit"],
      // 点击事件
      onClick: (row, index, params) => {
        console.log("编辑", row);
      }
    },
    {
      name: "delete",
      label: "删除",
      type: "danger",
      // 禁用条件
      disabled: (row) => row.status === "locked",
      // 显示条件
      show: (row, index) => row.canDelete,
      onClick: (row, index) => {
        console.log("删除", row);
      }
    }
  ]
}

逻辑数据类型

typescript
{
  name: "status",
  label: "状态",
  // 自动从字典获取显示值
  logicType: "ORDER_STATUS",
  logicValue: "status"
}

行合并配置

BaseTable 支持在列配置中直接定义行合并规则,无需手动编写 span-method

方式一:使用 spanOnceRow 自动合并相同值

typescript
{
  name: "category",
  label: "类别",
  width: 120,
  // 自动合并相邻相同值的单元格
  spanOnceRow: true
}

方式二:使用 span 函数自定义合并逻辑(推荐)

typescript
{
  name: "testCategory",
  label: "试验种类",
  width: 150,
  align: "center",
  // 自定义行合并逻辑
  span: ({ rowIndex, data }: any) => {
    if (!data || data.length === 0) {
      return { rowSpan: 1, colSpan: 1 };
    }

    const currentCategory = data[rowIndex]?.testCategory;
    const prevCategory = rowIndex > 0 ? data[rowIndex - 1]?.testCategory : null;

    // 如果是分组的第一行,计算需要合并的行数
    if (rowIndex === 0 || currentCategory !== prevCategory) {
      let rowSpan = 1;
      for (let i = rowIndex + 1; i < data.length; i++) {
        if (data[i].testCategory === currentCategory) {
          rowSpan++;
        } else {
          break;
        }
      }
      return { rowSpan, colSpan: 1 };
    }

    // 非第一行则隐藏
    return { rowSpan: 0, colSpan: 0 };
  }
}

完整示例:分组合并表格

vue
<template>
  <BaseTable :data="experimentData" :columns="columns" border />
</template>

<script setup lang="ts">
import { ref, computed } from "vue";
import { h } from "vue";

const experimentData = ref([
  { id: 1, testCategory: "化学成分", testItem: "C", value: "0.15" },
  { id: 2, testCategory: "化学成分", testItem: "Si", value: "0.30" },
  { id: 3, testCategory: "化学成分", testItem: "Mn", value: "1.20" },
  { id: 4, testCategory: "力学性能", testItem: "抗拉强度", value: "≥500" },
  { id: 5, testCategory: "力学性能", testItem: "屈服强度", value: "≥350" },
]);

const columns = computed(() => [
  {
    type: "index",
    label: "序号",
    width: 60,
    // 序号列也需要合并
    span: ({ rowIndex, data }: any) => {
      if (!data || data.length === 0) return { rowSpan: 1, colSpan: 1 };

      const currentCategory = data[rowIndex]?.testCategory;
      const prevCategory =
        rowIndex > 0 ? data[rowIndex - 1]?.testCategory : null;

      if (rowIndex === 0 || currentCategory !== prevCategory) {
        let rowSpan = 1;
        for (let i = rowIndex + 1; i < data.length; i++) {
          if (data[i].testCategory === currentCategory) {
            rowSpan++;
          } else {
            break;
          }
        }
        return { rowSpan, colSpan: 1 };
      }
      return { rowSpan: 0, colSpan: 0 };
    },
  },
  {
    name: "testCategory",
    label: "试验种类",
    width: 120,
    align: "center",
    span: ({ rowIndex, data }: any) => {
      if (!data || data.length === 0) return { rowSpan: 1, colSpan: 1 };

      const currentCategory = data[rowIndex]?.testCategory;
      const prevCategory =
        rowIndex > 0 ? data[rowIndex - 1]?.testCategory : null;

      if (rowIndex === 0 || currentCategory !== prevCategory) {
        let rowSpan = 1;
        for (let i = rowIndex + 1; i < data.length; i++) {
          if (data[i].testCategory === currentCategory) {
            rowSpan++;
          } else {
            break;
          }
        }
        return { rowSpan, colSpan: 1 };
      }
      return { rowSpan: 0, colSpan: 0 };
    },
  },
  {
    name: "testItem",
    label: "试验项目",
    width: 150,
  },
  {
    name: "value",
    label: "标准值",
    width: 100,
  },
]);
</script>

span 函数参数说明

typescript
interface SpanParams {
  rowIndex: number; // 当前行索引
  columnIndex: number; // 当前列索引
  row: any; // 当前行数据
  column: any; // 当前列配置
  data: any[]; // 全部表格数据
}

// 返回值
interface SpanResult {
  rowSpan: number; // 行合并数量,0 表示隐藏
  colSpan: number; // 列合并数量,0 表示隐藏
}

行合并最佳实践

  1. 使用 spanOnceRow: true - 适用于简单的相邻相同值合并
  2. 使用 span 函数 - 适用于复杂的分组合并逻辑
  3. 多列合并 - 需要合并的每一列都要配置 span 函数
  4. 注意性能 - span 函数会在每次渲染时调用,避免复杂计算
  5. 数据排序 - 确保数据按分组字段排序,否则合并效果不正确

样式控制

typescript
{
  name: "amount",
  label: "金额",
  // 单元格样式类
  cellClass: (params) => {
    return params.value < 0 ? "negative-amount" : "";
  },
  // 单元格样式
  cellStyle: (params) => {
    return { color: params.value < 0 ? "red" : "green" };
  },
  // 链接样式
  isLink: (row) => true,
  // 单元格点击
  onCellClick: (params) => {
    console.log("点击", params.data);
  },
  onCellDblclick: (params) => {
    console.log("双击", params.data);
  }
}

分组与汇总

typescript
{
  name: "amount",
  label: "金额",
  // 聚合函数
  aggregationFn: ["sum", "avg"],
  // 行分组
  rowGroup: true
}

数字精度

typescript
{
  name: "price",
  label: "单价",
  logicType: "number",
  // 保留2位小数
  precision: 2,
  // 不足自动补0
  precisionAutoFillZero: true
}

🎯 操作按钮配置 TableRowOperation

typescript
interface TableRowOperation {
  // 按钮标识
  name?: string;
  // 按钮文本
  label: string;
  // 主题颜色
  type?: "primary" | "success" | "warning" | "danger" | "info";
  // 图标
  icon?: string;
  // 自动加载状态
  autoLoading?: boolean;
  // 禁用
  disabled?: boolean | ((row) => boolean);
  // 显示条件
  show?: (row, index) => boolean;
  // 点击事件
  onClick?: (row, index, params) => void;
  // 按钮权限
  permission?: string[];
}

💡 完整示例

基础列表页

vue
<template>
  <div class="app-container">
    <BaseTable
      ref="tableRef"
      :data="list"
      :columns="columns"
      :loading="loading"
      showToolbar
      border
      @selectionChange="handleSelectionChange"
    />

    <jh-pagination
      v-show="page.total > 0"
      :total="page.total"
      v-model:currentPage="page.current"
      v-model:pageSize="page.size"
      @current-change="loadData"
      @size-change="loadData"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, h } from "vue";
import type { TableColumnDesc } from "@jhlc/common-core/src/components/table/base-table/type";

const tableRef = ref();
const list = ref([]);
const loading = ref(false);
const selectedRows = ref([]);
const page = ref({ current: 1, size: 10, total: 0 });

const columns = computed<TableColumnDesc[]>(() => [
  { type: "selection", width: 40 },
  { type: "index", label: "序号", width: 60 },
  {
    name: "orderNo",
    label: "订单号",
    width: 150,
    // 蓝色链接样式
    defaultSlot: ({ row }) => {
      return h(
        "span",
        {
          style: "color: #409eff; cursor: pointer;",
          onClick: () => viewDetail(row),
        },
        row.orderNo,
      );
    },
  },
  {
    name: "customerName",
    label: "客户名称",
    showOverflowTooltip: true,
  },
  {
    name: "amount",
    label: "金额",
    align: "right",
    formatter: (row) => `¥${Number(row.amount).toLocaleString()}`,
  },
  {
    name: "status",
    label: "状态",
    logicType: "ORDER_STATUS",
  },
  {
    name: "createTime",
    label: "创建时间",
    width: 180,
  },
  {
    label: "操作",
    width: 180,
    fixed: "right",
    operations: [
      {
        name: "view",
        label: "查看",
        onClick: (row) => viewDetail(row),
      },
      {
        name: "edit",
        label: "编辑",
        type: "primary",
        permission: ["order:edit"],
        onClick: (row) => editRecord(row),
      },
      {
        name: "delete",
        label: "删除",
        type: "danger",
        permission: ["order:delete"],
        disabled: (row) => row.status !== "draft",
        onClick: (row) => deleteRecord(row),
      },
    ],
  },
]);

const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};

const viewDetail = (row) => {
  // 查看详情
};

const editRecord = (row) => {
  // 编辑记录
};

const deleteRecord = (row) => {
  // 删除记录
};

const loadData = async () => {
  loading.value = true;
  try {
    // 加载数据
  } finally {
    loading.value = false;
  }
};

onMounted(() => {
  loadData();
});
</script>

可编辑表格

vue
<template>
  <BaseTable ref="tableRef" :data="list" :columns="editableColumns" border />
  <el-button @click="handleValidate">验证</el-button>
  <el-button @click="handleSave">保存</el-button>
</template>

<script setup lang="ts">
import { ref, computed } from "vue";

const tableRef = ref();
const list = ref([{ id: 1, name: "", quantity: 0, price: 0 }]);

const editableColumns = computed(() => [
  { type: "index", label: "序号", width: 60 },
  {
    name: "name",
    label: "名称",
    editable: true,
    rules: [{ required: true, message: "请输入名称" }],
  },
  {
    name: "quantity",
    label: "数量",
    editable: true,
    editComponent: () => ({
      tag: "el-input-number",
      props: { min: 1 },
    }),
  },
  {
    name: "price",
    label: "单价",
    editable: true,
    precision: 2,
  },
]);

const handleValidate = () => {
  tableRef.value?.validate((valid) => {
    console.log("验证结果:", valid);
  });
};

const handleSave = () => {
  const data = tableRef.value?.getTableData();
  console.log("保存数据:", data);
};
</script>

⚠️ 注意事项

  1. 列字段名使用 name 而非 prop

    typescript
    // ✅ 正确
    { label: "姓名", name: "userName" }
    
    // ❌ 错误
    { label: "姓名", prop: "userName" }
  2. 自定义渲染使用 defaultSlot 而非 render

    typescript
    // ✅ 正确
    {
      name: "title",
      defaultSlot: ({ row }) => h("span", { style: "color: blue;" }, row.title)
    }
    
    // ❌ 错误
    {
      name: "title",
      render: (value) => `<span style="color: blue;">${value}</span>`
    }
  3. formatter 只能返回纯文本

    typescript
    // ✅ 正确 - 返回文本
    formatter: (row) => `¥${row.amount}`;
    
    // ❌ 错误 - 返回 HTML(不会解析)
    formatter: (row) => `<span class="red">${row.amount}</span>`;
  4. 操作按钮的权限控制

    • 使用 permission 属性配置权限标识数组
    • 系统会自动根据用户权限显示/隐藏按钮
  5. 表格高度设置

    • 固定高度:height="400"
    • 自适应:配合 flex 布局,设置 height="100%"autoHeight
  6. 树形数据必须设置 rowKey

    vue
    <BaseTable :data="treeData" :columns="columns" rowKey="id" />

📚 相关文档

You may not distribute, modify, or sell this software without permission.