table cell 合并,这代码问题太多了,弄了好久没弄好


<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="tableInit"> Test </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
})
const MMRC = reactive({ startRowIndex: -1, startCellIndex: -1, endRowIndex: -1, endCellIndex: -1 })

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
        //  tb_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}}`)
    }
  }

  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 // 表格总列数
  }
  // 单元格没有合并前,表格每行原始列数: 对象: row_span ={'行号':列数','行号':列数','行号':列数',...}
  const row_span: any = {}
  for (let i = 0; i < tb.rows.length; i++) {
    row_span[i] = col_total
  }
  // 根据表格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() {
  checkMergeTable('form_table')
  const tb = document.getElementById('form_table') as HTMLTableElement
  
  // 计算出 绿色边框的div 位置 和 宽高
  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

  sTD.rowSpan = merge_row
  sTD.colSpan = merge_col
  if(sTD == eTD) return
  for (let i = s_row; i <= e_row; i++) {
    console.log('.....i..', 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('sTD',sTD)
  checkMergeTable('form_table')
    // 给当前元素套一层,  绿色边框的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 高度


}
//框选单元格操作

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) {
  const tb = ref_table.value
  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 addSelectedClass() {
  let selected = false, t;
  const tb = ref_table.value as HTMLTableElement
  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
      const rc = JSON.parse(tb_td.getAttribute('td_rc') as string)
      selected = rc.r >= MMRC.startRowIndex && rc.r <= MMRC.endRowIndex && rc.c >= MMRC.startCellIndex && rc.c <= MMRC.endCellIndex;
      if (!selected && rc.maxc) { //合并过的单元格,判断另外3(左下,右上,右下)个角的行列是否在区域内
        selected =
          (rc.maxr >= MMRC.startRowIndex && rc.maxr <= MMRC.endRowIndex && rc.c >= MMRC.startCellIndex && rc.c <= MMRC.endCellIndex) ||//左下
          (rc.r >= MMRC.startRowIndex && rc.r <= MMRC.endRowIndex && rc.maxc >= MMRC.startCellIndex && rc.maxc <= MMRC.endCellIndex) ||//右上
          (rc.maxr >= MMRC.startRowIndex && rc.maxr <= MMRC.endRowIndex && rc.maxc >= MMRC.startCellIndex && rc.maxc <= MMRC.endCellIndex);//右下
      }
      if (selected) {
        tb_td.setAttribute('className', 'selected')
      }
    }
  }

  // let rangeChange = false;
  // 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
  //     const rc = JSON.parse(tb_td.getAttribute('td_rc') as string)
  //     t = MMRC.startRowIndex;
  //     MMRC.startRowIndex = Math.min(MMRC.startRowIndex, rc.r);
  //     rangeChange = rangeChange || MMRC.startRowIndex != t;

  //     t = MMRC.endRowIndex;
  //     MMRC.endRowIndex = Math.max(MMRC.endRowIndex, rc.maxr || rc.r);
  //     rangeChange = rangeChange || MMRC.endRowIndex != t;

  //     t = MMRC.startCellIndex;
  //     MMRC.startCellIndex = Math.min(MMRC.startCellIndex, rc.c);
  //     rangeChange = rangeChange || MMRC.startCellIndex != t;

  //     t = MMRC.endCellIndex;
  //     MMRC.endCellIndex = Math.max(MMRC.endCellIndex, rc.maxc || rc.c);
  //     rangeChange = rangeChange || MMRC.endCellIndex != t;

  //   }
  // }
  // //注意这里如果用代码选中过合并的单元格需要重新执行选中操作
  // if (rangeChange) addSelectedClass()
  // console.log('selected', selected)
}

function handleMouseMove(event: any) {
  // 根据鼠标移动的位置来调整,绿色边框的div的大小和位置:
  if (event.target.nodeName != 'TD') return
  endTd.value = event.target as HTMLTableCellElement  // last Dom

  cellRange.end_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 },
  }

  if (cellRange.end_td.a.x >= cellRange.start_td.a.x) { // 右移
    cellRange.range_box.left = (cellRange.start_td.a.x - 1) + 'px'
    if (cellRange.end_td.b.x < cellRange.start_td.b.x) {
      console.log('右.......')
      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 宽度
    }

    if (cellRange.end_td.a.y >= cellRange.start_td.a.y) { // 右下
      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 高度
    } else {  // 右上
      cellRange.range_box.top = (cellRange.end_td.a.y - 1) + 'px'
      cellRange.range_box.height = (cellRange.start_td.c.y - cellRange.end_td.a.y + 2) + 'px'
    }
  } else { // 左移
    console.log('左.......')
    cellRange.range_box.left = (cellRange.end_td.a.x - 1) + 'px'
    cellRange.range_box.width = (cellRange.start_td.b.x - cellRange.end_td.a.x + 1) + 'px'

    if (cellRange.end_td.c.y <= cellRange.start_td.c.y) { // 左上
      console.log('左上')
      cellRange.range_box.top = (cellRange.end_td.a.y - 1) + 'px'
      cellRange.range_box.height = (cellRange.start_td.c.y - cellRange.end_td.a.y + 2) + 'px'
    } else { // 左下
      console.log('左下')
      cellRange.range_box.top = (cellRange.start_td.b.y - 1) + 'px'
      cellRange.range_box.height = (cellRange.end_td.d.y - cellRange.start_td.b.y + 2) + 'px'
    }
  }
}

const handleMouseDown_1 = (event: any) => {
  console.log('点了这个单元格:', document.styleSheets[0])
  if (event.target.nodeName != 'TD') return
  tableInit()
  // 记录第一个点击目标 4个角的座标 x,y 由于框选的方向不一样,所以要记录4个角的座标 x,y
  startTd.value = event.target // first Dom
  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 = (cellRange.start_td.a.x - 1) + 'px' // 绿色边框的div  position 定位的 left, 值绑定在 style 类属性中
  cellRange.range_box.top = (cellRange.start_td.a.y - 1) + 'px'  // 绿色边框的div  position 定位的 top, 值绑定在 style 类属性中
  cellRange.range_box.width = (event.target.offsetWidth + 1) + 'px' // 绿色边框的div 宽度
  cellRange.range_box.height = (event.target.offsetHeight + 2) + 'px' // 绿色边框的div 高度
  rangeBoxStyle.value = 'range-box-show' // 显示出来 (更改类名来显示)
  const form_table = document.getElementById('form_table') as HTMLTableElement

  // const testobj = document.getElementById('testid') as HTMLDivElement

  form_table.addEventListener("mousemove", handleMouseMove) //监听鼠标移动事件
  form_table.addEventListener("mouseup", handleMouseUp) //监听鼠标抬起事件
  // console.log(`第一个对象clientTop:${event.target.id}`, form_table.getBoundingClientRect())
  // console.log('点了这个单元格:', JSON.parse(event.target.getAttribute('td_rc')))

}

function handleMouseUp_1(event: any) {
  // endTd.value = event.target  // last Dom
  // console.log(event.target.innerHTML, event.target.parentElement.rowIndex, event.target.cellIndex)
  // rangeBoxStyle.value = 'range-box-none'

  const form_table = document.getElementById('form_table') as HTMLTableElement
  form_table.removeEventListener("mousemove", handleMouseMove);
  form_table.removeEventListener("mouseup", handleMouseUp);
  // cellRange.is_show_mask = false;
}



onMounted(() => {

})

</script>
<style lang="scss">
@import '../../assets/css/handle';

.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;
   
  }

  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>