!!!###!!!title=主从表——VisActor/VTable 教程文档!!!###!!!!!!###!!!description= !!!###!!!

主从表插件

功能介绍

MasterDetailPlugin 插件为 VTable ListTable 提供强大的主从表功能,能够在主表的行中嵌入完整的子表格,实现层次化的数据展示效果。该插件特别适用于需要展示关联详细信息的业务场景,如订单详情、项目任务、产品规格等复杂数据结构的可视化展示。

插件配置

配置接口定义

MasterDetailPlugin 采用 TypeScript 接口规范,确保类型安全和开发体验。插件构造函数接受 MasterDetailPluginOptions 配置对象,具体定义如下:

interface MasterDetailPluginOptions {
  /** 是否启用checkbox级联功能 - 控制主从表之间的复选框联动,默认为 true */
  enableCheckboxCascade?: boolean;
  /** 子表数据的字段名 - 用于指定记录中子表数据所在的属性名,默认为 'children' */
  childrenKey?: string;
  /** 子表配置选项 - 支持静态配置对象或动态配置函数 */
  detailTableOptions?: DetailTableOptions | ((params: { data: unknown; bodyRowIndex: number }) => DetailTableOptions);
}

interface DetailTableOptions extends VTable.ListTableConstructorOptions {
  /** 子表样式配置,包括布局边距和尺寸设置 */
  style?: {
    margin?: number | [number, number] | [number, number, number, number];
    height?: number;
  };
}

核心参数详解

参数名称类型默认值功能说明
enableCheckboxCascadebooleantrue是否启用主从表之间的checkbox级联功能,主表中的复选框选择会自动与相应的子表同步
detailTableOptionsDetailTableOptions | Function-子表配置选项,支持静态对象配置或基于数据的动态配置函数

DetailTableOptions 高级配置

DetailTableOptions 完全继承 VTable.ListTableConstructorOptions 的所有特性,这意味着子表享有与主表相同的功能和配置能力:

核心配置项:

  • columns:子表列定义,支持完整的列配置选项
  • theme:主题样式配置,可独立于主表设置
  • defaultRowHeight:子表行高设置
  • sortState:排序状态配置
  • widthMode:宽度模式(standard、adaptive、autoWidth 等)
  • 以及所有 ListTable 支持的高级配置选项

DetailTableOptions中不需要去配置record,展开行的时候会从配置的主表对应的行中的children中配置的值作为子表的record

样式配置选项

属性名称数据类型默认值配置说明
marginnumber | number[]0子表边距设置,支持灵活的边距配置:
单数值12 - 四边统一边距
双数值[12, 16] - 垂直和水平边距
四数值[12, 16, 12, 16] - 上、右、下、左独立设置
heightnumber | 'auto'300子表容器固定高度(单位:像素),建议根据业务数据量合理设置,也可通过配置auto来自动适应子表高度

快速开始

基础集成步骤

集成 MasterDetailPlugin 到您的项目中只需三个简单步骤:

第一步:导入插件

import { MasterDetailPlugin } from '@visactor/vtable-plugins';

第二步:创建插件实例

// 创建主从表插件实例
const masterDetailPlugin = new MasterDetailPlugin({
  id: 'master-detail-plugin',
  enableCheckboxCascade: true, // 启用checkbox级联功能(默认:true)
  detailTableOptions: {
    columns: [
      { field: 'task', title: '任务名称', width: 220 },
      { field: 'status', title: '状态', width: 120 }
    ],
    defaultRowHeight: 30,
    defaultHeaderRowHeight: 30,
    style: { margin: 12, height: 160 },
    theme: VTable.themes.BRIGHT
  }
});

第三步:配置到 VTable 实例

// 将插件集成到表格配置中
const tableOptions = {
  container: document.getElementById('tableContainer'),
  columns: [/* 主表列配置 */],
  records: [/* 主表数据 */],
  plugins: [masterDetailPlugin]  // 注册插件
};

// 创建表格实例
const tableInstance = new VTable.ListTable(tableOptions);

基础功能演示

以下是一个简化的主从表功能演示,展示插件的核心工作原理和基本配置方法:

配置数据对应展开状态

可以通过配置option中的hierarchyExpandLevel来配置该数据行对应的展开状态

const option: VTable.ListTableConstructorOptions = {
  // ......
  hierarchyExpandLevel: 2,
  // ......
};

也可以在数据records中的hierarchyState来配置该数据行对应的展开状态

{
  id: 1,
  name: `员工1`,
  department: 'Engineering',
  position: Senior Developer,
  salary: 15000,
  status: 'Active',
  hierarchyState: 'expand',
  children:[
    {
      project: `项目A-1`,
      role: '负责人',
      startDate: '2024-01-15',
      endDate: '2024-12-31',
      progress: 85
    },
  ]
};

动态配置

MasterDetailPlugin 支持基于数据内容和行位置的动态配置,实现更灵活的业务逻辑处理:

应用场景

  • 根据不同数据类型展示不同的子表结构
  • 为特定行设置个性化的样式主题
  • 基于业务规则动态调整子表高度和列配置

实现示例

const masterDetailPlugin = new MasterDetailPlugin({
  id: 'employee-detail-plugin',
  detailTableOptions: ({ data, bodyRowIndex }) => {
    if (bodyRowIndex === 0) {
      return {
        columns: [
          {
            field: 'project',
            title: '项目名称',
            width: 180
          },
          {
            field: 'role',
            title: '项目角色',
            width: 120
          },
          {
            field: 'startDate',
            title: '开始日期',
            width: 100
          },
          {
            field: 'endDate',
            title: '结束日期',
            width: 100
          },
          {
            field: 'progress',
            title: '项目进度',
            width: 100,
          }
        ],
        theme: VTable.themes.BRIGHT,
        style: {
          margin: 20,
          height: 300
        }
      };
    }
    return {
      columns: [
        {
          field: 'project',
          title: '项目名称',
          width: 180
        },
        {
          field: 'role',
          title: '项目角色',
          width: 120
        },
        {
          field: 'startDate',
          title: '开始日期',
          width: 100
        },
        {
          field: 'endDate',
          title: '结束日期',
          width: 100
        },
        {
          field: 'progress',
          title: '项目进度',
          width: 100,
        }
      ],
      theme: VTable.themes.DARK,
      style: {
        margin: 20,
        height: 300
      }
    };
  }
});

与groupBy分组搭配使用

如果在使用注册表插件的时候想要使用分组,那么配置groupBy的时候请只传入一个参数,不然插件将无法识别

懒加载设置

MasterDetailPlugin 支持懒加载功能,允许在用户展开行时动态异步加载子表数据,这对于处理大量数据或需要从服务器实时获取数据的场景非常有用。

懒加载的工作流程:

  • 数据标识:在主表数据中,将需要懒加载的行的 children 属性设置为 true
  • 事件触发机制:当用户点击展开图标时,VTable触发 TREE_HIERARCHY_STATE_CHANGE 事件

核心API:

  • 监听事件:'TREE_HIERARCHY_STATE_CHANGE'
  • 显示加载状态:tableInstance.setLoadingHierarchyState(col, row)
  • 设置子数据:plugin.setRecordChildren(detailData, col, row)

实现方式:

// 数据结构示例
const masterData = [
  {
    id: 1,
    name: "张三公司", 
    amount: 15000,
    // 静态子数据 - 直接显示
    children: [
      { productName: "笔记本电脑", quantity: 2, price: 5000 },
      { productName: "鼠标", quantity: 5, price: 100 }
    ]
  },
  {
    id: 2,
    name: "李四企业",
    amount: 25000,
    children: true // 懒加载标识 - 需要异步加载数据
  }
];

// 监听展开/收起事件
const { TREE_HIERARCHY_STATE_CHANGE } = VTable.ListTable.EVENT_TYPE;
tableInstance.on(TREE_HIERARCHY_STATE_CHANGE, async (args) => {
  // 只处理展开操作且 children 为 true(懒加载标识)
  if (args.hierarchyState === VTable.TYPES.HierarchyState.expand && 
      args.originData?.children === true) {
    
    // 显示loading状态
    tableInstance.setLoadingHierarchyState(args.col, args.row);
    
    try {
      // 异步获取数据
      const detailData = await fetchDataFromServer(args.originData.id);
      
      // 设置子数据并自动展开
      plugin.setRecordChildren(detailData, args.col, args.row);
    } catch (error) {
      console.error('Failed to load detail data:', error);
    }
  }
});

技术实现原理:

插件内部通过以下机制实现懒加载支持:监听VTable的 TREE_HIERARCHY_STATE_CHANGE 事件,确保层级状态的正确同步

以下是一个完整的懒加载示例,演示如何在订单管理系统中实现产品明细的懒加载:

典型业务场景示例

场景一:企业员工管理系统

在企业人力资源管理中,需要在员工列表中展示每个员工的项目参与情况。主表显示员工的基本信息(姓名、部门、职位等),点击展开后显示该员工参与的所有项目详情和工作记录。

场景二:B2B客户订单跟踪系统

业务需求:在 B2B 客户关系管理系统中,销售人员需要快速掌握客户概况,并能够详细查看每个客户的历史订单、交易金额和订单状态分布。

解决方案:主表展示客户关键信息(客户名称、所属行业、地区分布、累计交易等),展开后显示该客户的完整订单历史,包括订单详情、产品信息、交易金额和订单状态跟踪。

插件接口和事件

插件接口

getAllSubTableInstances(Function)

获取所有子表实例的映射表。

/**
 * 获取所有子表实例
 * @returns 子表实例的Map,键为bodyRowIndex,值为VTable实例
 */
getAllSubTableInstances(): Map<number, VTable.ListTable> | null

返回包含所有已创建子表实例的Map,其中键为主表的body行索引(不包含表头),值为对应的VTable子表实例。如果没有子表实例,则返回null。

getSubTableByRowIndex(Function)

根据主表行号获取子表实例。

/**
 * 根据主表行号获取子表实例
 * @param rowIndex 主表行索引(包含表头)
 * @returns 子表实例,如果不存在则返回null
 */
getSubTableByRowIndex(rowIndex: number): VTable.ListTable | null

