在关系型数据库中,数据往往是以列表的形式存储,但实际上物理结构上并列的数据存在逻辑上的关系,比如父子关系的树形结构等。同样数据库存储的数据在界面上展示时也需要将类似的关系展现出来,比如表格中常用的单元格合并。
在Excel中单元格的合并是由使用者操控的,并没有什么前置条件,但如果要通过代码实现单元格合并,就必须有一系列能让计算机读懂的规则。常见的的需要合并单元格的情况是:多个相邻的单元格有相同的值,同时相邻区域应该是长方形,如宽高为1*2、2*1、4*2等。
后端代码遍历DataTable中的数据,拼接成HTML,并在处理过程中合并单元格,输出给前端。
/// <summary> /// 数据表转换为HTML并自动合并单元格 /// </summary> /// <param name="dt">数据表</param> /// <param name="tableId">标识</param> /// <param name="configs">列配置</param> /// <param name="isAddSort">是否添加序号</param> /// <returns></returns> public string ConvertTableToHtmlWithMergeCells(DataTable dt,string tableId,List<SysDataTableConfigure> configs,bool isAddSort) { //先行后列,建立单元格值字典 Dictionary<string, List<int[]>> dicInit = new Dictionary<string, List<int[]>>(); for (int i = 0; i < dt.Rows.Count; i++) { for (int j = 0; j < dt.Columns.Count; j++) { if (!string.IsNullOrWhiteSpace(dt.Rows[i][j].ToString())) { var k = dt.Rows[i][j].ToString(); if (!dicInit.ContainsKey(k)) { dicInit.Add(k, new List<int[]> { new int[2] { i, j } }); } else { var v = dicInit[k]; dicInit.Remove(k); v.Add(new int[2] { i, j }); dicInit.Add(k, v); } } } } //过滤出具有相同值且需要合并的单元格 Dictionary<string, List<int[]>> dic = new Dictionary<string, List<int[]>>(); foreach (var k in dicInit) { if (IsNeedMerge(k.Value)) { dic.Add(k.Key, k.Value); } } //构造数据表 StringBuilder htmlBuilder = new StringBuilder(); htmlBuilder.Append("<table cellspacing=\"0\" cellpadding=\"0\" width=\"100% \" id=\"" + tableId+"\">"); htmlBuilder.Append("<tbody>"); //首行 htmlBuilder.Append("<tr class=\"firstRow\">"); if (isAddSort)//是否增加序号列 { htmlBuilder.Append("<td>序号</td>"); } if (configs!=null)//有列配置则读取列配置 { for (int i = 0; i < dt.Columns.Count; i++)//列 { htmlBuilder.Append("<td>"); htmlBuilder.Append(configs.First(t => t.EnglishName == dt.Columns[i].ColumnName).HeaderName); htmlBuilder.Append("</td>"); } } else//无列配置则使用DataTable列配置 { for (int i = 0; i < dt.Columns.Count; i++)//列 { htmlBuilder.Append("<td>"); htmlBuilder.Append(dt.Columns[i].ColumnName); htmlBuilder.Append("</td>"); } } htmlBuilder.Append("</tr>"); //遍历数据表并拼接内容 for (int i=0;i<dt.Rows.Count;i++)//行 { htmlBuilder.Append("<tr>"); int rowNum = i + 1;//行序号 if (isAddSort)//是否增加行序号 { htmlBuilder.Append("<td>"+rowNum+"</td>"); } for (int j=0;j<dt.Columns.Count;j++)//列 { string cellValue = dt.Rows[i][j].ToString(); if (!string.IsNullOrEmpty(cellValue)&&dic.ContainsKey(cellValue)) { var arrys = dic[cellValue]; int[] firstCell = arrys[0]; int[] lastCell = arrys[arrys.Count - 1]; int row = lastCell[0] - firstCell[0] + 1;//行数 int col = lastCell[1] - firstCell[1] + 1;//列数 if (i == firstCell[0] && j == firstCell[1])//需要合并的单元格中,只保留首行首列的一个 { htmlBuilder.Append("<td rowspan=\""+row+"\" colspan=\""+col+"\">"); htmlBuilder.Append(cellValue); htmlBuilder.Append("</td>"); } } else { htmlBuilder.Append("<td>"); htmlBuilder.Append(cellValue); htmlBuilder.Append("</td>"); } } htmlBuilder.Append("</tr>"); } htmlBuilder.Append("</tbody>"); htmlBuilder.Append("</table>"); string strHtml = htmlBuilder.ToString().Replace("\"", ""); return strHtml; } /// <summary> /// 判断是否需要合并单元格 /// </summary> /// <param name="arrys"></param> /// <returns></returns> private bool IsNeedMerge(List<int[]> arrys) { bool result = false; int num = arrys.Count;//具有相同值的单元格总数 if (num>1)//移除只出现值只出现过一次的单元格 { int[] firstCell = arrys[0]; int[] lastCell = arrys[arrys.Count - 1]; int row = lastCell[0] - firstCell[0] + 1;//行数 int col = lastCell[1] - firstCell[1] + 1;//列数 if (row * col == num)//若行数与列数之积等于单元格总数,则说明具有相同值的单元格相邻分布,可以合并 { result = true; } } return result; }这里的思路有点类似倒排索引,为数据表中的所有非空值建立索引表,记录每个值在数据表中出现的位置。首先进行一次过滤,将索引表中只出现过一次的值去除掉,只出现一次是肯定不需要合并单元格的,也能减少查询过程。然后循环遍历数据表,开始拼接HTML字符串,如果单元格的值在索引表中,说明需要拼接单元格。同时索引表中值的位置是按照行、列的顺序记录的,所以能确保位置列表中的第一个位置是需要合并单元格的第一个,只要设置这个单元格的跨行、跨列属性即可。
需要注意的是这个方法还存在以下问题:
1.只适用于具有相同值的单段连续单元格,多段间隔连续的单元格则无法分别合并。再进一步优化的思路是索引表在记录位置的基础上记录两个相同值单元格的间隔步长。
2.只适用于字符串类型的单元格值。在进一步优化的思路是对于数字类型的单元格值,要么不合并,要么除了合并单元格外,还要将计算数字之和。