实操案例|TinyVue树表+动态行合并

本文由孟智强同学原创。

背景

团队某个小项目切换 UI 框架,要将 Element 换成 TinyVue。期间遇到一个树表形式的业务表格,支持多级下钻,且第一列有合并行。当初用 Element 实现这个表格时费了一些周折,料想 TinyVue 上场应该也不轻松,谁曾想一上手才知道——这比 Element 实现容易多了!

先上最终效果图(表格内容已脱敏处理):

图片

显示树表

TinyVue 的表格组件支持树表(详情),我们只需将业务数据按约定格式处理好,再简单进行配置即可。用到的表格数据如下:

const rawData = [
  { area: '华北', province: '北京', city: '北京', store: '密云店', sales: 72 },
  { area: '华东', province: '山东', city: '淄博', store: '蓝翔NO.1', sales: 57 },
  { area: '华中', province: '湖北', city: '武汉', store: '北区', sales: 21 },
  { area: '华北', province: '北京', city: '北京', store: '朝阳总店', sales: 123 },
  { area: '华东', province: '山东', city: '青岛', store: '蜊叉泊分店', sales: 12 },
  { area: '华中', province: '湖北', city: '武汉', store: '南区', sales: 42 },
  { area: '华北', province: '北京', city: '北京', store: '香山店', sales: 72 },
  { area: '华南', province: '广东', city: '广州', store: '花城店', sales: 90 },
  { area: '华南', province: '广西', city: '桂林', store: '甲天下', sales: 15 },
  { area: '华中', province: '湖北', city: '武汉', store: '城中区', sales: 54 },
  { area: '华南', province: '广东', city: '东莞', store: '万象汇', sales: 35 },
  { area: '华东', province: '山东', city: '青岛', store: '金沙滩分店', sales: 39 },
  { area: '华南', province: '广东', city: '深圳', store: '天虹', sales: 85 },
  { area: '华东', province: '浙江', city: '绍兴', store: '鲁镇店', sales: 87 },
  { area: '华南', province: '广东', city: '东莞', store: '大润发', sales: 26 },
  { area: '华北', province: '内蒙古', city: '乌兰察布', store: '乌拉旗舰店', sales: 15 },
  { area: '华南', province: '广东', city: '广州', store: '越秀店', sales: 60 }
];

我们先把它处理成树表约定的格式:

const mergeKeys = ['area', 'province', 'city'];  // 需要合并的字段名

// 根据合并字段进行排序,方便后续处理
rawData.sort((a: any, b: any) => {
  for (let key of mergeKeys) {
    if (a[key] !== b[key]) {
      return a[key].localeCompare(b[key]);
    }
  }
  return 0;
});

// 生成树表所需格式的数据
const tableData = rawData.reduce((arr, item) => {
  const provinceNode = initNodeData(arr, item, 'province');
  const cityNode = initNodeData(provinceNode.children, item, 'city');

  cityNode.children.push({ 
    place: item.store,
    store: 1,
    sales: item.sales
  });

  return arr;
}, [] as any[]);

function initNodeData(arr: any[], row: any, placeKey: string) {
  const placeValue = row[placeKey];
  let matched = arr.find(v => v.place === placeValue);

  if (!matched) {
    matched = {
      area: row.area,
      place: placeValue,
      store: 0,
      sales: 0,
      children: []  // 子级
    };
    arr.push(matched);
  }

  // 统计当前行的 store 数量和 sales 
  matched.store += 1;
  matched.sales += row.sales;

  return matched;
}

处理好的 tableData 格式如下:

[
  {
    "area": "华北",
    "place": "北京",
    "store": 3,
    "sales": 267,
    "children": [
      {
        "area": "华北",
        "place": "北京",
        "store": 3,
        "sales": 267,
        "children": [
          {
            "place": "密云店",
            "store": 1,
            "sales": 72,
            "_RID": "row_3"
          },
          ...
        ]
      }
    ]
  },
  ...
]

模板的配置非常方便,只需为需要展开的列配置 tree-node 即可。

Element 中的树表默认只能通过第一列的单元格来控制展开和折叠,如果想要在其他列中实现同样的功能,就需要使用一点黑魔法 hack。这一点要为 TinyVue 的方便好用点赞!

