组件化的可编辑数据表格
引言
页面中的数据表格通常承载了用户所需的数据,通常用户需要根据实际 情况对部分数据进行修改,本文旨在介绍一种基于组件化思想的可编辑数据表格的设计与实现。在该组件中,用户可以通过点击表格中的可编辑单元格进行修改,并且可以通过JSON动态配置可编辑列、验证规则、样式等可配置项。本项目基于原生JS+HTML+CSS实现,没有额外技术栈,主要锻炼原生JS业务代码编写能力。
初步实现
由于功能可拆分为核心和非核心部分,所以整体设计上采取迭代的方式,首先完成核心功能,即如下功能:
- 当点击表格中可编辑数据时,在当前单元格中显示文本框并在其中显示原数据,从而为用户提供修改该数据的输入域
- 在编辑模式下点击文本框之外的任何区域即可确认输入
- 在编辑模式下输入数据不符合要求时,应该给出适当的提示,并强制改正后方可确认输入
初步表格实现的原型展示:可编辑表格及说明 (axshare.com)
首先我们将数据写死在HTML中,用于验证核心功能:
<table>
<thead>
<tr>
<th class="studentId">学号</th>
<th>姓名</th>
<th class="bj">班级</th>
<th class="chineseGrade">语文成绩</th>
<th class="mathGrade">数学成绩</th>
<th class="englishGrade">英语成绩</th>
<th class="Grade">总成绩</th>
</tr>
</thead>
<tbody>
<tr>
<td>001</td>
<td>陈思颖</td>
<td>一班</td>
<td>85</td>
<td>92</td>
<td>88</td>
</tr>
<tr>
<td>002</td>
<td>赵果</td>
<td>二班</td>
<td>78</td>
<td>89</td>
<td>90</td>
</tr>
<tr>
<td>003</td>
<td>王婷</td>
<td>三班</td>
<td>92</td>
<td>86</td>
<td>85</td>
</tr>
<tr>
<td>004</td>
<td>李磊</td>
<td>一班</td>
<td>80</td>
<td>87</td>
<td>79</td>
</tr>
<tr>
<td>005</td>
<td>张丹</td>
<td>二班</td>
<td>89</td>
<td>90</td>
<td>92</td>
</tr>
<tr>
<td>006</td>
<td>刘谦</td>
<td>三班</td>
<td>86</td>
<td>87</td>
<td>91</td>
</tr>
<tr>
<td>007</td>
<td>黄晓鹏</td>
<td>一班</td>
<td>75</td>
<td>82</td>
<td>78</td>
</tr>
<tr>
<td>008</td>
<td>郭林</td>
<td>二班</td>
<td>91</td>
<td>94</td>
<td>93</td>
</tr>
<tr>
<td>009</td>
<td>黄文静</td>
<td>三班</td>
<td>87</td>
<td>85</td>
<td>82</td>
</tr>
<tr>
<td>010</td>
<td>李晓宇</td>
<td>一班</td>
<td>84</td>
<td>88</td>
<td>77</td>
</tr>
</tbody>
</table>
要提示错误信息,还需要一个弹窗
<div class="error">输入有误哦!请重新输入</div>
在写业务前,将需要用到的DOM元素写在前面做好准备:
//获取成绩和班级元素
let chineseGrade = document.querySelector(".chineseGrade");
let mathGrade = document.querySelector(".mathGrade");
let englishGrade = document.querySelector(".englishGrade");
let bj = document.querySelector(".bj");
let studentId = document.querySelector(".studentId");
let Grade = document.querySelector(".Grade");
//获取需要的表单元素
let table = document.querySelector("table");
let tbody = table.children[1];
let sort = document.querySelector(".sort span");
下面就进入业务逻辑分析:
要实现点击表格数据编辑,需要给每个表格项添加点击事件,在点击时生成一个input输入框,并自动聚焦,且显示表格中当前格的数据,方便编辑,失焦后自动保存,将input的内容放在表格中,删除input元素,实现以上功能的代码如下:
function addClick() {
var trs = document.querySelectorAll("tbody tr");
trs.forEach((e) => {
e.querySelectorAll("td").forEach((x, i) => {
if (i < 6 && i >= 3) {
x.onclick = function () {
let score = x.innerText;
input = document.createElement("input");
input.value = score;
x.innerText = "";
x.appendChild(input);
input.focus();
input.onblur = function () {
var keys;
if (input.value < 0 || input.value > 100) {
document.querySelector(".error").style.display = "block";
setTimeout(() => {
document.querySelector(".error").style.display = "none";
}, 2000);
} else {
document.querySelector(".error").style.display = "none";
data.forEach((num, ind) => {
if (x.parentElement.children[0].innerText == num["学号"]) {
keys = Object.keys(data[ind]);
data[ind][
keys[Array.from(x.parentElement.children).indexOf(x)]
] = input.value;
data[ind]["总成绩"] =
Number(data[ind][keys[3]]) +
Number(data[ind][keys[4]]) +
Number(data[ind][keys[5]]);
}
});
input.remove();
updataHtml();
}
};
input.onclick = function (e) {
e.stopPropagation();
};
};
}
});
});
}
需要注意的是,由于input是表格元素的子组件,默认点击事件会产生冒泡触发父元素的点击事件(即产生输入框并聚焦),所以需要用stopPropagation()阻止冒泡产生。
其中包含数据验证,在数据错误时,弹出提示框:
if (input.value < 0 || input.value > 100) {
document.querySelector(".error").style.display = "block";
setTimeout(() => { document.querySelector(".error").style.display = "none";}, 2000);} else { document.querySelector(".error").style.display = "none";
}
在正确时,更新html结构,将临时data中的数据响应到DOM上:
function updataHtml() {
tbody.innerHTML = "";
data.forEach((e) => {
let tr = document.createElement("tr");
for (const i in e) {
let td = document.createElement("td");
td.innerHTML = e[i];
tr.appendChild(td);
}
tbody.appendChild(tr);
});
addClick();
}
至此表格的基础功能完成,需要注意的是,在进行输入数据更新时,最好将数据存入临时变量中,最后再整体跟新数据,这样方便后期功能的迭代。
添枝加叶
完成了核心功能,其中我最想实现的是排序功能,首先应该在html中显示排序信息:
<div class="sort">当前排序状态:<span>学号升序</span></div>
排序的方式有很多,我使用的是sort操作数组排序,然后将数组反应到dom结构中:
//记录点击次数,偶数为升序排列,奇数为降序排列
let clickNum = 0;
//点击语文成绩排序
chineseGrade.addEventListener("click", () => {
clickNum++;
if (clickNum % 2 === 0) {
sort.innerText = "语文成绩升序";
data.sort((a, b) => {
return a.语文成绩 - b.语文成绩;
});
} else {
sort.innerText = "语文成绩降序";
data.sort((a, b) => {
return b.语文成绩 - a.语文成绩;
});
}
updataHtml();
});
//点击数学成绩排序
mathGrade.addEventListener("click", () => {
clickNum++;
if (clickNum % 2 === 0) {
sort.innerText = "数学成绩升序";
data.sort((a, b) => {
return a.数学成绩 - b.数学成绩;
});
} else {
data.sort((a, b) => {
sort.innerText = "数学成绩降序";
return b.数学成绩 - a.数学成绩;
});
}
updataHtml();
});
//点击英语成绩排序
englishGrade.addEventListener("click", () => {
clickNum++;
if (clickNum % 2 === 0) {
sort.innerText = "英语成绩升序";
data.sort((a, b) => {
return a.英语成绩 - b.英语成绩;
});
} else {
sort.innerText = "英语成绩降序";
data.sort((a, b) => {
return b.英语成绩 - a.英语成绩;
});
}
updataHtml();
});
//点击学号排序
studentId.addEventListener("click", () => {
clickNum++;
if (clickNum % 2 === 0) {
data.sort((a, b) => {
sort.innerText = "学号升序";
return a.学号 - b.学号;
});
} else {
sort.innerText = "学号降序";
data.sort((a, b) => {
return b.学号 - a.学号;
});
}
updataHtml();
});
//点击总成绩排序
Grade.addEventListener("click", () => {
clickNum++;
if (clickNum % 2 === 0) {
sort.innerText = "总成绩升序";
data.sort((a, b) => {
return a.总成绩 - b.总成绩;
});
} else {
sort.innerText = "总成绩降序";
data.sort((a, b) => {
return b.总成绩 - a.总成绩;
});
}
updataHtml();
});
//点击班级排序.
bj.addEventListener("click", () => {
clickNum++;
if (clickNum % 2 === 0) {
sort.innerText = "班级升序";
data.sort((a, b) => {
return transform(a.班级) - transform(b.班级);
});
} else {
sort.innerText = "班级降序";
data.sort((a, b) => {
return transform(b.班级) - transform(a.班级);
});
}
updataHtml();
});
//把班级字符串转换为数字进行排序
function transform(a) {
let anser = 0;
switch (a[0]) {
case "一":
anser = 1;
break;
case "二":
anser = 2;
break;
case "三":
anser = 3;
break;
case "四":
anser = 4;
break;
case "五":
anser = 5;
break;
case "六":
anser = 6;
break;
default:
break;
}
return anser;
}
此处每一列的数据都单独写了排序函数,但是后期可以优化合并成一个排序方式,值得注意的是,班级在表格中用汉字的一二三四表示,所以需要自定义字码表,这样才可对汉字的数字大小进行排序,如果用unicode或ASCII码比大小,汉字的一二三四并没有在其中按照大小编码
当然排序功能全凭个人意愿来写,但是添枝加叶的主要需要实现功能如下:
- 假设表格中要显示的数据来自服务端,由 JSON 格式表示,格式如下: [{pro1:val1,pro2:val2,pro3:val3,…},{pro1:val1,pro2:val2,pro3:val3, …},{pro1:val1,pro2:val2,pro3:val3,…},…]
- 通过以上 JSON 格式的数组(数据自拟,建议数据对象不少于 3 个属性) 生成可编辑表格,并且能灵活配置可编辑的数据列。
- 能分别为不同的可编辑列提供验证规则。
- 能配置和实现数据行的可删除操作。
- 以上2、3、4均需提供默认配置,以简化可编辑表格函数的参数 传递。
- 数据修改能直接映射到从 JSON 生成的数据对象(该对象可作为缓存,以 备提交到服务器)中。
我们定义好json文件,用于动态生成表格,格式如下:
{
"head": [
{
"item": "学号",
"editable": false,
"sortable": true,
"pattern": "^\d{3}$"
},
{
"item": "姓名",
"editable": false,
"sortable": false,
"pattern": "^.{2,4}$"
},
{
"item": "班级",
"editable": false,
"sortable": true,
"pattern": "^\d{1,2}班$"
},
{
"item": "语文",
"editable": true,
"sortable": true,
"pattern": "^150$|^(\d|[1-9]\d|1[0-4]\d)(\.5)?$"
},
{
"item": "数学",
"editable": true,
"sortable": true,
"pattern": "^150$|^(\d|[1-9]\d|1[0-4]\d)(\.5)?$"
},
{
"item": "英语",
"editable": true,
"sortable": true,
"pattern": "^([0-9]{1,2}$)|(^[0-9]{1,2}\.5$)|100$"
},
{ "item": "总成绩", "editable": false, "sortable": true }
],
"students": [
["001", "张丽华", "3班", 85, 76, 91],
["002", "陈大明", "6班", 92, 85, 79],...
]
}
json中head数组为表头数据,用于动态生成表头,其中有四个字段:
item表示表头文字内容editable表示列可编辑性sortable表示列可排序性pattern表示列的数据验证规则,为正则表达式
这样一来,每一列的可排序性,可编辑性,验证规则都可以不同,且都可以根据后端传入数据动态改变
students中每个数组的每个字段和head整体的每个对象item一一对应
要进行表格内容的动态生成,那么首先要从json中获取数据,在本项目中使用axios简化获取步骤:
//获取成绩数据,localstorage不存在从axio获取
function loadData() {
axios.get("./data.json").then((res) => {
data = res.data;
if (localStorage.getItem("data")) data = JSON.parse(localStorage.data);
else {
data = res.data;
localStorage.data = JSON.stringify(data);
}
sort.innerText = localStorage.sort || sort.innerText;
}
其中sort是存入localstorage中上次的排序状态,以便于在第二次打开时和data数据排序做对应
下面将进行动态规则验证编写,其实只需要将之前的手动判断改为正则表达式的test方法返回true或false判断即可
input.onblur = function () {
if (
data.head[i].pattern &&
!RegExp(data.head[i].pattern).test(input.value)
) {
document.querySelector(".error").style.display = "block";
setTimeout(() => {
document.querySelector(".error").style.display = "none";
}, 2000);
} else {
document.querySelector(".error").style.display = "none";
if (i === 0) {
data.students[10 * newcurrentpage - (10 - row)][i] =
input.value;
} else {
data.students[10 * newcurrentpage - (10 - row)][i] =
Number(input.value) || input.value;
}
input.remove();
updataHtml();
}
};
对于可编辑和可排序性,只需在排序函数中加入验证,如果data.head中的对应字段为true则可执行函数,反之则不可:
//记录点击次数,偶数为升序排列,奇数为降序排列
let clickNum = 0;
function addSort() {
i = 0;
data.head.forEach((x) => {
if (x.sortable) {
document.querySelectorAll("table thead tr th").forEach((e) => {
if (e.innerText == x.item) {
e.onclick = (e) => {
clickNum++;
if (clickNum % 2 === 0) {
//降序
rank(e.target.innerText, clickNum);
updataHtml();
} else {
//升序
rank(e.target.innerText, clickNum);
updataHtml();
}
};
}
});
}
});
}
在这次迭代中,我将所有列的排序方法进行了统一,压缩精简了函数:
function rank(e, clickNum) {
let index = 0;
data.head.forEach((elm) => {
if (elm.item === e) {
index = data.head.indexOf(elm);
return;
}
});
data.students.sort((a, b) => {
if (clickNum % 2 === 0) {
sort.innerHTML = `${e}降序`;
localStorage.sort = sort.innerHTML = `${e}降序`;
return parseFloat(b[index]) - parseFloat(a[index]);
} else {
sort.innerHTML = `${e}升序`;
localStorage.sort = sort.innerHTML = `${e}升序`;
return parseFloat(a[index]) - parseFloat(b[index]);
}
});
}
用parseFloat函数对数据进行裁剪,这样只要数据中含有数字即可按照此方法排序
最后在这一部分需要做的最后功能就是删除和新增功能:
//删除行
let ok = document.querySelector(".ok");
let error = document.querySelector(".error");
let del = document.querySelector(".delete");
let delBtn = document.querySelector(".del button");
let tag = false;
delBtn.addEventListener("click", () => {
let value = del.value;
data.students.forEach((e) => {
if (value === e[0]) {
tag = true;
nowpage = Math.floor((data.students.indexOf(e) + 1) / 10) + 1;
if ((data.students.indexOf(e) + 1) / 10 === nowpage - 1) {
nowpage--;
}
if (nowpage === totalpage) {
lastpageList.pop();
}
data.students.splice(data.students.indexOf(e), 1);
btnn(nowpage);
ok.style.display = "block";
setTimeout(() => {
ok.style.display = "none";
}, 2000);
updataHtml();
return 0;
}
});
if (!tag) {
error.style.display = "block";
setTimeout(() => {
error.style.display = "none";
}, 2000);
} else {
tag = false;
}
});
//增加行
let newdata = document.querySelector(".add");
newdata.onclick = () => {
let input = document.querySelector(".input_pop form");
input.innerHTML = "";
data.head.forEach((e) => {
if (e.editable == false && e.item != "总成绩") {
let div = document.createElement("div");
div.innerText = e.item + ":";
let put = document.createElement("input");
put.setAttribute("type", "text");
div.appendChild(put);
input.appendChild(div);
}
});
InputModalPop();
};
其中一些函数涉及到分页和用户交互性使用,所以将在下一节中解释
最终完善
在实现主要功能之后,就是对一些功能进行完善,以及交互性、样式的设计,在上一节中,完成了验证规则和数据的动态生成,但是用户并不能通过更改json的方式来配置列属性,还有在进行新增数据时,需要初始配置不可编辑值,所以我进行了一些交互式弹窗的设计,例如,在进行新增时显示这个弹窗:
此弹窗中表单信息由不可编辑列动态生成,代码见上一部分最后
为了达到用户编辑表格列属性,我添加了右键表头事件,效果如下:
其中的内容和data对象中head数据双向绑定,会动态生成,具体实现代码如下:
html如下:
<div class="modal animated" style="display: none">
<h2>列属性配置</h2>
<hr />
<form>
<div>列名:<input type="text" /></div>
<div>
是否可编辑:<label for="editable">是</label
><input type="radio" id="editable" vlaue="editable" name="edit" />
<label for="uneditable">否</label
><input type="radio" id="uneditable" vlaue="uneditable" name="edit" />
</div>
<div>
是否可排序:<label for="sortable">是</label
><input type="radio" id="sortable" vlaue="sortable" name="sort" />
<label for="unsortable">否</label
><input type="radio" id="unsortable" vlaue="unsortable" name="sort" />
</div>
<div>
正则表达式(为空则不验证):<br /><textarea></textarea>
<div>
快捷验证规则:<button
type="button"
onclick="document.querySelector('.modal form textarea').value='^50$|^([0-4]\d?)(\.5)?$'"
>
0-50</button
><button
type="button"
onclick="document.querySelector('.modal form textarea').value='^([0-9]{1,2}$)|(^[0-9]{1,2}\.5$)|100$'"
>
0-100</button
><button
type="button"
onclick="document.querySelector('.modal form textarea').value='^150$|^(\d|[1-9]\d|1[0-4]\d)(\.5)?$'"
>
0-150
</button>
</div>
</div>
<hr />
<button onclick="modalOut(true)" type="button">确定</button>
<button onclick="modalOut(false)" type="button">取消</button>
</form>
</div>
//增加表头配置
function addConfigure() {
let name = document.querySelector(".modal form > div input");
let sortable = document.querySelectorAll(
".modal form div:nth-child(3) input"
);
let editable = document.querySelectorAll(
".modal form div:nth-child(2) input"
);
let pattern = document.querySelector(".modal form textarea");
document.querySelectorAll("table thead tr th").forEach((e, i) => {
e.addEventListener("contextmenu", function (event) {
popIndex = i;
event.preventDefault();
name.value = data.head[i].item;
if (data.head[i].sortable) sortable[0].checked = true;
else sortable[1].checked = true;
if (data.head[i].editable) editable[0].checked = true;
else editable[1].checked = true;
pattern.value = data.head[i].pattern || "";
modalPop();
});
});
}
//弹出弹窗
function modalPop() {
document.querySelector(".mask").style.display = "block";
document.querySelector(".modal").style.display = "block";
document.querySelector(".modal").classList.remove("bounceOut");
document.querySelector(".modal").classList.add("bounceIn");
}
最后,由于数据可能很多,为了控制每页数据数量,我添加了分页组件,由于各部分代码耦合性较高,所以就最终展示完整代码,效果如下
实现效果最终如下:
项目完整代码
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>可编辑表格</title>
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="animate.css" />
<script src="./axios.js"></script>
</head>
<body>
<div class="table">
<h1>可编辑表格</h1>
<div class="sort">当前排序状态:<span>学号升序</span></div>
<table>
<thead></thead>
<tbody></tbody>
</table>
<div class="click">
<input type="button" value="上一张" class="pre" />
<div class="btns"></div>
<input type="button" value="下一张" class="next" />
</div>
<div class="error">输入有误哦!请重新输入</div>
<div class="ok">成功删除</div>
</div>
<button class="add">新增一行</button>
<div class="del">
<input type="text" class="delete" placeholder="请输入要删除的学号" />
<button>删!</button>
</div>
<div class="mask animated" style="display: none"></div>
<div class="modal animated" style="display: none">
<h2>列属性配置</h2>
<hr />
<form>
<div>列名:<input type="text" /></div>
<div>
是否可编辑:<label for="editable">是</label
><input type="radio" id="editable" vlaue="editable" name="edit" />
<label for="uneditable">否</label
><input type="radio" id="uneditable" vlaue="uneditable" name="edit" />
</div>
<div>
是否可排序:<label for="sortable">是</label
><input type="radio" id="sortable" vlaue="sortable" name="sort" />
<label for="unsortable">否</label
><input type="radio" id="unsortable" vlaue="unsortable" name="sort" />
</div>
<div>
正则表达式(为空则不验证):<br /><textarea></textarea>
<div>
快捷验证规则:<button
type="button"
onclick="document.querySelector('.modal form textarea').value='^50$|^([0-4]\d?)(\.5)?$'"
>
0-50</button
><button
type="button"
onclick="document.querySelector('.modal form textarea').value='^([0-9]{1,2}$)|(^[0-9]{1,2}\.5$)|100$'"
>
0-100</button
><button
type="button"
onclick="document.querySelector('.modal form textarea').value='^150$|^(\d|[1-9]\d|1[0-4]\d)(\.5)?$'"
>
0-150
</button>
</div>
</div>
<hr />
<button onclick="modalOut(true)" type="button">确定</button>
<button onclick="modalOut(false)" type="button">取消</button>
</form>
</div>
<div class="modal animated input_pop" style="display: none">
<h2>新增行不可编辑列初始值配置</h2>
<hr />
<form></form>
<hr />
<button onclick="InputModalOut(true)" type="button">确定</button>
<button onclick="InputModalOut(false)" type="button">取消</button>
</div>
<section>
<h2>表格说明</h2>
<p>
1.默认前三个字段不可改,后三个字段可改,总成绩不可改(可动态配置,有悬停效果即可改)
</p>
<p>2.点击表头可以按照对应列排序(成绩/班级/学号/总成绩)</p>
<p>
3.编辑确认时,验证编辑成绩,默认语文数学0-150,英语0-100,可右键点击表头编辑验证规则
</p>
<p>4.编辑以上成绩后,鼠标点击其他区域则表示确认修改</p>
<p>5.表头添加右键点击事件,可弹出弹窗配置列属性</p>
</section>
</body>
<script src="index.js"></script>
</html>
css:
* {
margin: 0;
padding: 0;
--border: 2px solid rgba(121, 121, 121, 1);
}
body {
background: url(https://imger.nl/images/2023/04/10/11.jpg);
}
h1 {
font-family: "Rock Salt", cursive;
padding: 10px;
font-style: italic;
caption-side: bottom;
color: #1a4303;
letter-spacing: 10px;
font-size: 50px;
text-align: center;
}
tr,
td,
th {
border: var(--border);
font-weight: 600;
}
th {
font-weight: 600;
text-align: center;
background-color: rgb(215, 228, 184);
}
.table {
width: 50%;
position: relative;
padding: 0 20px;
}
table > thead > tr > th,
table > tbody > tr > td {
width: 90px;
height: 45px;
font-size: 16px;
}
table {
background-color: rgb(233, 238, 216);
text-align: center;
border: 2px solid black;
border-spacing: 0;
border-collapse: collapse; /*设置表格的边框是否被合并为一个单一的边框*/
margin: 0 auto;
}
input {
width: inherit;
height: inherit;
background-color: #f3f694;
border: none;
text-align: center;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
}
.error,
.ok {
display: none;
position: absolute;
top: 100px;
left: 50%;
transform: translate(-50%);
width: 160px;
text-align: center;
padding: 15px 18px;
background: rgb(168, 232, 136);
border-radius: 20px;
font-size: 13px;
font-weight: 600;
animation-name: move;
animation-duration: 2s;
animation-fill-mode: forwards;
}
section {
width: 640px;
height: 300px;
padding: 0 10px;
background-image: url("https://imger.nl/images/2023/04/10/6ff8efe01cfd3a2bfec456b8e3471e04.jpg");
opacity: 0.8;
background-size: 100%;
border: 2px black solid;
border-radius: 10px;
display: inline-block;
position: absolute;
top: 250px;
right: 3%;
}
h2 {
text-align: center;
}
p {
margin: 20px auto;
font-size: 16px;
font-weight: 600;
}
td,
th {
user-select: none;
cursor: pointer;
}
@keyframes move {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.sort {
width: 200px;
margin: 0 auto;
margin-bottom: 10px;
height: 15px;
line-height: 15px;
text-align: center;
padding: 15px 18px;
background: rgb(215, 228, 184);
border-radius: 10px;
font-size: 16px;
font-weight: 600;
border: var(--border);
}
.click {
text-align: center;
}
.click button {
width: 60px;
height: 30px;
margin: 5px;
border: var(--border);
border-radius: 5px;
background-color: rgb(233, 238, 216);
position: relative;
top: -2px;
}
.click button:hover {
background-color: #f3f694;
}
.pre,
.next {
width: 60px;
height: 30px;
margin: 5px;
border: var(--border);
border-radius: 5px;
background-color: rgb(251, 255, 238);
}
.add {
position: absolute;
top: 200px;
left: 62%;
width: 80px;
height: 30px;
background-color: #c2eea4;
border-radius: 5px;
border: 2px solid gray;
cursor: pointer;
font-weight: 600;
box-sizing: content-box;
}
.add:hover {
background-color: #f3f694;
}
.del input {
background-color: unset;
width: 200px;
height: 30px;
}
.del {
position: absolute;
top: 200px;
left: 68%;
border: 2px solid gray;
border-radius: 4px;
background-color: rgb(215, 228, 184);
width: 265px;
height: 30px;
box-sizing: content-box;
}
.del > button {
width: 60px;
border-radius: 0 4px 4px 0;
border: none;
border-left: 2px solid gray;
height: 30px;
background-color: #c2eea4;
position: relative;
top: -1px;
font-weight: 600;
text-align: center;
}
.del > button:hover {
background-color: #f3f694;
}
.modal {
width: 500px;
min-height: 100px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
background-color: rgb(233, 238, 216);
border: var(--border);
margin-left: -250px;
margin-top: -100px;
z-index: 9;
border-radius: 15px;
}
.mask {
width: 100%;
height: 100%;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.305);
backdrop-filter: blur(2px);
z-index: 8;
}
.modal form > div {
margin: 10px 0;
font-weight: 600;
color: #1a4303;
}
.modal button {
width: 60px;
height: 30px;
margin: 5px;
border: var(--border);
border-radius: 5px;
background-color: rgb(251, 255, 238);
}
.modal button:hover {
background-color: #d1edab;
}
.modal input[type="text"],
textarea {
height: 30px;
background-color: unset;
border: var(--border);
border-radius: 5px;
line-height: 30px;
}
textarea {
width: 80%;
margin-top: 10px;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
}
.btns {
display: inline-block;
}
js:
//获取需要的表单元素
let table = document.querySelector("table");
let thead = table.children[0];
let tbody = table.children[1];
let sort = document.querySelector(".sort span");
let btn = document.querySelector(".btns");
const tableList = [];
const pagesize = 10;
var newpageList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var lastpageList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var page1;
var newcurrentpage = 1;
var data = [];
var popIndex;
let totalpage = 0;
let nowpage = 0;
window.onload = function () {
loadData();
};
//获取成绩数据,localstorage不存在从axio获取
function loadData() {
axios.get("./data.json").then((res) => {
data = res.data;
if (localStorage.getItem("data")) data = JSON.parse(localStorage.data);
else {
data = res.data;
localStorage.data = JSON.stringify(data);
}
sort.innerText = localStorage.sort || sort.innerText;
let tr = document.createElement("tr");
data.head.forEach((e) => {
let th = document.createElement("th");
th.innerText = e.item;
tr.appendChild(th);
});
thead.appendChild(tr);
data.students.forEach((e) => {
let body_tr = document.createElement("tr");
let sum = 0;
e.forEach((x) => {
let td = document.createElement("td");
td.innerText = x;
body_tr.appendChild(td);
if (typeof x == "number") sum += x;
});
e[e.length] = sum;
let td = document.createElement("td");
td.innerText = sum;
body_tr.appendChild(td);
tbody.appendChild(body_tr);
});
btnn();
addClick();
addConfigure();
List(); //显示第一页
});
}
//按钮函数
let flag = 0;
function btnn(nowpage) {
flag++;
//拿取数据
if (data.students.length <= 10) {
btn.style = "display:none";
} else {
const datalength = data.students.length;
for (let i = 0; i < datalength; i++) {
tableList[i] = i;
}
const currentPage = 1;
totalpage = Math.ceil(datalength / pagesize);
page1 = totalpage;
pageList = tableList.slice(
(currentPage - 1) * pagesize,
currentPage * pagesize
);
List();
//创建按钮
btn.innerHTML = "";
for (let i = 1; i <= totalpage; i++) {
let button = document.createElement("button");
btn.insertBefore(button, btn.querySelector(".next"));
button.textContent = i;
addbtn(button);
if (i == 1 && flag === 1) {
button.style = "background-color:rgb(215, 228, 184);";
}
if (i === nowpage) {
button.style = "background-color:rgb(215, 228, 184);";
}
}
}
}
//给按钮增加点击事件
function addbtn(e) {
e.onclick = function (e) {
clear();
newcurrentpage = Number(e.target.innerText);
newpageList = tableList.slice(
(newcurrentpage - 1) * pagesize,
newcurrentpage * pagesize
);
e.target.style = "background-color:rgb(215, 228, 184);";
List();
};
}
//更新表头
function updataHead() {
thead.innerHTML = "";
let tr = document.createElement("tr");
data.head.forEach((e) => {
let th = document.createElement("th");
th.innerText = e.item;
tr.appendChild(th);
});
thead.appendChild(tr);
}
//清除样式
function clear() {
let allbutton = document.querySelectorAll("button");
for (let i = 0; i < allbutton.length; i++) allbutton[i].style = "";
}
//分页函数
function List() {
tbody.innerHTML = "";
for (let i = 0; i < newpageList.length; i++) {
let body_tr = document.createElement("tr");
tbody.appendChild(body_tr);
let sum = 0;
for (let j = 0; j < data.head.length; j++) {
if (data.students[newpageList[i]]) {
let td = document.createElement("td");
td.innerText = data.students[newpageList[i]][j];
body_tr.appendChild(td);
if (
typeof data.students[newpageList[i]][j] == "number" &&
j != data.head.length - 1
) {
if (j != 0) {
sum += data.students[newpageList[i]][j];
}
}
if (j == data.head.length - 1) {
td.innerText = sum;
}
} else {
break;
}
}
}
addClick();
addSort();
}
//增加表头配置
function addConfigure() {
let name = document.querySelector(".modal form > div input");
let sortable = document.querySelectorAll(
".modal form div:nth-child(3) input"
);
let editable = document.querySelectorAll(
".modal form div:nth-child(2) input"
);
let pattern = document.querySelector(".modal form textarea");
document.querySelectorAll("table thead tr th").forEach((e, i) => {
e.addEventListener("contextmenu", function (event) {
popIndex = i;
event.preventDefault();
name.value = data.head[i].item;
if (data.head[i].sortable) sortable[0].checked = true;
else sortable[1].checked = true;
if (data.head[i].editable) editable[0].checked = true;
else editable[1].checked = true;
pattern.value = data.head[i].pattern || "";
modalPop();
});
});
}
//弹出弹窗
function modalPop() {
document.querySelector(".mask").style.display = "block";
document.querySelector(".modal").style.display = "block";
document.querySelector(".modal").classList.remove("bounceOut");
document.querySelector(".modal").classList.add("bounceIn");
}
function InputModalPop() {
document.querySelector(".mask").style.display = "block";
document.querySelector(".input_pop").style.display = "block";
document.querySelector(".input_pop").classList.remove("bounceOut");
document.querySelector(".input_pop").classList.add("bounceIn");
}
function InputModalOut(bool) {
if (bool) {
let div = document.querySelectorAll(".input_pop form div input");
let newtr = [];
let unedit = [];
data.head.forEach((e, i) => {
if (!e.editable && e.item != "总成绩") {
unedit.push(i);
}
});
let j = 0;
let flag = 1;
for (let i = 0; i < data.head.length; i++) {
if (
j < unedit.length &&
(!div[j].value.trim() ||
!RegExp(data.head[unedit[j]].pattern).test(div[j].value))
) {
flag = 0;
}
if (unedit.includes(i)) newtr[i] = div[j++].value;
else newtr[i] = "";
}
if (flag) {
data.students[data.students.length] = newtr;
const datalength = data.students.length;
for (let i = 0; i < datalength; i++) {
tableList[i] = i;
}
newpageList = tableList.slice(
Math.floor((data.students.length - 1) / pagesize) * pagesize,
Math.floor((data.students.length - 1) / pagesize) * pagesize + 10
);
updataHtml();
btnn();
clear();
document.querySelector(".mask").style.display = "none";
document.querySelector(".input_pop").classList.remove("bounceIn");
document.querySelector(".input_pop").classList.add("bounceOut");
setTimeout(() => {
document.querySelector(".input_pop").style.display = "none";
}, 700);
btn.querySelector(":last-child").style =
"background-color:rgb(215, 228, 184);";
} else alert("不可编辑列必须有初始值,且符合对应列验证规则!");
} else {
document.querySelector(".mask").style.display = "none";
document.querySelector(".input_pop").classList.remove("bounceIn");
document.querySelector(".input_pop").classList.add("bounceOut");
setTimeout(() => {
document.querySelector(".input_pop").style.display = "none";
}, 700);
}
}
function modalOut(bool) {
document.querySelector(".mask").style.display = "none";
document.querySelector(".modal").classList.remove("bounceIn");
document.querySelector(".modal").classList.add("bounceOut");
setTimeout(() => {
document.querySelector(".modal").style.display = "none";
}, 700);
if (bool) {
let name = document.querySelector(".modal form > div input");
let sortable = document.querySelectorAll(
".modal form div:nth-child(3) input"
);
let editable = document.querySelectorAll(
".modal form div:nth-child(2) input"
);
let pattern = document.querySelector(".modal form textarea");
data.head[popIndex].item = name.value;
data.head[popIndex].pattern = pattern.value;
if (sortable[0].checked) data.head[popIndex].sortable = true;
else data.head[popIndex].sortable = false;
if (editable[0].checked) data.head[popIndex].editable = true;
else data.head[popIndex].editable = false;
updataHead();
updataHtml();
addSort();
addConfigure();
}
}
//上一页函数
let pre = document.querySelector(".pre");
pre.addEventListener("click", (e) => {
clear();
console.log(e.target); //输出的当前所在页 例如 2
newpageList = []; //创建一个空数组
newcurrentpage--; //当前页减一
if (newcurrentpage <= 0) {
newcurrentpage = page1;
}
let allbutton = document.querySelectorAll("button");
console.log(allbutton.values);
for (let i = 1; i <= allbutton.length; i++) {
if (i == newcurrentpage) {
allbutton[i - 1].style = "background-color:rgb(215, 228, 184);";
}
}
newpageList = tableList.slice(
(newcurrentpage - 1) * pagesize,
newcurrentpage * pagesize
); //传入数组 例如[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 第一页
e.stopPropagation();
List(); //调用分页函数
});
//下一页函数
let next = document.querySelector(".next");
next.addEventListener("click", (e) => {
clear();
console.log(newcurrentpage); //输出的当前所在页 例如 2
newpageList = []; //创建一个空数组
newcurrentpage++; //当前页加一
if (newcurrentpage > page1) {
newcurrentpage = 1;
}
console.log(newcurrentpage); //输出 1
let allbutton = document.querySelectorAll("button");
for (let i = 1; i <= allbutton.length; i++) {
if (i == newcurrentpage) {
allbutton[i - 1].style = "background-color:rgb(215, 228, 184);";
}
}
newpageList = tableList.slice(
(newcurrentpage - 1) * pagesize,
newcurrentpage * pagesize
); //传入数组 例如[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 第一页
e.stopPropagation();
List(); //调用分页函数
});
//记录点击次数,偶数为升序排列,奇数为降序排列
let clickNum = 0;
function addSort() {
i = 0;
data.head.forEach((x) => {
if (x.sortable) {
document.querySelectorAll("table thead tr th").forEach((e) => {
if (e.innerText == x.item) {
e.onclick = (e) => {
clickNum++;
if (clickNum % 2 === 0) {
//降序
rank(e.target.innerText, clickNum);
updataHtml();
} else {
//升序
rank(e.target.innerText, clickNum);
updataHtml();
}
};
}
});
}
});
}
//排序
function rank(e, clickNum) {
let index = 0;
data.head.forEach((elm) => {
if (elm.item === e) {
index = data.head.indexOf(elm);
return;
}
});
data.students.sort((a, b) => {
if (clickNum % 2 === 0) {
sort.innerHTML = `${e}降序`;
localStorage.sort = sort.innerHTML = `${e}降序`;
return parseFloat(b[index]) - parseFloat(a[index]);
} else {
sort.innerHTML = `${e}升序`;
localStorage.sort = sort.innerHTML = `${e}升序`;
return parseFloat(a[index]) - parseFloat(b[index]);
}
});
}
//更新表格
function updataHtml() {
let index = 0;
data.head.forEach((e) => {
if (e.item === "总成绩") {
index = data.head.indexOf(e);
}
});
tbody.innerHTML = "";
data.students.forEach((e) => {
let body_tr = document.createElement("tr");
let sum = 0;
e.forEach((x) => {
let td = document.createElement("td");
if (e.indexOf(x) === index) {
td.innerHTML = sum;
} else {
if (typeof x == "number") {
sum += x;
}
td.innerHTML = x;
}
body_tr.appendChild(td);
});
e[index] = sum;
tbody.appendChild(body_tr);
});
localStorage.data = JSON.stringify(data);
addSort();
addClick();
List();
}
//添加悬停效果
function onHover(x) {
x.onmouseenter = function () {
this.style.backgroundColor = "#f3f694";
};
x.onmouseleave = function () {
this.style.backgroundColor = "unset";
};
}
//添加表格点击事件
function addClick() {
let index = [];
data.head.forEach((e, i) => {
if (e.editable) index.push(i);
});
var trs = document.querySelectorAll("tbody tr");
trs.forEach((e, row) => {
e.querySelectorAll("td").forEach((x, i) => {
if (index.includes(i)) {
onHover(x);
x.onclick = function () {
let score = x.innerText;
input = document.createElement("input");
input.value = score;
if (i == 0) {
input.value = score.toString().padStart(3, "0");
}
x.innerText = "";
x.appendChild(input);
input.focus();
input.select();
input.onblur = function () {
if (
data.head[i].pattern &&
!RegExp(data.head[i].pattern).test(input.value)
) {
document.querySelector(".error").style.display = "block";
setTimeout(() => {
document.querySelector(".error").style.display = "none";
}, 2000);
} else {
document.querySelector(".error").style.display = "none";
if (i === 0) {
data.students[10 * newcurrentpage - (10 - row)][i] =
input.value;
} else {
data.students[10 * newcurrentpage - (10 - row)][i] =
Number(input.value) || input.value;
}
input.remove();
updataHtml();
}
};
input.onclick = function (e) {
e.stopPropagation();
};
};
}
});
});
}
//删除行
let ok = document.querySelector(".ok");
let error = document.querySelector(".error");
let del = document.querySelector(".delete");
let delBtn = document.querySelector(".del button");
let tag = false;
delBtn.addEventListener("click", () => {
let value = del.value;
data.students.forEach((e) => {
if (value === e[0]) {
tag = true;
nowpage = Math.floor((data.students.indexOf(e) + 1) / 10) + 1;
if ((data.students.indexOf(e) + 1) / 10 === nowpage - 1) {
nowpage--;
}
if (nowpage === totalpage) {
lastpageList.pop();
}
data.students.splice(data.students.indexOf(e), 1);
btnn(nowpage);
ok.style.display = "block";
setTimeout(() => {
ok.style.display = "none";
}, 2000);
updataHtml();
return 0;
}
});
if (!tag) {
error.style.display = "block";
setTimeout(() => {
error.style.display = "none";
}, 2000);
} else {
tag = false;
}
});
//增加行
let newdata = document.querySelector(".add");
newdata.onclick = () => {
let input = document.querySelector(".input_pop form");
input.innerHTML = "";
data.head.forEach((e) => {
if (e.editable == false && e.item != "总成绩") {
let div = document.createElement("div");
div.innerText = e.item + ":";
let put = document.createElement("input");
put.setAttribute("type", "text");
div.appendChild(put);
input.appendChild(div);
}
});
InputModalPop();
};