原生JS表格数据常用总结

主要是在数据报表这块, 做了好几年发现, 其实用户最终想要看的并不是酷炫的BI大屏, 而是最基础也是最复杂的 中国式报表. 更多就是倾向于从表格中去获取数据信息, 最简单的就是最好的, 于是还是来总结一下表格这块的东西.

基础表格

先来实现一个最基础的表格, 用 table 标签, 为更好语义话, 用 thead, tbody 将表格分为表头和表数据,

同时给表格行添加一个悬浮的的效果, 表头也进行加粗这样的, 稍微好看一点.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>基础表格</title>
  <style>
    /* 单元格边框合并, 宽度 100% */
    table {
      border-collapse: collapse;
      width: 100%;
    }
    /* 每个格子都加上边框 */
    th, td {
      border: 1px solid #ccc;
      text-align: center;
      height: 2em;
    }
    /* 表头加个背景 */
    th {
      background-color: #f8f8f9;
    }
    /* 行加一个悬停 */
    tr:hover {
      background-color: #f8f8f9;
    }

  </style>
</head>
<body>
  <div class="tableCon">
    <table>
      <thead>
        <tr>
          <th>姓名</th>
          <th>年龄</th>
          <th>性别</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>小王</td>
          <td>28</td>
          <td>男</td>
        </tr>
        <tr>
          <td>小红</td>
          <td>24</td>
          <td>女</td>
        </tr>
        <tr>
          <td>小张</td>
          <td>18</td>
          <td>男</td>
        </tr>
      </tbody>
    </table>
  </div>
</body>
</html>

表格字段可拖拽列宽

这个功能很重要, 尤其是对用户的一些交互上

// 拖拽列宽
    var table = document.querySelector('table')
    var resizableColumn = null;
    var startX = 0;
    var startWidth = 0;

    table.addEventListener("mousedown", function (event) {
      if (event.target.tagName === "TH" && event.offsetX > event.target.offsetWidth - 10) {
        resizableColumn = event.target;
        startX = event.pageX;
        startWidth = resizableColumn.offsetWidth;
      }
    });

    document.addEventListener("mousemove", function (event) {
      if (resizableColumn) {
        var newWidth = startWidth + (event.pageX - startX);
        resizableColumn.style.width = newWidth + "px";
      }
    });

    document.addEventListener("mouseup", function () {
      resizableColumn = null;
    });

固定首行首列

很多推荐的方式, 包括一些UI框架都是通过三至四个表格组合,然后js处理同步滚动来实现,这样的好处是容易实现,PC端也不会出现什么问题。

但是在手机端时,会有严重的不同步滚动现象,处理的不好时,甚至会出现错位等,体验不好. 其实我们用纯 css 实现即可.

  • table-layout: fiexd
  • position: sticky

为了让表格呈现滚动效果, 需给表格容器设置宽度, 同时给表格布局为 table-layout: fixed

表格的 fiexd 布局下, 单元格宽度根据第一行进行设定, 提前确定下来而不用再动态去根据内容计算, 可以加快渲染速度, 即我们可以事先固定表格宽度, 内容撑大则隐藏啥的操作 (针对数据指标)

固定行列则给需设置的行/列 单元格进行 position: sticky. 它的表现类似于 relative + fixed 的合体, 当超过目标区域时, 会固定于目标的位置.

    <style>
      .table-con {
        width: 200px;
        height: 300px;
        overflow: auto;
        border: 1px solid gray;
        border-right: 0;
        border-bottom: 0;
      }

      table {
        table-layout: fixed;
        border-collapse: separate;
        border-spacing: 0;
        width: 100%;
      }
    
      td,
      th {
        /* 设置td,th宽度高度 */
        width: 100px;
        height: 2em;

        /* 超出宽度显示为... */
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        text-align: center;
        /* 只设置右, 下的边框线 */
        border-right: 1px solid #ccc;
        border-bottom: 1px solid #ccc;
      }
      th {
        background-color: #f8f8f8;
      }

      /* 列首永远固定在头部  */
      thead tr th {
        position: sticky;
        top: 0;
      }
      /* 首行永远固定在左侧 */
      td:first-child,
      th:first-child {
        position: sticky;
        left: 0;
        z-index: 1;
        background-color: #f8f8f8;
      }
    
      th:first-child {
        z-index: 2;
        background-color: #f8f8f8;
      }
    </style>