<template>
  <tiny-grid :data="tableData" :tree-config="{ children: 'children' }" border>
    <tiny-grid-column title="大区" field="area" width="120" />
    <tiny-grid-column title="地区" field="place" tree-node /> <!-- 这列控制树表展开/折叠 -->
    <tiny-grid-column title="店铺" field="store" />
    <tiny-grid-column title="销售额" field="sales" />
  </tiny-grid>
</template>

效果如下图:

图片

跨行合并首列单元格

TinyVue 表格组件提供了 row-span 和 span-method 两种单元格合并的方法,但前者不支持嵌套树表,所以这里选择后者。span-method 这个方法是不是很眼熟?对了,Element Table 中也有这个方法,两者在使用上几乎一模一样。

<template>
  <tiny-grid span-method="spanMethod" ... >...</tiny-grid>
</template>
function spanMethod({ columnIndex, row }: { columnIndex: number, row: any}) {
  if (columnIndex === 0) {  // 首列跨行合并
    return {
      rowspan: 1,  // 如何求跨行合并数值?
      colspan: 1
    }
  }
}

接下来的问题就是如何求出 spanMethod 方法返回值中的 rowspan 的值。

我们先来看表格,在初始状态下,表格首列需要跨行合并的单元格如下图红框处所示:

图片

不难看出每个“大区”(area)的跨行合并数就是其相邻且内容相同单元格的个数,例如“大区”一列中相邻的“华北”单元格有2个,则它的跨行合并数就是2。我们可以通过定义一个 rowspanMapping 变量来保存这些“大区”分别对应的跨行合并数:

// 跨行合并数,Map key 为 area 的值,Map value 为跨行合并数
const rowspanMapping: Map<string, number> = new Map();

// 统计不同大区的默认跨行合并数
tableData.forEach((item) => {
  const spanValue = rowspanMapping.get(item.area) ?? 0;
  rowspanMapping.set(item.area, spanValue + 1);
});

然后我们就得到了表格初始形态首列的跨行合并数信息:

Map(4) {'华北' => 2, '华东' => 2, '华南' => 2, '华中' => 1}

再应用到 spanMethod 方法中:

function spanMethod({ columnIndex, row }: { columnIndex: number, row: any}) {
  if (columnIndex === 0) {  // 首列跨行合并
    return {
      rowspan: rowspanMapping.get(row.area),  // 获取当前单元格跨行合并数
      colspan: 1
    }
  }
}

然而,实际的渲染效果与我们预期的完全不同,表格布局出现了严重的错位。如下图所示:

图片

经过分析,我们发现在进行单元格合并时,合并组中的起始单元格会占据对应的行数,而其余单元格则需要被移除,以保证表格的结构不会出现错位。在 TinyVue 表格组件的 spanMthod 方法中,我们通过将 rowspan 设为 0 来实现这一点。例如,“华北”的 rowspan 为 2,则第一个“华北”单元格的 rowspan 值就为2,而第二个“华北”单元格则应该设为0,即移除不渲染。

为此,我们需要改进我们统计大区默认跨行合并数的方法。我们可以通过定义一个私有属性来记录当前“大区”是否首次出现:

tableData.forEach((item) => {
  const spanValue = rowspanMapping.get(item.area) ?? 0;
  // 当前 area 值首次出现(用于设定 rowspan 的作用行)
  item._isAppearFirst = !rowspanMapping.has(item.area);
  rowspanMapping.set(item.area, spanValue + 1);
});

合并方法也要随之调整:

function spanMethod({ columnIndex, row }: { columnIndex: number, row: any}) {
  if (columnIndex === 0) {  // 首列跨行合并
    return {
      // 当前大区若首次出现,则应用跨行合并数,否则设为0(不渲染)
      rowspan: row._isAppearFirst ? rowspanMapping.get(row.area) : 0,
      colspan: 1
    }
  }
}

完成后效果如下图所示:

图片

合并行动态适配树表展开与折叠

目前为止,我们已经实现了树表和表格初始化状态首列的行合并,接下来要将两者结合起来完成最终效果。

当展开树表中的任意一行时,当前行下方会显示新的表格行;相反,折叠任意一行时,下方对应的表格行也会隐藏。换句话说,随着树表的展开和折叠,表格的总行数会发生变化,因此需要动态调整对应首列的跨行合并数,以确保表格布局不发生错位。

