需求背景
我们最近在做一些商品的备案工作,历史有很多的备案记录,不能很好的利用。所以想做一个提效工具。
备案人员,在网页通过搜索历史知识库的数据,进行备案编辑,他们平时都在使用Excel,所以要求样式和使用体验求尽量向Excel 靠近,当然本篇主要讲table在线编辑的一些实现思路
主要实现功能
效果
template
<table>
<thead>
<tr>
<th v-for="(th, i) in headers" :key="i" @click="go">{{ th }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in rowsList" :key="rowIndex">
<td v-for="(cellValue, columnIndex) in row" :key="columnIndex">
<div>{{ cellValue }}</div>
</td>
</tr>
</tbody>
</table>
数据结构
const headers = ref(["Name", "Age", "Gender"]);
const rowsList = ref([
["Alice", 20, "Female"],
["Bob", 30, "Male"]
]);
api选择
可编辑提供了一些方式,input,textarea,还有h5新增的contentEditable。最终由于是多行文本编辑,并且要提供给用户一些换行等功能,所以最后选择了contentEditable
交互
交互想了两种,一种是提供一个按钮,用户点击按钮,当前行变为可编辑。
另一种是直接可以输入
但是在我这个场景下,主要就是操作便捷,按钮无疑是增加了操作的复杂度。可是使用直接输入,这么多输入框,都是可编辑状态,性能堪忧啊。但是最后采用了直接输入,并且动态设置可编辑,只有鼠标聚焦的时候,才是可编辑
实现逻辑
template
++
:contenteditable="currentRow === rowIndex && currentCol === columnIndex"
:ref="setRef(rowIndex, columnIndex)"
@click="focusCell(rowIndex, columnIndex, $event)"
++
steup
++
// 获取每个单元格的ref
const setRef = (rowIndex, columnIndex) => {
return (el) => {
if (!refs[rowIndex]) {
refs[rowIndex] = {};
}
refs[rowIndex][columnIndex] = el;
console.log("el", el);
};
};
const selectedRange = reactive({ start: null, end: null });
// 单击事件,获取焦点并且设置index
const focusCell = (rowIndex, columnIndex, $event) => {
selectedRange.start = { rowIndex, columnIndex };
selectedRange.end = { rowIndex, columnIndex };
currentRow.value = rowIndex;
currentCol.value = columnIndex;
setTimeout(() => {
refs[rowIndex][columnIndex].focus();
});
};
++
到这里,可编辑就做完了,但是编辑会有一个问题,contenteditable 输入的是元素本身的值,并没有和vue 做响应式数据,那赋值就可以了
template
++
@blur="setCell(rowIndex, columnIndex, $event.target.textContent)"
++
steup
++
// 失焦后,设置数据,这样的好处是减少频繁的数据更新,不然contenteditable会出现光标不准确的bug
const setCell = (rowIndex, columnIndex, value) => {
rowsList.value[rowIndex][columnIndex] = value;
};
++
⭐️ 功能描述
可以通过键盘的上下左右键盘,做单元的切换
(其实就是根据键盘事件,获取当前的索引,做可编辑可focus)
template
++
@keydown="handleKeydown(rowIndex, columnIndex, $event)"
++
steup
++
const handleKeydown = (rowIndex, columnIndex, event) => {
const { key } = event;
// 双击之后,锁住上下左右键的切换
if (keyFlag.value) {
return;
}
console.log("event", event);
switch (key) {
case "ArrowUp":
if (rowIndex > 0) {
keyDownFunc(rowIndex - 1, columnIndex);
}
break;
case "ArrowDown":
if (rowIndex < rowsList.value.length - 1) {
keyDownFunc(rowIndex + 1, columnIndex);
}
break;
case "ArrowLeft":
if (columnIndex > 0) {
keyDownFunc(rowIndex, columnIndex - 1);
}
break;
case "ArrowRight":
if (columnIndex < headers.value.length - 1) {
keyDownFunc(rowIndex, columnIndex + 1);
}
break;
}
};
// 上下左右键,去设置index,并且使当前单元格聚焦
const keyDownFunc = (rowIndex, columnIndex) => {
currentRow.value = rowIndex;
currentCol.value = columnIndex;
setTimeout(() => {
refs[rowIndex][columnIndex].focus();
console.log("refs[rowIndex][columnIndex]", refs[rowIndex][columnIndex]);
const el = refs[rowIndex][columnIndex];
// 此方式只针对于 input ,textarea
// const len = refs[rowIndex][columnIndex].innerText.length
// refs[rowIndex][columnIndex].setSelectionRange(0, len); // 设置光标到最后
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
});
};
++
但是会出现一个问题,就是永远只会上线左右,无法仅在当前单元格编辑。所以设计了双击解除编辑状态
template
++
@dblclick="handleDoubleClick"
++
steup
++
const handleDoubleClick = () => {
keyFlag.value = true;
selectedRange.start = null;
selectedRange.end = null;
};
++
⭐️ 功能描述
CV 复制粘贴
这个有点东西,你得先体验一下Excel里面的复制粘贴
比如我先复制一个Excel
粘贴出来
但是我们自己实现的table,复制下来没格式,这个时候,我们就需要自己去处理换行和空格
复制
template
++
@copy="handleCopy($event)"
++
steup
++
const focusCell = (rowIndex, columnIndex, $event) => {
console.log("$event", $event);
if ($event.shiftKey) { // shift
selectedRange.end = { rowIndex, columnIndex };
}
function handleCopy($event) {
currentRow.value = null;
// 自己处理格式,空格\t ,换行\n
const selectedValues = getSelectedValues();
if (selectedValues) {
ElMessage({
message: "复制成功",
type: "success"
});
}
$event.clipboardData.setData("text", selectedValues);
$event.preventDefault();
}
function getSelectedValues() {
const { rowIndex: startRow, columnIndex: startCol } = selectedRange.start;
const { rowIndex: endRow, columnIndex: endCol } = selectedRange.end;
let rowData = "";
for (let i = startRow; i <= endRow; i++) {
for (let j = startCol; j <= endCol; j++) {
rowData += rowsList.value[i][j] + "\t";
}
rowData += "\n";
}
return rowData;
}
++
粘贴
template
++
@paste="handlePaste($event)"
++
steup
++
function handlePaste($event) {
const clipboardData = $event.clipboardData;
if (clipboardData) {
ElMessage({
message: "粘贴成功",
type: "success"
});
}
const pastedData = clipboardData.getData("text");
setSelectedValues(pastedData);
$event.preventDefault();
}
function setSelectedValues(pastedData) {
console.log("pastedData", pastedData);
// 如果复制数据的大小与所选单元格不匹配,则退出
const { rowIndex: startRow, columnIndex: startCol } = selectedRange.start;
const { rowIndex: endRow, columnIndex: endCol } = selectedRange.end;
const rows = pastedData
.trim()
.split("\n")
.map((row) => row.split("\t"));
for (let i = 0; i < rows.length && startRow + i <= endRow; i++) {
const row = rows[i];
for (let j = 0; j < row.length && startCol + j <= endCol; j++) {
rowsList.value[startRow + i][startCol + j] = row[j];
}
}
}
++
就是一个函数,根据单元格的范围,是就return true。否就返回false
++
:class="{
selected: isSelected(rowIndex,columnIndex)
}"
++
steup
++
const isSelected = (rowIndex, columnIndex) => {
const { start, end } = selectedRange;
if (!start || !end) return false;
if (
rowIndex >= start.rowIndex &&
rowIndex <= end.rowIndex &&
columnIndex >= start.columnIndex &&
columnIndex <= end.columnIndex
) {
return true;
}
if (
rowIndex >= end.rowIndex &&
rowIndex <= start.rowIndex &&
columnIndex >= end.columnIndex &&
columnIndex <= start.columnIndex
) {
return true;
}
return false;
};
++
市面上也有很多其他的可编辑table
比如ant https://procomponents.ant.design/components/editable-table
vxe https://vxetable.cn/#/table/edit/popupForm
但是都不满足需求,所以自己造了一个轮子。
下面是github地址,欢迎start
https://gitee.com/Big_Cat-AK-47/edit-table.git