动态数据生成表格

就模拟一下后端传过来的 JSON 数据而已, 然后这里用原生 js 来创建表格和渲染数据而已.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>动态表格</title>
  <style> /* 同上面一样的内容 */
	
</head>
<body>
  <div id="table-con"></div>

  <script>
    // 模拟后端传过来的数据
    var tableData = [
      { name: 'youge', age: 28, sex: 'M'},
      { name: 'yaya', age: 18, sex: 'F'},
      { name: 'jiezai', age: 30, sex: 'M'},
    ]

    // 当行数到 10w, 浏览器就有点卡了
    // for (var i = 1; i < 100000; i++) {
    //   tableData.push({ name: 'test', age: 30, sex: 'F'})
    // }

    var tableCon = document.getElementById('table-con')
    var table = document.createElement('table')
    var thead = document.createElement('thead')
    var tbody = document.createElement('tbody')

    // 创建表头, 并获取第一个对象的键作为表头
    headRow = document.createElement('tr')
    headCells = Object.keys(tableData[0])

    for (var i = 0; i < headCells.length; i++) {
      var th = document.createElement('th')
      th.innerHTML = headCells[i]
      headRow.appendChild(th)
    }
    // 将表头挂载到 thead中
    thead.appendChild(headRow)


    // 创建表格数据行, tr, td 根据数据
    for (var i = 0; i < tableData.length; i++) {
      var tr = document.createElement('tr')
      for (var j = 0; j < headCells.length; j++) {
        const td = document.createElement('td')
        td.innerHTML = tableData[i][headCells[j]]
        tr.appendChild(td)
      }
      // 将每行数据挂载到 tbody 下
      tbody.appendChild(tr)
    }

    // 表格树: tableCon -> table -> thead/tbody -> tr -> td
    table.appendChild(thead)
    table.appendChild(tbody)
    tableCon.appendChild(table)
  </script>
</body>
</html>

这里的模拟数据也可以这样做一个 fake 数据生成, 方便后续的测试.

// 模拟后端传过来的数据
function genData(rows, columns) {
    var arr = []
    for (var i = 0; i < rows; i++) {
        var obj = {i}
        for (var j = 0; j < columns; j++) {
            obj[j] = j 
        }
        arr.push(obj)
    }
    return arr
}