举例说明:

“华东”的跨行合并数默认为2

图片

当展开“山东”时,会显示下级的“青岛”和“淄博”,此时“华东”的跨行合并数应该加上这2个新出现的表格行,变为4,才能确保表格布局恢复正常。

图片

同理,当继续展开“青岛”时,会显示2个下级行,此时“华东”的跨行合并数应该再加上这2个新出现的表格行,变为6

图片

原理搞清了,下面分享代码实现。

既然需要根据树表的展开和折叠来计算跨行合并数,那么首先就要监听树表切换事件 toggle-tree-change

<template>
  <tiny-grid @toggle-tree-change="handleExpand" ... >
    ...
  </tiny-grid>
</template>

function handleExpand({ row }: { row: any }) {
  // ?
}

在表格组件底层,当树表展开或折叠后,会重新获取单元格合并数信息以进行布局。换句话说,我们需要在 handleExpand 方法中计算出树表行操作后的合并数,以便 spanMethod 方法能够获取到最新的单元格合并数。

为了计算操作后的合并数,我们需要知道当前操作是“展开”还是“折叠”,以便进行针对性的处理:

  • 当前为折叠操作,需要减去待折叠的行数

  • 当前为展开操作,需要加上待展开的行数

我们为每行数据添加一个私有属性 _treeExpanded 来存储展开/折叠状态。

function initNodeData(arr: any[], row: any, placeKey: string) {
  ...
  if (!matched) {
    matched = {
      ...
      _treeExpanded: false  // 自定义属性:当前树表行是否展开
    };
  }
 ...
}

这样我们就可以在 handleExpand 方法中进行展开或折叠的合并行数处理了:

function handleExpand({ row }: { row: any }) {
  const curRowspan = rowspanMapping.get(row.area) ?? 0;  // 当前 area 的跨行合并数
  const changedRowspan = row.children.length;  // 当前行的子级数量
  let spanValue;
  
  if (row._treeExpanded) {  // 当前行已展开:当前为折叠操作,需要减去待折叠的行数
    spanValue = curRowspan - changedRowspan;
  } else {  // 当前行已折叠:当前为展开操作,需要加上待展开的行数
    spanValue = curRowspan + changedRowspan;
  }
  
  row._treeExpanded = !row._treeExpanded;  // 更新状态
  rowspanMapping.set(row.area, spanValue);
}

目前的效果已经非常接近了,但通过测试我们发现:当树表按层级顺序逐步展开或折叠时,一切正常,然而,若直接跨层级操作,就会出现布局错位。正如下图所示:

图片

这是因为跨层级操作时,我们的代码中只考虑了当前行子级的变动数量,而没有考虑子级的子级,以及更深层级的展开/折叠状态。因此,我们需要将当前行的所有子级都纳入考虑。下面是最终调整后的代码:

function handleExpand({ row }: { row: any }) {
  const curRowspan = rowspanMapping.get(row.area) ?? 0;
  let changedRowspan = row.children.length;

  // 当前行已展开:当前为折叠操作,需要减去待折叠的行数(含后代)
  // 当前行已折叠:当前为展开操作,需要加上待展开的行数(含后代)
  const deepExpandedCount = (arr: any[]) => {
    if (!Array.isArray(arr)) {
      return;
    }

    arr.forEach((item: any) => {
      if (item._treeExpanded) {
        // 递归累加已经展开的子级个数
        changedRowspan += item.children.length;
        deepExpandedCount(item.children);
      }
    });
  }

  deepExpandedCount(row.children);

  const spanValue = curRowspan + changedRowspan * (row._treeExpanded ? -1 : 1);
  
  row._treeExpanded = !row._treeExpanded;  // 更新状态
  rowspanMapping.set(row.area, spanValue);
}

要点总结

1、为需要控制树表展开/折叠的列配置 tree-node 属性;

2、span-method 方法返回当前单元格的合并数,其中 rowspan 为跨行合并数,当设为0时,当前单元格不渲染;

3、定义一个变量以存储表格的跨行合并数信息;

4、对于相邻且内容相同的同组单元格,仅设置该组首个单元格的 rowspan 为对应的跨行合并数,其余单元格均设为 0;

