一、需求描述
(1)表格嵌套展示
(2)父表格每一行有编辑操作,点击编辑后表格内容从展示状态变为可编辑状态;点击取消后能恢复初始值
(3)表格左侧、顶端、右侧实现粘性布局
二、实现效果
漂亮的表格 | 菜鸟工具 --基于这个表格的样式,也可粘贴下方的代码到此网站看效果
(1)展示状态
(2)编辑状态
(3)确定提交,传递的参数
(4)粘性布局
三、实现代码
(1)HTML
<table width="100%" border="0" cellspacing="1" cellpadding="4" class="tb-father">
<caption>
<p class="tb-caption">表格标题</p>
<p class="tb-remark text-right">2017-01-02---2017-05-02</p>
</caption>
<thead>
<tr>
<th class="titfont width60" rowspan="2">表头1</th>
<th class="titfont width100" rowspan="2">表头2</th>
<th class="titfont width60" rowspan="2">表头3</th>
<th class="titfont width80" rowspan="2">表头4</th>
<th class="titfont width80" rowspan="2">表头5</th>
<th class="titfont width60" rowspan="2">表头6</th>
<th class="titfont width60" rowspan="2">表头7</th>
<th class="titfont width80" rowspan="2">表头8</th>
<th class="titfont width100" rowspan="2">表头9</th>
<th class="titfont width100" rowspan="2">表头10</th>
<th class="titfont width60" rowspan="2">操作</th>
</tr>
</thead>
<tbody>
<!-- for循环展示,设置每行id为row_返回数据唯一标识 -->
<tr id="row_key">
<td class="width60">
<div class="showVal">合并内容1</div>
<div class="changeVal">
<!-- 修改数据:input -->
<input type="text" data-submit-prefix="fatherObj" data-id="name1" placeholder="表头1" value="合并内容1" />
<!-- 记录原有数据,点击取消能复原;data-id用于区分原始值元素和需提交元素 -->
<input type="hidden" data-id="name1Val" value="合并内容1" />
</div>
</td>
<td class="width100">
<div class="showVal">合并内容2</div>
<div class="changeVal">
<!-- 修改数据:textarea -->
<textarea type="text" data-submit-prefix="fatherObj" data-id="name2" placeholder="表头2">合并内容2</textarea>
<!-- 记录原有数据,点击取消能复原 -->
<input type="hidden" data-id="name2Val" value="合并内容2" />
</div>
</td>
<td class="width60">
<div class="showVal">
<label><input type="radio" value="1" checked disabled />是</label><br />
<label><input type="radio" value="0" disabled />否</label>
</div>
<div class="changeVal">
<!-- 修改数据:radio,由于同一name的radio只能选其一的特殊性,需要加上唯一标识符 -->
<label><input type="radio" data-submit-prefix="fatherObj" data-id="name3" name="row_key_fatherObj.name3" value="1" checked />是</label><br />
<label><input type="radio" data-submit-prefix="fatherObj" data-id="name3" name="row_key_fatherObj.name3" value="0" />否</label>
<!-- 记录原有数据,点击取消能复原 -->
<input type="hidden" data-id="name3Val" value="1" />
</div>
</td>
<td class="width80">
<div class="showVal">
<label><input type="checkbox" value="1" checked disabled />多选1</label><br />
<label><input type="checkbox" value="2" checked disabled />多选2</label><br />
<label><input type="checkbox" value="3" disabled />多选3</label>
</div>
<div class="changeVal">
<!-- 修改数据:checkbox -->
<label><input type="checkbox" data-submit-prefix="fatherObj" data-id="name4" value="1" checked />多选1</label><br />
<label><input type="checkbox" data-submit-prefix="fatherObj" data-id="name4" value="2" checked />多选2</label><br />
<label><input type="checkbox" data-submit-prefix="fatherObj" data-id="name4" value="3" />多选3</label>
<!-- 记录原有数据,点击取消能复原 -->
<input type="hidden" data-id="name4Val" value="1,2" />
</div>
</td>
<td class="width80">
<div class="showVal">合并内容5</div>
<div class="changeVal">
<!-- 修改数据:select -->
<select data-submit-prefix="fatherObj" data-id="name5">
<option value="">请选择</option>
<option value="1" selected>合并内容5</option>
<option value="2">选择2</option>
<option value="3">选择3</option>
</select>
<!-- 记录原有数据,点击取消能复原 -->
<input type="hidden" data-id="name5Val" value="1" />
</div>
</td>
<td colspan="5" class="tb-son-container">
<table border="0" cellspacing="0" cellpadding="4" class="tb-son">
<tbody>
<!-- for循环展示tr -->
<tr class="tb-son-tr">
<td class="width60">
<div class="showVal">拆分内容11</div>
<div class="changeVal">
<!-- 修改数据:input,便于提交在data-submit-prefix设入循环index -->
<input type="text" data-submit-prefix="fatherObj.sonObj[0]" data-id="name11" placeholder="表头1" value="拆分内容11" />
<!-- 记录原有数据,点击取消能复原;data-id用于区分原始值元素和需提交元素 -->
<input type="hidden" data-id="name11Val" value="拆分内容11" />
</div>
</td>
<td class="width60">
<div class="showVal">拆分内容12</div>
<div class="changeVal">
<!-- 修改数据:textarea,便于提交在data-submit-prefix设入循环index -->
<textarea type="text" data-submit-prefix="fatherObj.sonObj[0]" data-id="name12" placeholder="表头2">拆分内容12</textarea>
<!-- 记录原有数据,点击取消能复原 -->
<input type="hidden" data-id="name12Val" value="拆分内容12" />
</div>
</td>
<td class="width80">
<div class="showVal">
<label><input type="radio" value="1" checked disabled />是</label><br />
<label><input type="radio" value="0" disabled />否</label>
</div>
<div class="changeVal">
<!-- 修改数据:radio,便于提交在data-submit-prefix设入循环index;由于同一name的radio只能选其一的特殊性,需要加上唯一标识符和子表格for循环渲染的index -->
<label><input type="radio" data-submit-prefix="fatherObj.sonObj[0]" data-id="name13" name="row_key_0_sonObj.name13" value="1" checked />是</label><br />
<label><input type="radio" data-submit-prefix="fatherObj.sonObj[0]" data-id="name13" name="row_key_0_sonObj.name13" value="0" />否</label>
<!-- 记录原有数据,点击取消能复原 -->
<input type="hidden" data-id="name13Val" value="1" />
</div>
</td>
<td class="width100">
<div class="showVal">
<label><input type="checkbox" value="1" checked disabled />多选1</label><br />
<label><input type="checkbox" value="2" checked disabled />多选2</label><br />
<label><input type="checkbox" value="3" disabled />多选3</label>
</div>
<div class="changeVal">
<!-- 修改数据:checkbox,便于提交在data-submit-prefix设入循环index -->
<label><input type="checkbox" data-submit-prefix="fatherObj.sonObj[0]" data-id="name14" value="1" checked />多选1</label><br />
<label><input type="checkbox" data-submit-prefix="fatherObj.sonObj[0]" data-id="name14" value="2" checked />多选2</label><br />
<label><input type="checkbox" data-submit-prefix="fatherObj.sonObj[0]" data-id="name14" value="3" />多选3</label>
<!-- 记录原有数据,点击取消能复原 -->
<input type="hidden" data-id="name14Val" value="1,2" />
</div>
</td>
<td class="width100">
<div class="showVal">拆分内容15</div>
<div class="changeVal">
<!-- 修改数据:select,便于提交在data-submit-prefix设入循环index -->
<select data-submit-prefix="fatherObj.sonObj[0]" data-id="name15">
<option value="">请选择</option>
<option value="1" selected>拆分内容15</option>
<option value="2">选择2</option>
<option value="3">选择3</option>
</select>
<!-- 记录原有数据,点击取消能复原 -->
<input type="hidden" data-id="name15Val" value="1" />
</div>
</td>
</tr>
</tbody>
</table>
</td>
<td class="width60">
<!-- 传入每行id唯一标识:row_key -->
<div class="showVal">
<button onclick="edit('row_key')">编辑</button>
</div>
<div class="changeVal">
<button onclick="submit('row_key')">确定</button>
<button onclick="reset('row_key')">取消</button>
</div>
</td>
</tr>
</tbody>
</table>
(2)CSS
@charset "utf-8";
/* CSS Document */
/* 标题 */
.tb-father .tb-caption{
font-family: 微软雅黑;
font-size: 26px;
font-weight: bold;
color: #255e95;
height: 60px;
line-height: 60px;
border-bottom: 1px dashed rgb(204, 204, 204);
margin: 0 !important;
}
.tb-father .tb-remark {
margin-top: 8px !important;
margin-bottom: 12px !important;
}
.text-right {
text-align: right;
}
/* 表格基本样式 */
.tb-father th {
position: sticky; /* 固定首行 */
top: 0;
z-index: 2; /* 次级,最高级在固定左右侧 */
border: 1px solid rgb(204, 204, 204);
background: #e9faff;
height:25px;
line-height:150%;
text-align: center;
}
.tb-father th:last-child {
z-index: 3;
}
.tb-father th:last-child, tr[id^='row']>td:last-child{
position: sticky; /* 固定尾列 */
right: 0;
}
/* 粘性元素的边框在滚动时变得不可见,直接把原来的border改成outline影响宽高样式;因此在涉及th和td的伪类元素上用outline */
.tb-father th::before,
td.left-outline::before,
tr[id^='row']>td:last-child::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
outline: 1px solid rgb(204, 204, 204);
z-index: -1;
}
.tb-father td{
background-color:#ffffff;
height:25px;
line-height:150%;
word-wrap: break-all;
text-align: center;
}
.tb-father tr[id^='row']>td {
border: 1px solid rgb(204, 204, 204);
}
.tb-father input[type='text'],textarea,select {
width: 90%;
}
.titfont {
font-family: 微软雅黑;
font-size: 16px;
font-weight: bold;
color: #255e95;
background-color:#e9faff;
}
/* 父子表格 */
.tb-father {
table-layout: fixed;
border-collapse: collapse;
}
.tb-son-container {
padding: 0;
border: none;
}
.tb-son {
width: 100%;
height: 100%;
border-collapse: collapse; /* 设置这个tr的边框才生效 */
}
.tb-son tr:not(:last-child) {
border-bottom: 1px solid rgb(204, 204, 204);
}
.tb-son td:not(:last-child){
border-right: 1px solid rgb(204, 204, 204);
}
.width60 {
width: 60px;
}
.tb-son width60{
padding: 0 2px;
}
.width80 {
width: 80px;
}
.width100 {
width: 100px;
}
/* 显示隐藏 */
.showVal {
display: block;
}
.changeVal {
display: none;
}
(3)JS
// 封装原生JS获取元素NodeList并遍历方法
// 看到有涉及封装原生JS修改this指向的博文,待细看 https://www.cnblogs.com/surfaces/p/5169774.html
function getElementListAndForeach(fatherEle, selectStr, fn) {
// 获取子元素nodelist,把nodelist转换成array
let domList = [].slice.call(fatherEle.querySelectorAll(selectStr));
domList.forEach(item => {
fn(item);
})
}
// 点击编辑
function edit(trKey) {
let fatherTr = document.querySelector("#" + trKey);
getElementListAndForeach(fatherTr, '.showVal', function(item) {
item.style.display = 'none';
});
getElementListAndForeach(fatherTr, '.changeVal', function(item) {
item.style.display = 'block';
});
}
// 点击取消,重设各类选项值
// 流程:提交输入框 data-id="key值";原始值存储框 data-id="key值Val";点击取消键,取原始值置入提交输入框
function reset(trKey) {
let fatherTr = document.querySelector("#" + trKey);
getElementListAndForeach(fatherTr, '.showVal', function(item) {
item.style.display = 'block';
});
// 遍历array修改展示状态 & 重设各类选项值
getElementListAndForeach(fatherTr, '.changeVal', function(item) {
item.style.display = 'none';
// 1.获取存放原始value的obj 一般以input type="hidden" 写在代码里
let originValObj = item.querySelector("[data-id$='Val']");
// 1.1.获取原始value
let originVal = originValObj?.value;
// 1.2.获取key,根据key获取需提交的元素
let submitKey = originValObj?.getAttribute('data-id')?.substring(0, originValObj?.getAttribute('data-id')?.length - 3);
let submitObj = item.querySelector("[data-id="+ submitKey +"]");
// 2.获取元素标签名,不同类型的设置值
let submitTagName = submitObj?.tagName;
if(submitTagName) {
switch(submitTagName) {
case "INPUT":
let submitInputType = submitObj?.getAttribute('type');
switch(submitInputType) {
case "radio":
let radioList = item.querySelectorAll("[data-id="+ submitKey +"]");
radioList = [].slice.call(radioList);
radioList.forEach(radioItem => {
if(radioItem.value == originVal) radioItem.checked = true;
})
break;
case "checkbox":
let originValList = [];
if(originVal != "") originValList = originVal.split(",");
let checkboxList = item.querySelectorAll("[data-id="+ submitKey +"]");
checkboxList = [].slice.call(checkboxList);
checkboxList.forEach(checkboxItem => {
if(originValList.indexOf(checkboxItem.value) != -1) {
checkboxItem.checked = true;
} else {
checkboxItem.checked = false;
}
})
break;
default: submitObj.value = originVal; break;
}
break;
default: submitObj.value = originVal; break;
}
}
});
}
// 点击确定提交
function submit(trKey) {
// 1.初始化上传参数
let params = {};
// 2.获取父tr元素
let fatherTr = document.querySelector("#" + trKey);
getElementListAndForeach(fatherTr, '.changeVal', function(item) {
// 2.1.获取表格需提交的元素和key名
let originValObj = item.querySelector("[data-id$='Val']");
let submitKey = originValObj?.getAttribute('data-id')?.substring(0, originValObj?.getAttribute('data-id')?.length - 3);
let submitObj = item.querySelector("[data-id="+ submitKey +"]");
let submitPrefix = submitObj?.getAttribute("data-submit-prefix");
let submitValue = "";
// 2.2.获取元素标签名,不同类型的设置值
let submitTagName = submitObj?.tagName;
if(submitPrefix && submitTagName) {
switch(submitTagName) {
case "INPUT":
let submitInputType = submitObj?.getAttribute('type');
switch(submitInputType) {
case "radio":
let radioList = item.querySelectorAll("[data-id="+ submitKey +"]");
radioList = [].slice.call(radioList);
radioList.forEach(radioItem => {
if(radioItem?.checked && radioItem?.value) submitValue = radioItem?.value;
})
break;
case "checkbox":
let checkboxList = item.querySelectorAll("[data-id="+ submitKey +"]");
checkboxList = [].slice.call(checkboxList);
let submitValueList = [];
checkboxList.forEach(checkboxItem => {
if(checkboxItem?.checked && checkboxItem?.value) submitValueList.push(checkboxItem?.value);
});
if(submitValueList.length > 0) submitValue = submitValueList.join(",");
break;
default:
if(submitObj?.value) submitValue = submitObj?.value; break;
}
break;
case "SELECT": submitValue = submitObj?.options[submitObj?.selectedIndex].value; break;
default: if(submitObj?.value) submitValue = submitObj?.value; break;
}
params[submitPrefix + "." + submitKey] = submitValue;
}
})
console.log(params);
// 调接口送params信息
}
// 固定左侧几列(这个有局限性,只能固定合并列)
function fixLeftColumn (num) {
console.log('触发固定左侧几列');
for(let i = 1, leftOffset = 0; i <= num; i++) {
let fatherTable = document.querySelector(".tb-father");
let th = fatherTable.querySelector("th:nth-child(" + i + ")");
th.style.position = "sticky";
th.style.left = leftOffset + "px";
th.style.zIndex = "3";
let td = getElementListAndForeach(fatherTable, "tr[id^='row']>td:nth-child(" + i + ")", function (item) {
item.style.position = "sticky";
item.style.left = leftOffset + "px";
item.setAttribute("class", item.getAttribute("class") + " left-outline");
})
leftOffset += Number(th.offsetWidth);
}
}
// 模拟数据
function mockData () {
// 模拟数据
let sonTable = document.querySelector(".tb-son").querySelector("tbody");
// 复制子表格单行
let sonTableSampleTr = sonTable.querySelector(".tb-son-tr").cloneNode(true);
// 由于同一name的radio只能选其一的特殊性,需要加上唯一标识符和子表格for循环渲染的index
getElementListAndForeach(sonTableSampleTr, "input[data-id=name13]", function(item) {
item.setAttribute("name", "row_key_1_sonObj.name13");
});
// 便于提交在data-submit-prefix设入循环index
getElementListAndForeach(sonTableSampleTr, "[data-submit-prefix^='fatherObj.sonObj']", function(item) {
item.setAttribute("data-submit-prefix", "fatherObj.sonObj[1]");
});
sonTable.appendChild(sonTableSampleTr);
// 复制子表格单行
let sonTableSampleTr1 = sonTable.querySelector(".tb-son-tr").cloneNode(true);
// 由于同一name的radio只能选其一的特殊性,需要加上唯一标识符和子表格for循环渲染的index
getElementListAndForeach(sonTableSampleTr1, "input[data-id=name13]", function(item) {
item.setAttribute("name", "row_key_2_sonObj.name13");
});
// 便于提交在data-submit-prefix设入循环index
getElementListAndForeach(sonTableSampleTr1, "[data-submit-prefix^='fatherObj.sonObj']", function(item) {
item.setAttribute("data-submit-prefix", "fatherObj.sonObj[2]");
});
sonTable.appendChild(sonTableSampleTr1);
let fatherTable = document.querySelector(".tb-father").querySelector("tbody");
}
// 节流函数(时间戳节流,立即触发,下次在N秒后才能触发)
function throttle (fn, waiting) {
// 上一次执行时间
let previous = 0;
return function (...args) {
// 当前时间
let now = +new Date();
if (now - previous > waiting) {
previous = now;
fn.apply(this, args);
}
}
}
window.onload = function () {
// 1.模拟数据
mockData();
// 2.固定左侧5列
fixLeftColumn(5);
// 如果缩放浏览器,left值固定,显示会有问题;所以增加监听事件改变left值
// resize触发太频繁影响性能,用节流函数套一层
let throttleFn = throttle(fixLeftColumn, 100);
window.addEventListener("resize", function () { throttleFn(5); });
}