LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

使用 HTML + JavaScript 实现可编辑表格(附完整代码)

admin
2025年12月24日 11:26 本文热度 988
可编辑表格是数据管理系统中的重要组件,它将数据展示与编辑功能融为一体,使用户能够直接在表格界面中修改数据内容。通过纯前端技术实现的可编辑表格,无需复杂的后端支持即可提供流畅的数据编辑体验,特别适用于数据录入、修改等场景。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现一个可编辑表格。

效果演示

本系统采用简洁的三段式布局:顶部为表格标题区域,中间为主要的表格编辑区域,底部为状态栏区域。用户可以直接在表格单元格中编辑数据,通过键盘快捷键进行导航,实时查看数据变化状态。

页面结构

页面包含三个主要区域:表格头部、表格编辑区域和状态栏。

表格编辑区域

表格编辑区域是整个应用的核心,包含一个可滚动的表格,表格中的每个单元格都支持直接编辑。
<div class="table-wrapper" id="tableWrapper">  <table class="data-table" id="dataTable">    <thead>    <tr>      <th data-column="id" style="width: 80px;">ID</th>      <th data-column="name" style="width: 100px;">姓名</th>      <th data-column="email" style="width: 180px;">邮箱</th>      <th data-column="phone" style="width: 120px;">电话</th>      <th data-column="department" style="width: 100px;">部门</th>      <th data-column="salary" style="width: 100px;">薪资</th>      <th data-column="status" style="width: 80px;">状态</th>    </tr>    </thead>    <tbody id="tableBody"></tbody>  </table></div>

状态栏区域

状态栏区域显示表格统计信息、编辑模式提示和键盘快捷键说明。
<div class="status-bar">  <div class="nav-info">    <span id="recordInfo">共 0 条记录</span>    <span>编辑模式</span>  </div>  <div class="status-message" id="statusMessage"></div>  <div class="shortcuts">    <span class="shortcut">Tab</span> 下一个    <span class="shortcut">↑↓</span> 上下导航  </div></div>

核心功能实现

定义全局变量

