基于Vue TS
制作一个类似 Excel 合并单元格的功能。4个方向合可以进行框选合并单元格
这代码把自已都绕晕了。弄了几天,还没弄好, 没脾气了
自已感觉都太复杂化了。会者不难,难则不会吧
能帮弄好,诚心帮忙的兄弟私,或者追加,都行。
<template>
<div id="testid" style="background-color: goldenrod;">创建表单</div>
<table id='form_table' ref='ref_table' class="form-table" @mousedown.left="handleMouseDown">
<tr>
<td colspan="2">0-0</td>
<td rowspan="2">0-2</td>
<td>0-3</td>
<td>0-4</td>
</tr>
<tr>
<td>1-0</td>
<td>1-1</td>
<td colspan="2">1-3</td>
</tr>
<tr>
<td rowspan="2">2-0</td>
<td>2-1</td>
<td colspan="2">2-2</td>
<td>2-4</td>
</tr>
<tr>
<td>3-1</td>
<td colspan="3">3-2</td>
</tr>
<div id="select_range" :class="rangeBoxStyle" style="pointer-events: none;">
</div>
</table>
<button @click="mergeCell"> 合并单元格 </button>
<button @click="splitCell"> 分解单元格 </button>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
const ref_table = ref<HTMLTableElement>()
const rangeBoxStyle = ref<string>('range-box-none')
const startTd = ref<HTMLTableCellElement>()
const endTd = ref<HTMLTableCellElement>()
const cellRange = reactive({
start_td: { a: { x: 0, y: 0 }, b: { x: 0, y: 0 }, c: { x: 0, y: 0 }, d: { x: 0, y: 0 } }, // 第一个选取 td 三个角的座标
end_td: { a: { x: 0, y: 0 }, b: { x: 0, y: 0 }, c: { x: 0, y: 0 }, d: { x: 0, y: 0 } }, // 最后一个选取 td 三个角的座标
range_box: { left: '', top: '', width: '', height: '' }, // 框选 DIV
})
// 移除 Dom 的函数
function removeElement(_element: any) {
const _parentElement = _element.parentNode;
if (_parentElement) {
_parentElement.removeChild(_element);
}
}
//初始化单元格,目的: 给所有的单元格做上标记
function tableInit() {
// 获取表格对象
const tb = ref_table.value as HTMLTableElement
//补全表格的td
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
for (let x = 0; x < tb.rows[i].children.length; x++) { // 循环每行的 td
const tb_td = tb.rows[i].children[x] as HTMLTableCellElement
if (tb_td.colSpan > 1) { // 假如 当前td 的 colSpan > 1
for (let a = 0; a < tb_td.colSpan - 1; a++) { tb.rows[i].insertCell(x + a + 1).className = 'td-hidden' }
}
if (tb_td.rowSpan > 1) { // 假如 当前td 的 rowSpan > 1,向下面行 插入 td
for (let y = i + 1; y < i + 1 + tb_td.rowSpan - 1; y++) { //
tb.rows[y].insertCell(x).className = 'td-hidden'
}
}
}
}
// 给每个单元格做上标记 td_rc ={ r:初始行数 , c:初始列数, maxc:本列列数+要合并的列数 , maxr:本行行数 + 要合并的行数 }
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
for (let x = 0; x < tb.rows[i].children.length; x++) { // 循环每行的 td
const tb_td = tb.rows[i].children[x] as HTMLTableCellElement
tb_td.setAttribute('td_rc', `{"r":${i},"c":${x},"maxc":${x + tb_td.colSpan - 1},"maxr":${i + tb_td.rowSpan - 1}}`)
}
}
// 移除补全的 td
const td_hidden = document.getElementsByClassName('td-hidden')
for (let len = td_hidden.length, i = len - 1; i >= 0; i--) {
removeElement(td_hidden[i])
}
}
// 检查table的完整性
function checkMergeTable(table_id: string) {
// 获取表格对象
const tb = document.getElementById(table_id) as HTMLTableElement
if (tb.rows.length == 0) return false;
if (tb.rows[0].cells.length == 0) return false;
// 表格总列数计算, 计算第一行
let col_total = 0
for (let i = 0; i < tb.rows[0].children.length; i++) { // count columns of first row
col_total = col_total + (tb.rows[0].children[i] as HTMLTableCellElement).colSpan // 表格总列数
}
// 单元格 没有执行 合并前,表格每行的总列数
const row_span: any = {}
for (let i = 0; i < tb.rows.length; i++) {
row_span[i] = col_total // 写入对象: row_span ={'行号':列数','行号':列数','行号':列数',...}
}
// 根据表格Td的 rowSpan colSpan 属性,改写 + - row_span 的值
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
for (let x = 0; x < tb.rows[i].children.length; x++) { // 循环每行的 td
const tb_td = tb.rows[i].children[x] as HTMLTableCellElement
if (tb_td.colSpan > 1) { // 假如 当前td 的 colSpan > 1
row_span[i] = row_span[i] - tb_td.colSpan + 1 // 则减少相应的列
}
if (tb_td.rowSpan > 1) { // 假如 当前td 的 rowSpan > 1
const margin_row = tb_td.rowSpan - 1 // 当前td, 要向下合并的行数
for (let y = i + 1; y <= i + margin_row; y++) { // i+1 行 至 i+ margin_row 行, 每行则 减少 1列
row_span[y] = row_span[y] - 1
}
}
}
}
// 比对列数
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
if ((tb.rows[i] as HTMLTableRowElement).cells.length != row_span[i]) { // 假如 列数不对
console.log('表格错误!', row_span[i])
console.log('tb.rows[i].children', tb.rows[i].children)
console.log('tb.rows[i].cells', tb.rows[i].cells,)
return false
}
}
console.log('表格检查', row_span)
return true;
}
// 执行合并
function mergeCell() {
if (startTd.value == endTd.value || typeof startTd.value == 'undefined' || typeof endTd.value == 'undefined') return
tableInit()
checkMergeTable('form_table')
const sTD = startTd.value as HTMLTableCellElement
const eTD = endTd.value as HTMLTableCellElement
const sRC = JSON.parse(sTD.getAttribute('td_rc') as string)
const eRC = JSON.parse(eTD.getAttribute('td_rc') as string)
const s_row = sRC.r // 起始行
const e_row = eRC.maxr // 结束行
const rowList = ref_table.value?.children as HTMLCollection
let merge_row = Math.abs(sRC.r - eRC.maxr) + 1
let merge_col = Math.abs(sRC.c - eRC.maxc) + 1
// 检查通过 执行合并
if (!checkRang()) return // 检查函数
sTD.rowSpan = merge_row
sTD.colSpan = merge_col
for (let i = s_row; i <= e_row; i++) {
for (let x = rowList[i].children.length - 1; x >= 0; x--) {
const td = rowList[i].children[x]
const td_rc = JSON.parse((td as HTMLTableCellElement).getAttribute('td_rc') as string)
if (td != sTD && td_rc.r >= s_row && td_rc.r <= eRC.maxr && td_rc.c >= sRC.c && td_rc.c <= eRC.maxc) {
(rowList[i] as HTMLTableRowElement).deleteCell(x)
console.log(`删除单元格${td.innerHTML}`)
}
}
}
checkMergeTable('form_table')
endTd.value = startTd.value
// 给当前元素套一层, 绿色边框的div, 并显示出来
cellRange.range_box.left = (sTD.offsetLeft - 1) + 'px' // 绿色边框的div position 定位的 left, 值绑定在 style 类属性中
cellRange.range_box.top = (sTD.offsetTop - 1) + 'px' // 绿色边框的div position 定位的 top, 值绑定在 style 类属性中
cellRange.range_box.width = (sTD.offsetWidth + 1) + 'px' // 绿色边框的div 宽度
cellRange.range_box.height = (sTD.offsetHeight + 2) + 'px' // 绿色边框的div 高度
}
// 分解单元格
function splitCell() {
if (startTd.value?.nodeName !== 'TD' || startTd.value !== endTd.value || startTd.value.rowSpan == 1 && startTd.value.colSpan == 1) return console.log('不可分解')
console.log('可以分解.............')
const tb = ref_table.value as HTMLTableElement
const td_row = (startTd.value.parentNode as HTMLTableRowElement)
const td_col = startTd.value.cellIndex // 当前列的 index
const init_col = JSON.parse(startTd.value.getAttribute('td_rc') as string).c // 没合并前的 index
const init_rowspan = startTd.value.rowSpan
const init_colspan = startTd.value.colSpan
// 分解行
let new_td
for (let i = td_row.rowIndex; i < td_row.rowIndex + init_rowspan; i++) {
const td_rc = JSON.parse(tb.rows[i].children[0].getAttribute('td_rc') as string)
// 计算插入位置
let in_index
for (let x = 0; x < tb.rows[i].children.length; x++) {
const td_rc1 = JSON.parse(tb.rows[i].children[x].getAttribute('td_rc') as string)
if(typeof td_rc1 == 'undefined' || td_rc1 == null) {
break
} else {
console.log('td_rc1x', td_rc1)
if (td_rc1.maxc == init_col - 1) { // 说明这列刚好是 在 分解Td 的前面
in_index = (tb.rows[i].children[x] as HTMLTableCellElement).cellIndex + 1
console.log('1到这了in_index', in_index)
} else {
in_index = 0
}
}
}
if (init_rowspan > 1 && i > td_row.rowIndex ) { // 第一行不要加TD
new_td = tb.rows[i].insertCell(in_index)
if (i == td_row.rowIndex + startTd.value.rowSpan - 1) { endTd.value = new_td } // 最后一个 添加 TD 时
} else {
startTd.value.colSpan = 1
}
if (init_colspan > 1) {
console.log('2到这了in_index', in_index)
for (let y = td_col + 1; y < td_col + init_colspan; y++) {
if( i == td_row.rowIndex){
const new_td = tb.rows[i].insertCell(y); new_td.innerText = '新列'
} else {
const new_td = tb.rows[i].insertCell(in_index); new_td.innerText = '新列'
}
if (i == td_row.rowIndex + init_rowspan - 1 && y == td_col + init_colspan - 1) { endTd.value = new_td }
}
}
}
startTd.value.rowSpan = 1
startTd.value.colSpan = 1
console.log('远行到这了')
tableInit()
}
// 检查合并范围的合法性
function checkRang() {
const sTD = startTd.value as HTMLTableCellElement
const eTD = endTd.value as HTMLTableCellElement
const sRC = JSON.parse(sTD.getAttribute('td_rc') as string)
const eRC = JSON.parse(eTD.getAttribute('td_rc') as string)
const s_row = sRC.r // 起始行
const e_row = eRC.maxr // 结束行
const rowList = ref_table.value?.children as HTMLCollection
let test = true
// 左侧检查
if (eRC.maxr != sRC.maxr) { // 左边只有一个元素时,不检查
for (let i = s_row; i <= e_row; i++) {
for (let x = 0; x < rowList[i].children.length; x++) {
const td = rowList[i].children[x]
const td_rc = JSON.parse((td as HTMLTableCellElement).getAttribute('td_rc') as string)
if (td != sTD && td_rc.r >= s_row && td_rc.r <= eRC.maxr && td_rc.maxc >= sRC.c && td_rc.c <= eRC.maxc) {
console.log(`${i},当前行${sRC.maxr},`)
if (i > sRC.maxr) {
if (td_rc.c == sRC.c) { // 有左边 第一个 对齐的单元格时
test = true
console.log(`${i},左侧检查通过:第几行:${i},第几列 ${x}!`)
if (td_rc.maxr > i) { // 假如这个单元格向下有合并,跳过合并行 再向下检查
i = td_rc.maxr
}
break
} else {
test = false
}
}
}
}
if (!test) {
console.log(`左侧不能合并`)
return false
}
}
}
// 右侧检查
for (let i = s_row; i <= e_row; i++) {
for (let x = 0; x < rowList[i].children.length; x++) {
const td = rowList[i].children[x] as HTMLTableCellElement
const td_rc = JSON.parse(td.getAttribute('td_rc') as string)
if (td != sTD && td_rc.r >= s_row && td_rc.r <= eRC.maxr && td_rc.maxc >= sRC.c && td_rc.c <= eRC.maxc) {
if (td_rc.maxc == eRC.maxc) { // 有左边 第一个 对齐的单元格时
test = true
if (td_rc.maxr > i) { //单元格向合并了,跳过合并行 再检查
i = td_rc.maxr
}
console.log(`右侧检查通过:第几行:${i},第几列 ${x}!`)
break
} else {
test = false
}
}
}
if (!test) {
console.log(`右侧不能合并`)
return false
}
}
// 上侧检查
let amend = sRC.maxc - sTD.cellIndex // 修正对比 数值
if (eRC.maxc != sRC.maxc) { // 第一行只有一个元素时 不用检查
for (let x = sTD.cellIndex + 1; x < rowList[s_row].children.length; x++) { // 从第二元素开始检查
const td = rowList[s_row].children[x] as HTMLTableCellElement
const td_rc = JSON.parse(td.getAttribute('td_rc') as string)
if (td_rc.c !== (x + amend)) { // 如果少了一列,说明是从上面合并下来了一个位置
console.log(`上侧不能合并,问题出在列号为:${x} 的单元格,${td_rc.c} 不等于 ${x + amend}!`)
test = false
break
}
if (td_rc.maxc > x) { x = td_rc.maxc }
if (td_rc.maxc == eRC.maxc) { break }
}
}
if (!test) {
return false
}
// 下侧检查
for (let x = sTD.cellIndex; x < rowList[e_row].children.length; x++) {
const td = rowList[e_row].children[x] as HTMLTableCellElement
const td_rc = JSON.parse(td.getAttribute('td_rc') as string)
if (td_rc.maxr !== e_row) {
test = false
console.log(`下侧不能合并,问题出在第 ${e_row}行,第${x}列`)
break
}
if (td_rc.maxc == eRC.maxc) { break }
}
if (!test) { return false } else { return true }
}
//框选单元格操作
const handleMouseDown = (event: any) => {
if (event.target.nodeName != 'TD') return
console.log('第一个单元格', event.target)
tableInit()
startTd.value = event.target // first Dom
const sTD = startTd.value as HTMLTableCellElement
// 记录第一个点击目标 4个角的座标 x,y 由于框选的方向不一样,所以要记录4个角的座标 x,y
cellRange.start_td = {
a: { x: event.target.offsetLeft, y: event.target.offsetTop },
b: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop },
c: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop + event.target.offsetHeight },
d: { x: event.target.offsetLeft, y: event.target.offsetTop + event.target.offsetHeight },
}
// 给当前元素套一层, 绿色边框的div, 并显示出来
cellRange.range_box.left = (sTD.offsetLeft - 1) + 'px' // 绿色边框的div position 定位的 left, 值绑定在 style 类属性中
cellRange.range_box.top = (sTD.offsetTop - 1) + 'px'
cellRange.range_box.width = (sTD.offsetWidth + 1) + 'px' // 绿色边框的div 宽度
cellRange.range_box.height = (sTD.offsetHeight + 2) + 'px' // 绿色边框的div 高度
rangeBoxStyle.value = 'range-box-show' // 显示出来 (更改类名来显示)
const form_table = document.getElementById('form_table') as HTMLTableElement
form_table.addEventListener("mousemove", handleMouseMove) //监听鼠标移动事件
form_table.addEventListener("mouseup", handleMouseUp) //监听鼠标抬起事件
}
function handleMouseUp(event: any) {
endTd.value = event.target
const form_table = document.getElementById('form_table') as HTMLTableElement
form_table.removeEventListener("mousemove", handleMouseMove);
form_table.removeEventListener("mouseup", handleMouseUp);
// cellRange.is_show_mask = false;
}
function handleMouseMove(event: any) {
// 根据鼠标移动的位置来调整,绿色边框的div的大小和位置:
if (event.target.nodeName != 'TD') return
endTd.value = event.target as HTMLTableCellElement // 最后一个 框选 的 Td
cellRange.end_td = { // 当前 Td 的位置
a: { x: event.target.offsetLeft, y: event.target.offsetTop },
b: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop },
c: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop + event.target.offsetHeight },
d: { x: event.target.offsetLeft, y: event.target.offsetTop + event.target.offsetHeight },
}
// 更新 绿色边框的div
if (cellRange.end_td.a.x >= cellRange.start_td.a.x && cellRange.end_td.a.y >= cellRange.start_td.a.y) { // 右下
cellRange.range_box.left = (cellRange.start_td.a.x - 1) + 'px'
if (cellRange.end_td.b.x < cellRange.start_td.b.x) {
cellRange.range_box.width = (cellRange.start_td.b.x - cellRange.start_td.a.x + 1) + 'px'
} else {
cellRange.range_box.width = (cellRange.end_td.c.x - cellRange.start_td.a.x + 1) + 'px' // 绿色边框的div 宽度
}
cellRange.range_box.top = (cellRange.start_td.a.y - 1) + 'px' // 绿色边框的div position 定位的 top
cellRange.range_box.height = (cellRange.end_td.c.y - cellRange.start_td.a.y + 2) + 'px' // 绿色边框的div 高
}
}
onMounted(() => {
})
</script>
<style lang="scss">
.range-box-none {
display: none;
position: absolute;
}
.range-box-show {
border: 2px solid rgb(66, 166, 66);
display: block;
position: absolute;
left: v-bind('cellRange.range_box.left');
top: v-bind('cellRange.range_box.top'); // offsetTop
width: v-bind('cellRange.range_box.width'); // offsetLeft
height: v-bind('cellRange.range_box.height'); // offsetLeft
}
.form-table {
margin: auto;
margin-top: 100px;
font-size: 12px;
position: relative;
// 让文字不可选
user-select: none;
-webkit-user-seletct: none;
-moz-user-seletct: none;
td {
border: 0.5px solid;
border-color: rgb(97, 97, 97);
}
td {
width: 200px;
}
.td-hidden {
display: none;
background-color: rgb(152, 152, 152);
}
// .td-hidden::before {
// position: absolute;
// content: '你好';
// color: white;
// }
td::before {
position: relative;
left: -10px;
content: attr(td_rc);
color: rgb(234, 173, 94);
}
}
</style>