使用HTML+JavaScript實現(xiàn)可編輯表格的方法
一、可編輯表格
可編輯表格是數(shù)據(jù)管理系統(tǒng)中的重要組件,它將數(shù)據(jù)展示與編輯功能融為一體,使用戶能夠直接在表格界面中修改數(shù)據(jù)內容。通過純前端技術實現(xiàn)的可編輯表格,無需復雜的后端支持即可提供流暢的數(shù)據(jù)編輯體驗,特別適用于數(shù)據(jù)錄入、修改等場景。本文將介紹如何使用 HTML、CSS 和 JavaScript 實現(xiàn)一個可編輯表格。
二、效果演示
本系統(tǒng)采用簡潔的三段式布局:頂部為表格標題區(qū)域,中間為主要的表格編輯區(qū)域,底部為狀態(tài)欄區(qū)域。用戶可以直接在表格單元格中編輯數(shù)據(jù),通過鍵盤快捷鍵進行導航,實時查看數(shù)據(jù)變化狀態(tài)。

三、系統(tǒng)分析
1、頁面結構
頁面包含三個主要區(qū)域:表格頭部、表格編輯區(qū)域和狀態(tài)欄。
1.1、表格編輯區(qū)域
表格編輯區(qū)域是整個應用的核心,包含一個可滾動的表格,表格中的每個單元格都支持直接編輯。
<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;">狀態(tài)</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
1.2、狀態(tài)欄區(qū)域
狀態(tài)欄區(qū)域顯示表格統(tǒng)計信息、編輯模式提示和鍵盤快捷鍵說明。
<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>
2、核心功能實現(xiàn)
2.1定義全局變量
originalData 用于保存表格的初始數(shù)據(jù),currentData 用于存儲當前表格的實時數(shù)據(jù),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();
2.2渲染表格
renderTable() 函數(shù)負責根據(jù) currentData 中的數(shù)據(jù)動態(tài)生成表格界面,每個單元格都包含一個輸入框或選擇框,支持直接編輯。
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);
});
// 狀態(tài)列
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);
});
}
2.3更新單元格數(shù)據(jù)
updateCell() 函數(shù)處理單元格數(shù)據(jù)更新,包括數(shù)據(jù)驗證和狀態(tài)提示。
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');
}
}
2.4鍵盤導航功能
系統(tǒng)實現(xiàn)了完整的鍵盤導航功能,支持 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);
}
} 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);
}
}
}
});
四、擴展建議
- 數(shù)據(jù)持久化:增加保存和加載功能,將表格數(shù)據(jù)保存到本地存儲或服務器
- 數(shù)據(jù)導入導出:支持從CSV、Excel文件導入數(shù)據(jù),或將表格數(shù)據(jù)導出為多種格式
- 批量操作:支持多選行進行批量編輯、刪除等操作
- 排序和篩選:增加列排序和數(shù)據(jù)篩選功能
- 撤銷重做:實現(xiàn)編輯歷史記錄,支持撤銷和重做操作
五、完整代碼
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: 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>
<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;">狀態(tài)</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);
});
// 狀態(tài)列
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.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);
}
}
}
});
// Tab鍵導航
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) { // 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 === 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();
// 獲取表頭高度(考慮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>
以上就是使用HTML+JavaScript實現(xiàn)可編輯表格的方法的詳細內容,更多關于HTML+JS可編輯表格的資料請關注腳本之家其它相關文章!
相關文章
javascript簡單實現(xiàn)等比例縮小圖片的方法
這篇文章主要介紹了javascript簡單實現(xiàn)等比例縮小圖片的方法,涉及javascript針對頁面元素屬性的讀取、運算及設置相關技巧,需要的朋友可以參考下2016-07-07

