满口金牙 2022-06-17 01:42 采纳率: 91.5%
浏览 132
已结题

Vue TS, 会者不难,难则不会,这代码自已都被绕晕了,请教

基于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>

  • 写回答

4条回答 默认 最新

查看更多回答(3条)

报告相同问题?

问题事件

  • 系统已结题 6月28日
  • 已采纳回答 6月20日
  • 创建了问题 6月17日

悬赏问题

  • ¥15 netty整合springboot之后自动重连失效
  • ¥15 悬赏!微信开发者工具报错,求帮改
  • ¥20 wireshark抓不到vlan
  • ¥20 关于#stm32#的问题:需要指导自动酸碱滴定仪的原理图程序代码及仿真
  • ¥20 设计一款异域新娘的视频相亲软件需要哪些技术支持
  • ¥15 stata安慰剂检验作图但是真实值不出现在图上
  • ¥15 c程序不知道为什么得不到结果
  • ¥40 复杂的限制性的商函数处理
  • ¥15 程序不包含适用于入口点的静态Main方法
  • ¥15 素材场景中光线烘焙后灯光失效