// 生成一个 1000行, 30列的 对象数据 [ {}, {}, {}...]
var tableData = genData(10000, 30

表格分页

如果表格内容过多,我们可以添加分页功能,使用户能够浏览大量数据。

这要结合后端接口来实现, 这里我们可用通过数据行隐藏的方式来模拟分页的实现:

前端

  • pageNum: 前端传给后端, 要访问的页码
  • pageSise: 前端传给后端, 每页显示的数量

后端

  • totalCount: 假设数据是按照自增id存储的, 先算出总的数据量 totalCount , 假设是 101
  • totalPages: 根据前端传过来的每页数量 pageSize, 假设是 5, 则算出一共要分 ceil( 101 / 5 ) = 21 页
  • currentPageData: 从总表总截取当前页条数返回, 偏移位置为: pageSize * pageNum, 偏移量是 pageSize
select * from tb limit [offset], rows

-- 假设要获取第1页, 每页5行, 则:(1-1) * 5, 5; id 为 1, 2, 3, 4, 5
select * from tb limit 0, 5 

-- 假设要获取第4页, 每页4行, 则:(4-1) * 5, 5; id 为 16, 17, 18, 19
select * from tb limit 15, 5

数据的开始索引 startIndex = (pageNum -1) * pageSize,

数据的结束索引 endIndex = startIndex + pageSize

<div id="table-con"></div>
<div id="paginationContainer"></div>
// 表格数据渲染部分 ...

// 表格分页
    var pageSize = 10     // 每页显示行数
    var currentPage = 1  // 当前页码

    // 渲染某个页码的数据 (模拟接口就筛选出来和隐藏其他行)
    function displayOnePage(page) {
      // var table = document.getElementsByTagName('table')
      var rows = table.rows 
      var totalRows = rows.length - 1  // 减去表头行

      // 计算起始行索引 和 结束行索引
      var startIndex = (page - 1) * pageSize + 1
      var endIndex = Math.min(startIndex + pageSize - 1, totalRows)


      // 遍历表格行, 只显示当前页, 其余的隐藏 (真实环境是后端直接返回当前页哦)
      for (var i = 1; i <= totalRows; i++) {
        if ( i >= startIndex && i <= endIndex) {
          console.log(i);
          rows[i].style.display = ''
        } else {
          rows[i].style.display = 'none'
        }
      }
    }

    // 分页器
    function renderPagination() {
      // var table = document.getElementsByTagName('table')
      var rows = table.rows 
      var totalRows = rows.length -1

      var paginationContainer = document.getElementById('paginationContainer')
      paginationContainer.innerHTML = ''

      // 分页器页码数, 比如有 100行, 每页显示 10行, 则一共有10页
      var totalPages = Math.ceil(totalRows / pageSize)
      // 根据计算的页码数, 生成分页的按钮
      for (var i = 1; i <= totalPages; i++) {
        var btn = document.createElement('button')
        btn.textContent = i 
        // 点击哪页, 就用上面的 displayOnePage() 来显示页面数据
        btn.onclick = function () {
          currentPage = parseInt(this.textContent)
          displayOnePage(currentPage)
        }
        // 将创建的页码按钮进行挂载
        paginationContainer.appendChild(btn)
      }
    }

    // 监听加载事件
    document.addEventListener('DOMContentLoaded', function() {
      displayOnePage(currentPage)
    })

    renderPagination()

对于页码可以进行样式美化一下:

/* 分页器-按钮美化 */
#paginationContainer button {
    display: inline-block;
    margin-right: 10px;
    margin-top: 10px;
    padding: 10 20px;
    width: 40px;
    border: none;
    font-size: 16px;
    color: #fff;
    text-align: center;
    background-color: #008CBA;
}

表格字段点击排序

原理就是获取表格数据所有行组成的类数组 rows, 通过对数组的排序 sort (fn), 里面的函数根据用户点击的是哪一列来作为排序判断的依据.

  • 获取表格数据行数组 rows
  • 根据点击的哪个列进行 sort(function( td1, td2) )
  • tbody 的内容进行替换 (追加一样效果) tbody.appendChild(rows[i])
<script>
    var table = document.getElementById('myTable')
    // 获取表格字段列表
    var columnList = []
    var headers = table.getElementsByTagName('th')
    for (var i = 0; i < headers.length; i++) {
      columnList.push(headers[i].textContent)
    }

    function sortTable(colIndex) {
      // 获取表格数据行, 并将其copy转为一个数组, 主要是为了调用 sort 方法
      var tbody = table.tBodies[0]
      var rows = tbody.getElementsByTagName('tr')
      rows = Array.prototype.slice.call(rows)

      // 根据点击表头的 字段索引 进行比较 sort 
      var flag = false 
      rows.sort(function (row1, row2) {
        var a = row1.getElementsByTagName('td')[colIndex].textContent
        var b = row2.getElementsByTagName('td')[colIndex].textContent
        return a > b ? 1 : -1
      })
        
      for (var i = 0; i < rows.length; i++) {
        tbody.appendChild(rows[i])
      }
    }

    table.querySelector('thead').addEventListener('click', function(e) {
      var column = e.target.textContent
      var colIndex = columnList.indexOf(column)
      sortTable(colIndex)
    })
  </script>

表格模糊筛选

就是遍历单元格的的每行, 每列, 找到匹配项, 然后显示该行而已. 当然是模拟哈.

htmlCopy code
<input type="text" id="searchInput" onkeyup="filterTable()" placeholder="搜索...">

