使用 HTML + JavaScript 实现可编辑表格
文章目录
一、可编辑表格
可编辑表格是数据管理系统中的重要组件,它将数据展示与编辑功能融为一体,使用户能够直接在表格界面中修改数据内容。通过纯前端技术实现的可编辑表格,无需复杂的后端支持即可提供流畅的数据编辑体验,特别适用于数据录入、修改等场景。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现一个可编辑表格。
二、效果演示
本系统采用简洁的三段式布局:顶部为表格标题区域,中间为主要的表格编辑区域,底部为状态栏区域。用户可以直接在表格单元格中编辑数据,通过键盘快捷键进行导航,实时查看数据变化状态。
三、系统分析
1、页面结构
页面包含三个主要区域:表格头部、表格编辑区域和状态栏。
1.1、表格编辑区域
表格编辑区域是整个应用的核心,包含一个可滚动的表格,表格中的每个单元格都支持直接编辑。
<divclass="table-wrapper"id="tableWrapper"><tableclass="data-table"id="dataTable"><thead><tr><thdata-column="id"style="width: 80px;">ID</th><thdata-column="name"style="width: 100px;">姓名</th><thdata-column="email"style="width: 180px;">邮箱</th><thdata-column="phone"style="width: 120px;">电话</th><thdata-column="department"style="width: 100px;">部门</th><thdata-column="salary"style="width: 100px;">薪资</th><thdata-column="status"style="width: 80px;">状态</th></tr></thead><tbodyid="tableBody"></tbody></table></div>1.2、状态栏区域
状态栏区域显示表格统计信息、编辑模式提示和键盘快捷键说明。
<divclass="status-bar"><divclass="nav-info"><spanid="recordInfo">共 0 条记录</span><span>编辑模式</span></div><divclass="status-message"id="statusMessage"></div><divclass="shortcuts"><spanclass="shortcut">Tab</span> 下一个 <spanclass="shortcut">↑↓</span> 上下导航 </div></div>2、核心功能实现
2.1定义全局变量
originalData 用于保存表格的初始数据,currentData 用于存储当前表格的实时数据,selectedRows 用于跟踪当前选中的行。
let originalData =[{id:1,name:'张三',email:'[email protected]',phone:'13800138000',department:'技术部',salary:15000,status:'在职'},{id:2,name:'李四',email:'[email protected]',phone:'13900139000',department:'销售部',salary:12000,status:'在职'},// ...];let currentData =[...originalData];let selectedRows =newSet();2.2渲染表格
renderTable() 函数负责根据 currentData 中的数据动态生成表格界面,每个单元格都包含一个输入框或选择框,支持直接编辑。
functionrenderTable(){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}"token interpolation">${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 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);});}2.3更新单元格数据
updateCell() 函数处理单元格数据更新,包括数据验证和状态提示。
functionupdateCell(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');}}2.4键盘导航功能
系统实现了完整的键盘导航功能,支持 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.currentEditRow, window.currentEditColumn, newValue);}}elseif(['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);}elseif(event.key ==='Tab'){handleTabNavigation(event.shiftKey);}}}});四、扩展建议
- 数据持久化:增加保存和加载功能,将表格数据保存到本地存储或服务器
- 数据导入导出:支持从CSV、Excel文件导入数据,或将表格数据导出为多种格式
- 批量操作:支持多选行进行批量编辑、删除等操作
- 排序和筛选:增加列排序和数据筛选功能
- 撤销重做:实现编辑历史记录,支持撤销和重做操作
五、完整代码
git地址:https://gitee.com/ironpro/hjdemo/blob/master/table-edit/index.html
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>可编辑表格</title><style>*{margin: 0;padding: 0;box-sizing: border-box;}body{background-color: #f5f7fa;min-height: 100vh;padding: 20px;overflow: hidden;}.container{max-width: 1400px;margin: 0 auto;background: white;border-radius: 15px;box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);height:calc(100vh - 40px);display: flex;flex-direction: column;}.header{background: #ffffff;color: #333;padding: 15px 20px;display: flex;justify-content: space-between;align-items: center;flex-shrink: 0;border-bottom: 1px solid #e1e5eb;}.header h1{font-size: 18px;font-weight: 500;}.table-wrapper{flex: 1;overflow: auto;position: relative;padding-bottom: 5px;}.table-wrapper::-webkit-scrollbar{width: 6px;height: 6px;}.table-wrapper::-webkit-scrollbar-track{background: #f1f5f9;}.table-wrapper::-webkit-scrollbar-thumb{background: #cbd5e1;border-radius: 3px;}.table-wrapper::-webkit-scrollbar-thumb:hover{background: #94a3b8;}.data-table{width: 100%;border-collapse: collapse;font-size: 13px;table-layout: fixed;}.data-table thead{position: sticky;top: 0;z-index: 10;}.data-table thead::after{content:"";position: absolute;left: 0;right: 0;bottom: 0;height: 1px;background: #d1d5db;z-index: 11;}.data-table th{background: #f8fafc;color: #374151;padding: 12px 10px;text-align: left;font-weight: 500;cursor: pointer;user-select: none;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;border-right: 1px solid #d1d5db;}.data-table th:hover{background: #f1f5f9;}.data-table td{padding: 0;border: 1px solid #d1d5db;border-top: none;border-left: none;position: relative;height: 40px;overflow: hidden;}.data-table tbody tr:last-child td{border-bottom: 1px 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{width: 100%;height: 100%;border: none;padding: 10px;font-size: 13px;font-family: inherit;background: transparent;outline: none;cursor: text;}.always-edit:focus{background: white;box-shadow: inset 0 0 0 1px #3b82f6;z-index: 5;position: relative;}.id-input{text-align: center;font-weight: 500;color: #4b5563;}.status-active{color: #10b981;font-weight: 500;}.status-inactive{color: #ef4444;font-weight: 500;}.status-select{width: 100%;height: 100%;border: none;padding: 10px;font-size: 13px;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;padding: 10px 20px;border-top: 1px solid #e1e5eb;display: flex;justify-content: space-between;align-items: center;font-size: 12px;color: #64748b;flex-shrink: 0;}.nav-info{display: flex;gap: 15px;align-items: center;}.shortcuts{display: flex;gap: 10px;}.shortcut{background: #e2e8f0;padding: 3px 7px;border-radius: 3px;font-size: 11px;font-family: monospace;}.status-message{flex: 1;margin: 0 20px;color: #3b82f6;font-weight: 500;transition: opacity 0.3s;}.status-message.success{color: #10b981;}.status-message.error{color: #ef4444;}</style></head><body><divclass="container"><divclass="header"><h1>可编辑表格</h1></div><divclass="table-wrapper"id="tableWrapper"><tableclass="data-table"id="dataTable"><thead><tr><thdata-column="id"style="width: 80px;">ID</th><thdata-column="name"style="width: 100px;">姓名</th><thdata-column="email"style="width: 180px;">邮箱</th><thdata-column="phone"style="width: 120px;">电话</th><thdata-column="department"style="width: 100px;">部门</th><thdata-column="salary"style="width: 100px;">薪资</th><thdata-column="status"style="width: 80px;">状态</th></tr></thead><tbodyid="tableBody"></tbody></table></div><divclass="status-bar"><divclass="nav-info"><spanid="recordInfo">共 0 条记录</span><span>编辑模式</span></div><divclass="status-message"id="statusMessage"></div><divclass="shortcuts"><spanclass="shortcut">Tab</span> 下一个 <spanclass="shortcut">↑↓</span> 上下导航 </div></div></div><script>let originalData =[{id:1,name:'张三',email:'[email protected]',phone:'13800138000',department:'技术部',salary:15000,status:'在职'},{id:2,name:'李四',email:'[email protected]',phone:'13900139000',department:'销售部',salary:12000,status:'在职'},// ...];let currentData =[...originalData];let selectedRows =newSet();functionrenderTable(){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}"token interpolation">${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 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);});}functionupdateCell(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');}}functionselectCell(rowIndex, column, value){selectRow(rowIndex); window.currentEditColumn = column; window.currentEditValue = value;}functionselectRow(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;}functionupdateRecordInfo(){ document.getElementById('recordInfo').textContent =`共 ${currentData.length} 条记录`;}functionshowStatusMessage(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.currentEditRow, window.currentEditColumn, newValue);}}elseif(['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);}elseif(event.key ==='Tab'){handleTabNavigation(event.shiftKey);}}}});// Tab键导航functionhandleTabNavigation(isShiftKey){if(window.currentEditRow ===undefined|| window.currentEditColumn ===undefined)return;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);}// 上下箭头导航functionhandleArrowNavigation(direction){if(window.currentEditRow ===undefined|| window.currentEditColumn ===undefined)return;const currentRow = window.currentEditRow;let newRow = currentRow;const totalRows = currentData.length;if(direction ==='ArrowUp'&& currentRow >0){ newRow = currentRow -1;}elseif(direction ==='ArrowDown'&& currentRow < totalRows -1){ newRow = currentRow +1;}if(newRow !== currentRow){focusCell(newRow, window.currentEditColumn);}}// 聚焦到指定单元格functionfocusCell(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]);}}}// 确保元素在视窗中可见functionensureElementVisible(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;}elseif(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>