5、监听表格的 toggle-tree-change 事件,区分树表的展开、折叠操作,动态修改对应的跨行合并数信息。

关于OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网:https://opentiny.design/

OpenTiny 代码仓库:https://github.com/opentiny/

TinyVue 源码:https://github.com/opentiny/tiny-vue

TinyEngine 源码: https://github.com/opentiny/tiny-engine

欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~ 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/920648.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Mesh路由组网

Mesh无线网格网络&#xff0c;多跳&#xff08;multi-hop&#xff09;网络&#xff0c;为解决全屋覆盖信号&#xff0c;一般用于家庭网络和小型企业 原理 网关路由器&#xff08;主路由&#xff0c;连接光猫&#xff09;&#xff0c;Mesh路由器&#xff08;子路由&#xff0c;…

基于Windows系统用C++做一个点名工具

目录 一、前言 二、主要技术点 三、准备工作 四、主界面 1.绘制背景图 2、实现读取花名册功能 3.实现遍历花名册功能 4.实现储存功能 4.1创建数据库 4.2存储数据到数据库表 4.3读取数据库表数据 一、前言 人总是喜欢回忆过去&#xff0c;突然回忆起…

11.9K Star!强大的 Web 爬虫工具 FireCrawl:为 AI 训练与数据提取提供全面支持

在这个信息爆炸的时代&#xff0c;数据就是力量。尤其是对于开发者来说&#xff0c;获取并利用好数据&#xff0c;就意味着拥有更多的主动权和竞争力。 无论是用来训练大语言模型&#xff0c;还是用于增强检索生成&#xff08;RAG&#xff09;&#xff0c;数据都扮演着至关重要…

云原生之k8s服务管理

文章目录 服务管理Service服务原理ClusterIP服务 对外发布应用服务类型NodePort服务Ingress安装配置Ingress规则 Dashboard概述 认证和授权ServiceAccount用户概述创建ServiceAccount 权限管理角色与授权 服务管理 Service 服务原理 容器化带来的问题 自动调度&#xff1a;…

前端面试题整理-前端异步编程

1. 进程、线程、协程的区别 在并发编程领域&#xff0c;进程、线程和协程是三个核心概念&#xff0c;它们在资源管理、调度和执行上有着本质的不同。 首先&#xff0c;进程是操作系统进行资源分配和调度的独立单位&#xff08;资源分配基本单位&#xff09;&#xff0c;每个进…

动静态库:选择与应用的全方位指南

目录 1 软链接 1.1 软链接的建立方式和观察现象 1.2 软链接的原理 2 硬链接 2.1 硬链接的建立方式和观察现象 2.2 硬链接的本质 2.3 我们用户不能给目录建立硬链接 3. 动静态库复习 4 动静态库的制作 4.1 静态库的制作与使用 4.1.2 打包 4.1.3 静态库的使用 4.2 动…

【ROS2】多传感器融合、实现精准定位:robot_localization

1、简述 robot_localization在SLAM建图、导航中常用于将多个传感器融合(IMU、里程计、深度相机、GPS等),以提高定位精度,为机器人提供了在三维空间中的非线性状态估计 robot_localization包含两个状态估计节点: ekf_localization_node:扩展卡尔曼滤波(EKF),缺点是非…

极客大挑战2024wp

极客大挑战2024wp web 和misc 都没咋做出来&#xff0c;全靠pwn✌带飞 排名 密码学和re没做出几个&#xff0c;就不发了 web ez_pop 源代码 <?php Class SYC{public $starven;public function __call($name, $arguments){if(preg_match(/%|iconv|UCS|UTF|rot|quoted…

40分钟学 Go 语言高并发:并发下载器开发实战教程

并发下载器开发实战教程 一、系统设计概述 1.1 功能需求表 功能模块描述技术要点分片下载将大文件分成多个小块并发下载goroutine池、分片算法断点续传支持下载中断后继续下载文件指针定位、临时文件管理进度显示实时显示下载进度和速度进度计算、速度统计错误处理处理下载过…

李宏毅机器学习课程知识点摘要(1-5集)

前5集 过拟合&#xff1a; 参数太多&#xff0c;导致把数据集刻画的太完整。而一旦测试集和数据集的关联不大&#xff0c;那么预测效果还不如模糊一点的模型 所以找的数据集的量以及准确性也会影响 由于线性函数的拟合一般般&#xff0c;所以用一组函数去分段来拟合 sigmoi…