<script>
// 关键字筛选
    function filterTable() {
        var input = document.getElementById('searchInput')
        var keyword = input.value.toLowerCase() // 英文的话统一大小写

        var table = document.querySelector('table')
        var rows = table.getElementsByTagName("tr")  // 所有的表格行

        // 遍历每个单元格, 先行后列
        for (var i = 0; i < rows.length; i++) {
            // 获取一行内的所有列 cells
            var cells = rows[i].getElementsByTagName('td')

            var found = false 
            // 遍历每个格子进行查找
            for (var j = 0; j < cells.length; j++) {
                var cell = cells[j]
                if (cell) {
                    // 获取当前单元格的值 和 输入的 keyword 进行比对
                    var cellValue = cell.textContent || cell.innerText
                    if (cellValue.toLocaleLowerCase().indexOf(keyword) > -1) {
                        found = true
                        break;
                    }
                }
            }
            // 如果找到就显示这些行呗
            if (found) {
                rows[i].style.display = ""
            } else {
                rows[i].style.display = "none"
            }
        }
    }
</script>

数据响应式和打印样式

让我们使表格在不同屏幕尺寸下具有良好的显示效果,通过CSS媒体查询来实现响应式设计:

例如, 当屏幕宽度小于600px时,表格单元格的内边距和字体大小会变小,以适应小屏幕设备

为了让用户能够打印表格时获得最佳打印效果,我们可以添加打印样式, ctr + P 可以预览。

<style>
  /* 鼠标悬停效果 */
  tr:hover {
    background-color: #f5f5f5;
  }
  
  /* 响应式设计 */
  @media screen and (max-width: 600px) {
    th, td {
      padding: 6px;
      font-size: 12px;
    }
  }
  
  @media print {
    table {
      width: 100%;
      border-collapse: collapse;
    }
    th, td {
      border: 1px solid #000;
      padding: 8px;
      // 字体变红
      color: red;
    }
    caption {
      font-weight: bold;
    }
  }
</style>

在这个步骤中,我们使用了@media print媒体查询,定义了打印时表格的样式,使得打印出来的表格具有清晰的边框和适当的间距

导出CSV

对于简单的导出需求,如导出少量数据到CSV或JSON格式,前端可以实现。这通常通过创建一个隐藏的<a>标签,设置其href为包含数据的Blob URL,然后模拟点击这个标签来实现下载。

对于更复杂或需要特定格式的导出(如Excel、PDF等),后端通常更合适。后端可以处理数据的查询、转换和格式化,然后生成相应的文件供前端下载。

当数据量很大时,前端处理可能会导致性能问题或浏览器崩溃。在这种情况下,后端可以处理数据的分页、筛选和导出。后端还可以处理权限和安全性问题,确保只有授权的用户可以导出数据。

因此最佳的方案是, 前端传查询条件, 后端来导出.

  • 前端负责触发导出操作,并将必要的参数(如筛选条件、排序等)传递给后端。
  • 后端根据这些参数查询数据库,处理数据,并生成导出文件。然后,后端将文件发送给前端,前端负责显示下载链接或触发下载操作。
  • 解决中文乱码问题: blob = new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type });
// 前端导出csv
    function exportTableToCSV() {
      var csv = '';
      var rows = document.querySelectorAll('table tr');
      
      for (var i = 0; i < rows.length; i++) {
        var row = rows[i];
        var rowArray = [];

        for (var j = 0; j < row.cells.length; j++) {
          var cell = row.cells[j];
          rowArray.push(cell.textContent.replace(/(\r\n|\n|\r)/gm, ''));
        }
        csv += rowArray.join(',') + '\n';
      }

      var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
      // 专门来处理中文问题, 强制编码为 unicode
      blob = new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type });

      var url = URL.createObjectURL(blob);
      var a = document.createElement('a');
      a.href = url;
      a.setAttribute('download', 'table.csv');
      a.click();
    }

基本常用的一些功能就这些了, 对于表格, 主要我是搞数据为主, 因此核心是能显示数据, 数据排序, 检索数据 和下载, 基本就行了. 其实根本就可以不用各类 ui 组件, 原生的就行啦.

posted @ 2024-05-04 14:37  致于数据科学家的小陈  阅读(336)  评论(0编辑  收藏  举报