根据主表的行索引(包含表头)获取对应的子表实例。该方法会自动计算出对应的body行索引。

getSubTableByBodyRowIndex(Function)

根据主表body行号获取子表实例。

/**
 * 根据主表body行号获取子表实例
 * @param bodyRowIndex 主表body行索引(不包含表头)
 * @returns 子表实例,如果不存在则返回null
 */
getSubTableByBodyRowIndex(bodyRowIndex: number): VTable.ListTable | null

根据主表的body行索引(不包含表头)获取对应的子表实例。这是最直接的获取子表实例的方法。

filterSubTables(Function)

根据条件筛选子表实例。

/**
 * 根据条件筛选子表实例
 * @param predicate 筛选条件函数
 * @returns 符合条件的子表实例数组
 */
filterSubTables(
  predicate: (bodyRowIndex: number, subTable: VTable.ListTable, record?: unknown) => boolean
): Array<{ bodyRowIndex: number; subTable: VTable.ListTable; record?: unknown }>

根据提供的筛选条件函数筛选子表实例。筛选函数接收body行索引、子表实例和记录数据作为参数,返回布尔值表示是否符合条件。返回值为包含符合条件的子表信息的数组。

setRecordChildren(Function)

设置记录的子数据并展开。

/**
 * 设置记录的子数据并展开
 * @param children 子数据数组
 * @param col 列索引
 * @param row 行索引
 */
setRecordChildren(children: unknown[], col: number, row: number): void

为指定单元格对应的记录设置子数据,并自动展开该行显示子表。该方法会修改原始记录数据的children属性并刷新表格显示,是在懒加载的时候使用

插件事件

主从表插件通过setupSubTableEventForwarding机制遍历VTable.TABLE_EVENT_TYPE将子表的所有事件转发到主表,使用VTable.TABLE_EVENT_TYPE.PLUGIN_EVENT事件类型进行统一转发:

子表事件转发

主从表插件事件转发机制。

/**
 * 插件事件信息接口
 */
interface SubTableEventInfo {
  /** 子表事件类型 */
  eventType: keyof typeof VTable.TABLE_EVENT_TYPE;
  /** 主表行索引(不包含表头) */
  masterBodyRowIndex: number;
  /** 主表行索引(包含表头) */
  masterRowIndex: number;
  /** 子表实例 */
  subTable: VTable.ListTable;
  /** 原始事件数据 */
  originalEventArgs?: unknown;
}

当子表发生任何事件时,插件会将事件信息包装为SubTableEventInfo对象,并通过主表的PLUGIN_EVENT事件进行转发。

监听示例:

// 监听子表事件
tableInstance.on(VTable.TABLE_EVENT_TYPE.PLUGIN_EVENT, (args) => {
  const { plugin, pluginEventInfo } = args;
  
  // 检查是否是主从表插件的事件
  if (plugin?.name === 'Master Detail Plugin') {
    const eventInfo = pluginEventInfo;
    
    console.log('子表事件:', {
      eventType: eventInfo.eventType,           // 原始事件类型,如 'click_cell'
      masterRowIndex: eventInfo.masterRowIndex, // 主表行索引(含表头)
      masterBodyRowIndex: eventInfo.masterBodyRowIndex, // 主表body行索引
      subTable: eventInfo.subTable,            // 子表实例
      originalEventArgs: eventInfo.originalEventArgs // 原始事件参数
    });
    
    // 根据事件类型处理不同逻辑
    if (eventInfo.eventType === VTable.TABLE_EVENT_TYPE.CLICK_CELL) {
      // 处理子表单元格点击事件
      handleSubTableCellClick(eventInfo);
    }
  }
});

通过这种事件转发机制,开发者可以在主表级别统一处理所有子表的交互事件,实现复杂的业务逻辑。

技术实现原理

  • ViewBox 定位系统:插件基于 VTable 的 ViewBox 机制进行精确定位,确保子表能够准确渲染在展开行的指定位置。

  • Canvas 共享渲染:子表与主表共享同一个 Canvas 画布,避免了多画布切换带来的性能开销,同时保证了视觉一致性。

  • 动态行高调整:通过智能调整展开行的行高,同时保持该行中 CellGroup 的原始高度不变,巧妙地创造出用于渲染子表的空白区域。

  • 滚动事件优化:插件自动将 scrollEventAlwaysTrigger 设置为 true,确保在表格滚动到边界时仍能触发滚动事件,实现子表的自动滚动效果。

主从表插件的一个重要设计理念是:子表所在的行在视觉上呈现独立的行,但其实际上属于对应的主表展开行

主表的行号(如图中的1、2、3、4、5、6, 7)始终保持连续,不会因为子表的存在而中断,子表实际上是通过动态调整主表展开行的高度,使得其行内拥有对应的空间,然后在该行内部创建的渲染区域,因此子表并没有真正的"行号",主从表插件通过各种技术手段让展开行在视觉上呈现为一个独立的子表区域,但本质上这个区域仍然属于主表对应的行

注意事项

  • 请不要使用转置功能
  • 请不要把tree和主从表一起用

本文档贡献者

抽象薯片