Spring Boot教程之五:在 IntelliJ IDEA 中运行第一个 Spring Boot 应用程序

在 IntelliJ IDEA 中运行第一个 Spring Boot 应用程序 IntelliJ IDEA 是一个用 Java 编写的集成开发环境 (IDE)。它用于开发计算机软件。此 IDE 由 Jetbrains 开发&#xff0c;提供 Apache 2 许可社区版和商业版。它是一种智能的上下文感知 IDE&#xff0c;可用于在各种应用程序…

本地Docker部署开源WAF雷池并实现异地远程登录管理界面

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

如何快速将Excel数据导入到SQL Server数据库

工作中&#xff0c;我们经常需要将Excel数据导入到数据库&#xff0c;但是对于数据库小白来说&#xff0c;这可能并非易事&#xff1b;对于数据库专家来说&#xff0c;这又可能非常繁琐。 这篇文章将介绍如何帮助您快速的将Excel数据导入到sql server数据库。 准备工作 这里&…

[产品管理-91]:产品经理的企业运营的全局思维-1

目录 前言&#xff1a;企业架构图 产品经理的企业运营全局思维 1、用户 - 用户价值与体验&#xff1a;真正的需求&#xff0c;真正的问题&#xff0c;一切的原点 2、大势 - 顺应宏观大势&#xff1a;政策趋势、行业趋势、技术趋势 3、市场 - 知己知彼&#xff1a;市场调研…

简单实现vue2响应式原理

vue2 在实现响应式时&#xff0c;是根据 object.defineProperty() 这个实现的&#xff0c;vue3 是通过 Proxy 对象实现&#xff0c;但是实现思路是差不多的&#xff0c;响应式其实就是让 函数和数据产生关联&#xff0c;在我们对数据进行修改的时候&#xff0c;可以执行相关的副…

论文解析:EdgeToll:基于区块链的异构公共共享收费系统(2019,IEEE INFOCOM 会议);layer2 应对:频繁小额交易,无交易费

目录 论文解析:EdgeToll:基于区块链的异构公共共享收费系统(2019,IEEE INFOCOM 会议) 核心内容概述 核心创新点原理与理论 layer2 应对:频繁小额交易,无交易费 论文解析:EdgeToll:基于区块链的异构公共共享收费系统(2019,IEEE INFOCOM 会议) 核心内容是介绍了一个…

基于python Django的boss直聘数据采集与分析预测系统,爬虫可以在线采集,实时动态显示爬取数据,预测基于技能匹配的预测模型

本系统是基于Python Django框架构建的“Boss直聘”数据采集与分析预测系统&#xff0c;旨在通过技能匹配的方式对招聘信息进行分析与预测&#xff0c;帮助求职者根据自身技能找到最合适的职位&#xff0c;同时为招聘方提供更精准的候选人推荐。系统的核心预测模型基于职位需求技…

SemiDrive E3 硬件设计系列---唤醒电路设计

一、前言 E3 系列芯片是芯驰半导体高功能安全的车规级 MCU&#xff0c;对于 MCU 的硬件设计部分&#xff0c;本系列将会分模块进行讲解&#xff0c;旨在介绍 E3 系列芯片在硬件设计方面的注意事项与经验&#xff0c;本文主要讲解 E3 硬件设计中唤醒电路部分的设计。 二、RTC 模…

Leetcode198. 打家劫舍(HOT100)

代码&#xff1a; class Solution { public:int rob(vector<int>& nums) {int n nums.size();vector<int> f(n 1), g(n 1);for (int i 1; i < n; i) {f[i] g[i - 1] nums[i - 1];g[i] max(f[i - 1], g[i - 1]);}return max(f[n], g[n]);} }; 这种求…

一文探究48V新型电气架构下的汽车连接器

【哔哥哔特导读】汽车电源架构不断升级趋势下&#xff0c;48V系统是否还有升级的必要&#xff1f;48V新型电气架构将给连接器带来什么改变&#xff1f; 在插混和纯电车型逐渐普及、800V高压平台持续升级的当下&#xff0c;48V技术还有市场吗? 这个问题很多企业的回答是不一定…