originalData 用于保存表格的初始数据,currentData 用于存储当前表格的实时数据,selectedRows 用于跟踪当前选中的行。
let originalData = [  {id1name'张三'email'zhangsan@example.com'phone'13800138000'department'技术部'salary15000status'在职'},  {id2name'李四'email'lisi@example.com'phone'13900139000'department'销售部'salary12000status'在职'},  // ...];
let currentData = [...originalData];let selectedRows = new Set();

渲染表格

renderTable() 函数负责根据 currentData 中的数据动态生成表格界面,每个单元格都包含一个输入框或选择框,支持直接编辑。
function renderTable() {  const tbody = document.getElementById('tableBody');  tbody.innerHTML = '';
  currentData.forEach((row, rowIndex) => {    const tr = document.createElement('tr');    tr.dataset.rowIndex = rowIndex;    if (selectedRows.has(rowIndex)) tr.classList.add('selected');
    const columns = [      { key'id'cls'id-input'input'text' },      { key'name'cls''input'text' },      { key'email'cls''input'text' },      { key'phone'cls''input'text' },      { key'department'cls''input'text' },      { key'salary'cls'number-input'input'text' }    ];
    columns.forEach(col => {      const td = document.createElement('td');      td.innerHTML = `<input type="${col.input}" class="always-edit ${col.cls}" value="${row[col.key]}" onchange="updateCell(${rowIndex}, '${col.key}', this.value)" onfocus="selectCell(${rowIndex}, '${col.key}', this.value)">`;      tr.appendChild(td);    });    // 状态列    const statusTd = document.createElement('td');    const statusOptions = ['在职''离职'];    statusTd.innerHTML = `<select class="status-select" onchange="updateCell(${rowIndex}, 'status', this.value)" onfocus="selectCell(${rowIndex}, 'status', this.value)">                ${statusOptions.map(option => `<option value="${option}${row.status === option ? 'selected' : ''}>${option}</option>`).join('')}              </select>`;    tr.appendChild(statusTd);
    tbody.appendChild(tr);  });}

更新单元格数据

updateCell() 函数处理单元格数据更新,包括数据验证和状态提示。
function updateCell(rowIndex, column, value) {  const originalValue = currentData[rowIndex][column];  if (column === 'id' || column === 'salary') value = parseInt(value) || 0;
  if (value !== originalValue) {    if (column === 'id') {      const newId = parseInt(value);      const existingIds = currentData.map(row => row.id).filter((id, index) => index !== rowIndex);      if (existingIds.includes(newId)) {        showStatusMessage('错误:ID已存在!''error');        renderTable();        return;      }    }    currentData[rowIndex][column] = value;    const rowId = currentData[rowIndex].id;    showStatusMessage(`ID ${rowId}: 已更新 ${column} = ${value}`'success');  }}

键盘导航功能

系统实现了完整的键盘导航功能,支持 Tab 键、方向键和 Ctrl+S 快捷键。

document.addEventListener('keydown'function(event) {  if ((event.ctrlKey || event.metaKey) && event.key === 's') {    // Ctrl+S 保存    event.preventDefault();    if (window.currentEditRow !== undefined &&      window.currentEditColumn !== undefined &&      window.currentEditValue !== undefined) {      const activeElement = document.activeElement;      const newValue = activeElement.value;      updateCell(window.currentEditRowwindow.currentEditColumn, newValue);    }  } else if (['ArrowUp''ArrowDown''Tab'].includes(event.key)) {    // 键盘导航    if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'SELECT') {      event.preventDefault();      if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {        handleArrowNavigation(event.key);      } else if (event.key === 'Tab') {        handleTabNavigation(event.shiftKey);      }    }  }});

扩展建议

  • 数据持久化:增加保存和加载功能,将表格数据保存到本地存储或服务器

  • 数据导入导出:支持从CSV、Excel文件导入数据,或将表格数据导出为多种格式

  • 批量操作:支持多选行进行批量编辑、删除等操作

  • 排序和筛选:增加列排序和数据筛选功能

  • 撤销重做:实现编辑历史记录,支持撤销和重做操作

完整代码

git地址:https://gitee.com/ironpro/hjdemo/blob/master/table-edit/index.html
<!DOCTYPE html><html lang="zh-CN"><head>  <meta charset="utf-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>可编辑表格</title>  <style>      * {          margin0;          padding0;          box-sizing: border-box;      }      body {          background-color#f5f7fa;          min-height100vh;          padding20px;          overflow: hidden;      }      .container {          max-width1400px;          margin0 auto;          background: white;          border-radius15px;          box-shadow0 20px 40px rgba(0000.1);          heightcalc(100vh - 40px);          display: flex;          flex-direction: column;      }      .header {          background#ffffff;          color#333;          padding15px 20px;          display: flex;          justify-content: space-between;          align-items: center;          flex-shrink0;          border-bottom1px solid #e1e5eb;      }      .header h1 {          font-size18px;          font-weight500;      }      .table-wrapper {          flex1;          overflow: auto;          position: relative;          padding-bottom5px;      }      .table-wrapper::-webkit-scrollbar {          width6px;          height6px;      }      .table-wrapper::-webkit-scrollbar-track {          background#f1f5f9;      }      .table-wrapper::-webkit-scrollbar-thumb {          background#cbd5e1;          border-radius3px;      }      .table-wrapper::-webkit-scrollbar-thumb:hover {          background#94a3b8;      }      .data-table {          width100%;          border-collapse: collapse;          font-size13px;          table-layout: fixed;      }      .data-table thead {          position: sticky;          top0;          z-index10;      }      .data-table thead::after {          content"";          position: absolute;          left0;          right0;          bottom0;          height1px;          background#d1d5db;          z-index11;      }      .data-table th {          background#f8fafc;          color#374151;          padding14px 10px;          text-align: left;          font-weight500;          cursor: pointer;          user-select: none;          white-space: nowrap;          overflow: hidden;          text-overflow: ellipsis;          border-right1px solid #d1d5db;      }      .data-table th:hover {          background#f1f5f9;      }      .data-table td {          padding0;          border1px solid #d1d5db;          border-top: none;          border-left: none;          position: relative;          height40px;          overflow: hidden;      }      .data-table tbody tr:last-child td {          border-bottom1px solid #d1d5db;      }      .data-table th:last-child.data-table td:last-child {          border-right: none;      }      .data-table tr:nth-child(even) {          background#f9fafb;      }      .data-table tr:hover {          background#f1f5f9 !important;      }      .data-table tr.selected {          background#dbeafe !important;      }      .always-edit {          width100%;          height100%;          border: none;          padding10px;          font-size13px;          font-family: inherit;          background: transparent;          outline: none;          cursor: text;      }      .always-edit:focus {          background: white;          box-shadow: inset 0 0 0 1px #3b82f6;          z-index5;          position: relative;      }      .id-input {          text-align: center;          font-weight500;          color#4b5563;      }      .status-active {          color#10b981;          font-weight500;      }      .status-inactive {          color#ef4444;          font-weight500;      }      .status-select {          width100%;          height100%;          border: none;          padding10px;          font-size13px;          font-family: inherit;          background: transparent;          outline: none;          cursor: pointer;      }      .status-select:focus {          background: white;          box-shadow: inset 0 0 0 1px #3b82f6;      }      .number-input {          text-align: right;      }      .status-bar {          background#f8fafc;          padding10px 20px;          border-top1px solid #e1e5eb;          display: flex;          justify-content: space-between;          align-items: center;          font-size14px;          color#64748b;          flex-shrink0;      }      .nav-info {          display: flex;          gap15px;          align-items: center;      }      .shortcuts {          display: flex;          gap10px;      }      .shortcut {          background#e2e8f0;          padding3px 7px;          border-radius3px;          font-size11px;          font-family: monospace;      }      .status-message {          flex1;          margin0 20px;          color#3b82f6;          font-weight500;          transition: opacity 0.3s;      }      .status-message.success {          color#10b981;      }      .status-message.error {          color#ef4444;      }  </style></head><body><div class="container">  <div class="header">    <h1>可编辑表格</h1>  </div>  <div class="table-wrapper" id="tableWrapper">    <table class="data-table" id="dataTable">      <thead>      <tr>        <th data-column="id" style="width: 80px;">ID</th>        <th data-column="name" style="width: 100px;">姓名</th>        <th data-column="email" style="width: 180px;">邮箱</th>        <th data-column="phone" style="width: 120px;">电话</th>        <th data-column="department" style="width: 100px;">部门</th>        <th data-column="salary" style="width: 100px;">薪资</th>        <th data-column="status" style="width: 80px;">状态</th>      </tr>      </thead>      <tbody id="tableBody"></tbody>    </table>  </div>
  <div class="status-bar">    <div class="nav-info">      <span id="recordInfo">共 0 条记录</span>      <span>编辑模式</span>    </div>    <div class="status-message" id="statusMessage"></div>    <div class="shortcuts">      <span class="shortcut">Tab</span> 下一个      <span class="shortcut">↑↓</span> 上下导航    </div>  </div></div>
<script>  let originalData = [    {id1name'张三'email'zhangsan@example.com'phone'13800138000'department'技术部'salary15000status'在职'},    {id2name'李四'email'lisi@example.com'phone'13900139000'department'销售部'salary12000status'在职'},    // ...  ];
  let currentData = [...originalData];  let selectedRows = new Set();
  function renderTable() {    const tbody = document.getElementById('tableBody');    tbody.innerHTML = '';
    currentData.forEach((row, rowIndex) => {      const tr = document.createElement('tr');      tr.dataset.rowIndex = rowIndex;      if (selectedRows.has(rowIndex)) tr.classList.add('selected');
      const columns = [        { key'id'cls'id-input'input'text' },        { key'name'cls''input'text' },        { key'email'cls''input'text' },        { key'phone'cls''input'text' },        { key'department'cls''input'text' },        { key'salary'cls'number-input'input'text' }      ];
      columns.forEach(col => {        const td = document.createElement('td');        td.innerHTML = `<input type="${col.input}" class="always-edit ${col.cls}" value="${row[col.key]}"                  onchange="updateCell(${rowIndex}, '${col.key}', this.value)"                  onfocus="selectCell(${rowIndex}, '${col.key}', this.value)">`;        tr.appendChild(td);      });      // 状态列      const statusTd = document.createElement('td');      const statusOptions = ['在职''离职'];      statusTd.innerHTML = `<select class="status-select" onchange="updateCell(${rowIndex}, 'status', this.value)" onfocus="selectCell(${rowIndex}, 'status', this.value)">                  ${statusOptions.map(option => `<option value="${option}${row.status === option ? 'selected' : ''}>${option}</option>`).join('')}                </select>`;      tr.appendChild(statusTd);      tbody.appendChild(tr);    });  }
  function updateCell(rowIndex, column, value) {    const originalValue = currentData[rowIndex][column];    if (column === 'id' || column === 'salary') value = parseInt(value) || 0;
    if (value !== originalValue) {      if (column === 'id') {        const newId = parseInt(value);        const existingIds = currentData.map(row => row.id).filter((id, index) => index !== rowIndex);        if (existingIds.includes(newId)) {          showStatusMessage('错误:ID已存在!''error');          renderTable();          return;        }      }      currentData[rowIndex][column] = value;      const rowId = currentData[rowIndex].id;      showStatusMessage(`ID ${rowId}: 已更新 ${column} = ${value}`'success');    }  }
  function selectCell(rowIndex, column, value) {    selectRow(rowIndex);    window.currentEditColumn = column;    window.currentEditValue = value;  }
  function selectRow(rowIndex) {    document.querySelectorAll('tr').forEach(tr => tr.classList.remove('selected'));    selectedRows.clear();    selectedRows.add(rowIndex);    const tr = document.querySelector(`tr[data-row-index="${rowIndex}"]`);    if (tr) tr.classList.add('selected');    window.currentEditRow = rowIndex;  }
  function updateRecordInfo() {    document.getElementById('recordInfo').textContent = `共 ${currentData.length} 条记录`;  }
  function showStatusMessage(message, type = 'info') {    const statusMessageEl = document.getElementById('statusMessage');    statusMessageEl.textContent = message;    statusMessageEl.className = `status-message ${type}`;    setTimeout(() => {      if (statusMessageEl.textContent === message) {        statusMessageEl.textContent = '';        statusMessageEl.className = 'status-message';      }    }, 5000);  }  // 键盘事件处理  document.addEventListener('keydown'function(event) {    if ((event.ctrlKey || event.metaKey) && event.key === 's') {      // Ctrl+S 保存      event.preventDefault();      if (window.currentEditRow !== undefined &&        window.currentEditColumn !== undefined &&        window.currentEditValue !== undefined) {        const activeElement = document.activeElement;        const newValue = activeElement.value;        updateCell(window.currentEditRowwindow.currentEditColumn, newValue);      }    } else if (['ArrowUp''ArrowDown''Tab'].includes(event.key)) {      // 键盘导航      if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'SELECT') {        event.preventDefault();        if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {          handleArrowNavigation(event.key);        } else if (event.key === 'Tab') {          handleTabNavigation(event.shiftKey);        }      }    }  });  // Tab键导航  function handleTabNavigation(isShiftKey) {    if (window.currentEditRow === undefined || window.currentEditColumn === undefinedreturn;    const currentRow = window.currentEditRow;    const currentColumn = window.currentEditColumn;    const totalRows = currentData.length;    const totalColumns = 7;    const columnOrder = ['id''name''email''phone''department''salary''status'];    const currentColumnIndex = columnOrder.indexOf(currentColumn);
    let nextRow = currentRow;    let nextColumnIndex = currentColumnIndex;    if (isShiftKey) { // Shift+Tab: 向前导航      nextColumnIndex--;      if (nextColumnIndex < 0) {        nextRow--;        if (nextRow < 0) nextRow = totalRows - 1;        nextColumnIndex = totalColumns - 1;      }    } else { // Tab: 向后导航      nextColumnIndex++;      if (nextColumnIndex >= totalColumns) {        nextRow++;        if (nextRow >= totalRows) nextRow = 0;        nextColumnIndex = 0;      }    }    const nextColumn = columnOrder[nextColumnIndex];    focusCell(nextRow, nextColumn);  }  // 上下箭头导航  function handleArrowNavigation(direction) {    if (window.currentEditRow === undefined || window.currentEditColumn === undefinedreturn;    const currentRow = window.currentEditRow;    let newRow = currentRow;    const totalRows = currentData.length;    if (direction === 'ArrowUp' && currentRow > 0) {      newRow = currentRow - 1;    } else if (direction === 'ArrowDown' && currentRow < totalRows - 1) {      newRow = currentRow + 1;    }    if (newRow !== currentRow) {      focusCell(newRow, window.currentEditColumn);    }  }  // 聚焦到指定单元格  function focusCell(row, column) {    window.currentEditRow = row;    window.currentEditColumn = column;    selectRow(row);    const tr = document.querySelector(`tr[data-row-index="${row}"]`);    if (tr) {      const columnOrder = ['id''name''email''phone''department''salary''status'];      const columnIndex = columnOrder.indexOf(column);      const inputs = tr.querySelectorAll('input, select');      if (inputs[columnIndex]) {        inputs[columnIndex].focus();        if (inputs[columnIndex].tagName === 'INPUT' || inputs[columnIndex].tagName === 'TEXTAREA') {          inputs[columnIndex].select();        }        ensureElementVisible(inputs[columnIndex]);      }    }  }  // 确保元素在视窗中可见  function ensureElementVisible(element) {    const tableWrapper = document.getElementById('tableWrapper');    const rect = element.getBoundingClientRect();    const wrapperRect = tableWrapper.getBoundingClientRect();    // 获取表头高度(考虑sticky属性)    const headerHeight = document.querySelector('.data-table thead').offsetHeight;    if (rect.bottom > wrapperRect.bottom) {      const scrollAmount = rect.bottom - wrapperRect.bottom;      tableWrapper.scrollTop += scrollAmount + 10;    } else if (rect.top < wrapperRect.top + headerHeight) {      // 向上滚动时考虑表头高度      const scrollAmount = (wrapperRect.top + headerHeight) - rect.top;      tableWrapper.scrollTop -= scrollAmount + 10;    }  }  document.addEventListener('DOMContentLoaded'function() {    renderTable();    updateRecordInfo();  });</script></body></html>


阅读原文:原文链接


该文章在 2025/12/24 12:21:18 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved