可编辑表格是数据管理系统中的重要组件,它将数据展示与编辑功能融为一体,使用户能够直接在表格界面中修改数据内容。通过纯前端技术实现的可编辑表格,无需复杂的后端支持即可提供流畅的数据编辑体验,特别适用于数据录入、修改等场景。本文将介绍如何使用 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 = [ {id: 1, name: '张三', email: 'zhangsan@example.com', phone: '13800138000', department: '技术部', salary: 15000, status: '在职'}, {id: 2, name: '李四', email: 'lisi@example.com', phone: '13900139000', department: '销售部', salary: 12000, status: '在职'}, ];
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') { 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); } } 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); } } }});
扩展建议
完整代码
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> * { 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: 14px 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: 14px; 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><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 = [ {id: 1, name: '张三', email: 'zhangsan@example.com', phone: '13800138000', department: '技术部', salary: 15000, status: '在职'}, {id: 2, name: '李四', email: 'lisi@example.com', phone: '13900139000', department: '销售部', salary: 12000, status: '在职'}, ];
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') { 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); } } 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); } } } }); function handleTabNavigation(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) { nextColumnIndex--; if (nextColumnIndex < 0) { nextRow--; if (nextRow < 0) nextRow = totalRows - 1; nextColumnIndex = totalColumns - 1; } } else { 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 === undefined) return; 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(); 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 编辑过