IT评测·应用市场-qidao123.com技术社区

标题: Vue3+element-ui 实现可编辑表格,鼠标右键自界说菜单(复制行列,粘贴行列 [打印本页]

作者: 乌市泽哥    时间: 2024-11-3 17:49
标题: Vue3+element-ui 实现可编辑表格,鼠标右键自界说菜单(复制行列,粘贴行列
一、功能

在vue3项目中实现可编辑的图表,可以点击单位格动态录入表头和表项数据。鼠标右键单击弹出自界说菜单,具有行列的复制、粘贴、插入、删除等操作。
二、实现

1.表格与数据结构

表格我直接采用了element-ui -> el-table 的结构。为了实现表项的自界说编辑,还需要借助el-table的内置插槽进行处置惩罚。
  1. <el-table
  2.     :data="tableData"
  3.     height="300"
  4.     border
  5.     style="width: 100%;z-index: 2;"
  6.     :header-cell-style="{
  7.     'font-size': '1.2rem',
  8.     'line-height': '2rem',
  9.     'font-weight': '600',
  10.     }">
  11.         <el-table-column label="" type="index" width="40" fixed></el-table-column>
  12.         <el-table-column
  13.             v-for="(column,index) in columnList"
  14.             :prop="column.prop"
  15.             :label="column.label"
  16.             >
  17.             <!-- 自定义表头 -->
  18.             <template #header>
  19.                 <p> {{column.label}} </p>
  20.             </template>
  21.             <template #default="{ row }">
  22.                 <p> {{column.label}} </p>
  23.             </template>
  24.         </el-table-column>
  25. </el-table>
复制代码
接下来构建表内数据tableData和columnList的结构。由于想要实现点击单位格可以输入数据,那么就需要添加一个“show”属性控制当前表格是否为编辑模式,也就是说控制当前单位格是`<p> {{column.label}} </p>`展示形式还是`<el-input />`输入形式。数据结构示例如下:
  1. const tableData = ref([
  2.     {
  3.         '日期':{ content: '2024-08-08', show: true },
  4.         '姓名':{ content: '张三', show: true },
  5.         '年龄':{ content: 12, show: true },
  6.     },
  7.     {
  8.         '日期':{ content: '2024-08-08', show: true },
  9.         '姓名':{ content: '张三', show: true },
  10.         '年龄':{ content: 12, show: true },
  11.     }
  12. ])
  13. const columnList = ref([
  14.     { prop:"日期", label:"日期", show:true, },
  15.     { prop:"姓名", label:"姓名", show:true, },
  16.     { prop:"年龄", label:"年龄", show:true, }
  17. ])
复制代码
借助“show”属性我们就可以完善一下表格结构:
  1. <el-table
  2.     :data="tableData"
  3.     height="300"
  4.     border
  5.     style="width: 100%;z-index: 2;"
  6.     :header-cell-style="{
  7.     'font-size': '1.2rem',
  8.     'line-height': '2rem',
  9.     'font-weight': '600',
  10.     }">
  11.         <el-table-column label="" type="index" width="40" fixed></el-table-column>
  12.         <el-table-column
  13.             v-for="(column,index) in columnList"
  14.             :prop="column.prop"
  15.             :label="column.label"
  16.             >
  17.             <!-- 自定义表头 -->
  18.             <template #header>
  19.                 <p  
  20.                     v-show="column.show"
  21.                     >
  22.                     {{column.label}}
  23.                 </p>
  24.                 <el-input
  25.                     v-show="!column.show"
  26.                     v-model="column.label"
  27.                    >
  28.                 </el-input>
  29.             </template>
  30.             <template #default="{ row }">
  31.                 <p  
  32.                     v-show="row[column.prop].show"
  33.                     >
  34.                     {{row[column.prop].content}}
  35.                 </p>
  36.                 <el-input
  37.                     type="textarea"
  38.                     :autosize="{minRows:1,maxRows:4}"
  39.                     v-show="!row[column.prop].show"
  40.                     v-model="row[column.prop].content"
  41.                    >
  42.                 </el-input>
  43.             </template>
  44.         </el-table-column>
  45. </el-table>
复制代码
2.单位格内编辑输入标签与展示标签切换的函数事件

为单位格内标签添加双击事件dblclick控制“show”属性修改,同时在el-input上添加失去核心也可以取消编辑的函数事件。(取消编辑同时添加双击事件和失去核心事件是由于若只添加双击事件,用户在编辑后必须要双击才能将输入框变为展示标签;若只添加失去核心事件,用户必须在双击控制输入框显示后,点击输入框聚焦,再点击外部失焦才可以将输入框隐藏)


  1. <el-table
  2.     :data="tableData"
  3.     height="300"
  4.     border
  5.     style="width: 100%;z-index: 2;"
  6.     :header-cell-style="{
  7.     'font-size': '1.2rem',
  8.     'line-height': '2rem',
  9.     'font-weight': '600',
  10.     }">
  11.         <el-table-column label="" type="index" width="40" fixed></el-table-column>
  12.         <el-table-column
  13.             v-for="(column,index) in columnList"
  14.             :prop="column.prop"
  15.             :label="column.label"
  16.             >
  17.             <!-- 自定义表头 -->
  18.             <template #header>
  19.                 <p  
  20.                     v-show="column.show"
  21.                     @dblclick="$event => handleEdit(column, $event.target)"
  22.                     >
  23.                     {{column.label}}
  24.                 </p>
  25.                 <el-input
  26.                     v-show="!column.show"
  27.                     v-model="column.label"
  28.                    @dblclick="column.show = !column.show"
  29.                    @blur="column.show = true">
  30.                 </el-input>
  31.             </template>
  32.             <template #default="{ row }">
  33.                 <p  
  34.                     v-show="row[column.prop].show"
  35.                     @dblclick="$event => handleEdit(row[column.prop], $event.target)"
  36.                     >
  37.                     {{row[column.prop].content}}
  38.                 </p>
  39.                 <el-input
  40.                     type="textarea"
  41.                     :autosize="{minRows:1,maxRows:4}"
  42.                     v-show="!row[column.prop].show"
  43.                     v-model="row[column.prop].content"
  44.                    @dblclick="row[column.prop].show = !row[column.prop].show"
  45.                    @blur="row[column.prop].show = true">
  46.                 </el-input>
  47.             </template>
  48.         </el-table-column>
  49. </el-table>
复制代码
  1. function handleEdit(cell, pEl) {
  2.     console.log("双击被调用");
  3.     const editIputEl = Array.from(pEl.nextSibling.childNodes).find(n => ['INPUT','TEXTAREA'].includes(n.tagName))
  4.     cell.show = false
  5.     editIputEl && nextTick(() => {
  6.       editIputEl.focus()
  7.     })
  8. }
复制代码
3.扫除欣赏器默认鼠标右键事件,修改为自界说事件。
  1. <el-table
  2.     :data="tableData"
  3.     height="700"
  4.     border
  5.     @header-contextmenu="(column, $event) => rightClick(null, column, $event)"
  6.     @row-contextmenu="rightClick"
  7.     :row-class-name="tableRowClassName"
  8.     style="width: 100%;z-index: 4;"
  9.     :header-cell-style="{
  10.     'font-size': '1.2rem',
  11.     'line-height': '3.2rem',
  12.     'font-weight': '600',
  13.     }">
  14. </el-table>
复制代码
对表行和表头添加鼠标右键点击事件。


  1. const curTarget =  ref(
  2.     {                 // 当前目标信息
  3.         rowIdx: null,              // 行下标
  4.         colIdx: null,              // 列下标
  5.         isHead: undefined          // 当前目标是表头?
  6.     }
  7. )
  8. function rightClick(row, column, $event) {
  9.   // 阻止浏览器自带的右键菜单弹出
  10.   $event.preventDefault()
  11.   if(column.index === null) return
  12.   // 表格容器的位置
  13.   const { x: tbX, y: tbY } = tbContainerRef.value.getBoundingClientRect()
  14.   
  15.   // 当前鼠标位置
  16.   const { x: pX, y: pY } = $event
  17.   // 定位菜单
  18.   const ele = document.getElementById('rightMenu')
  19.   ele.style.top = pY + 'px'
  20.   ele.style.left = pX + 'px'
  21.   // 边界调整
  22.   if(window.innerWidth - 140 < pX - tbX) {
  23.     ele.style.left = 'unset'
  24.     ele.style.right = 0
  25.   }
  26.   ele.style.display="block" // 原生代码,显示
  27.   // 当前目标
  28.   curTarget.value = {
  29.     rowIdx: row ? row.row_index : null,
  30.     colIdx: column.index,
  31.     isHead: !row
  32.   }
  33. }
复制代码
菜单结构:
  1. <div id="rightMenu" class="rightMenu" >
  2.         <div class="item" @click="copyCurrentRow">
  3.                 <span style="margin-left: 4px"> 复制当前行</span>
  4.         </div>
  5.         <div class="item" @click="pasteCurrentRow">
  6.                 <span style="margin-left: 4px"> 粘贴到当前行</span>
  7.         </div>
  8.         <div class="item" @click="copyCurrentColumn">
  9.                 <span style="margin-left: 4px"> 复制当前列</span>
  10.         </div>
  11.         <div class="item add-column" @click="pasteCurrentColumn">
  12.                 <span style="margin-left: 4px"> 粘贴到当前列</span>
  13.                 <div class="column-box"></div>
  14.         </div>
  15.         <div class="item delete-table">
  16.                 <el-popconfirm title="确定删除该行吗?" @confirm="delRow" placement="top">
  17.                             <template #reference>
  18.                                     <span style="margin-left: 4px"> 删除当前行</span>
  19.                             </template>
  20.                 </el-popconfirm>
  21.         </div>
  22.         <div class="item delete-table"  >
  23.                 <el-popconfirm title="确定删除该列吗?" @confirm="delColumn" placement="top">
  24.                         <template #reference>
  25.                                     <span style="margin-left: 4px"> 删除当前列</span>
  26.                          </template>
  27.                  </el-popconfirm>
  28.         </div>
  29.         <div class="item" @click="addRow()">
  30.                 <span style="margin-left: 4px"> 上方插入*空白行</span>
  31.         </div>
  32.         <div class="item" @click="addRow(true)">
  33.                 <span style="margin-left: 4px"> 下方插入空白行</span>
  34.         </div>
  35.         <div class="item" @click="addColumn()">
  36.                 <span style="margin-left: 4px"> 左侧插入空白列</span>
  37.         </div>
  38.         <div class="item" @click="addColumn(true)">
  39.                 <span style="margin-left: 4px"> 右侧插入空白列</span>
  40.         </div>
  41. </div>
复制代码
除了控制菜单显示还要控制菜单隐藏
  1. const hideMenu = (e) => {
  2.     const menu = document.getElementById('rightMenu');
  3.     menu.style.display="none" // 原生代码,显示
  4. };
  5. document.addEventListener("click", hideMenu);
复制代码
4.复制、粘贴、插入、删除等excel表格工具功能。

复制行列内容时,注意一定要利用深拷贝!
  1. // 新增行
  2. function addRow(later) {
  3.     hideMenu()
  4.     if(curTarget.value.rowIdx === null) return
  5.     const idx = later ? curTarget.value.rowIdx + 1 : curTarget.value.rowIdx
  6.     let obj = {}
  7.     columnList.value.forEach(p => {
  8.       obj[p.prop] = { content: '', show: true }
  9.     })
  10.     tableData.value.splice(idx, 0, obj)
  11. }
  12. // 删除行
  13. const popperVisibleRow = ref(false)
  14. function delRow() {
  15.     hideMenu()
  16.     curTarget.value.rowIdx !== null && tableData.value.splice(curTarget.value.rowIdx, 1)
  17. }
  18. const count_col = ref(0)
  19. // 新增列
  20. function addColumn(later) {
  21.     hideMenu()
  22.   const idx = later ? curTarget.value.colIdx + 1 : curTarget.value.colIdx
  23.   let key = 'col_' + ++count_col.value
  24.   for(let i =0;i<columnList.value.length;i++){
  25.     if(key == columnList.value[i].prop){
  26.         key = 'col_' + ++count_col.value
  27.     }
  28.   }
  29.   let obj = { prop: key, label: key, show: true }
  30.   columnList.value.splice(idx, 0, obj)
  31.   console.log(columnList.value);
  32.   tableData.value.forEach(p => {
  33.     p[obj.prop] = { content: '', show: true }
  34.   })
  35. }
  36. // 删除列
  37. function delColumn() {
  38.     hideMenu()
  39.   let colKey = columnList.value[curTarget.value.colIdx].prop
  40.   columnList.value.splice(curTarget.value.colIdx, 1)
  41.   tableData.value.forEach(p => delete p[colKey] )
  42. }
  43. //复制行列 拷贝时深拷贝不能简单的复制
  44. const currentRow = ref({})
  45. const currentColumn = ref({
  46.     col:[],
  47.     data:[]
  48. })
  49. function copyCurrentRow(){
  50.     console.log(curTarget.value.rowIdx,"curTarget.value.rowIdx");
  51.     if(curTarget.value.rowIdx === null) return
  52.     currentRow.value = deepcopy(tableData.value[curTarget.value.rowIdx])
  53.     hideMenu()
  54. }
  55. function copyCurrentColumn(){
  56.     console.log(curTarget.value.colIdx,"curTarget.value.colIdx");
  57.     const colData = []
  58.     currentColumn.value.col = deepcopy(columnList.value[curTarget.value.colIdx])
  59.     //复制列名,复制列的数据
  60.     for(let i = 0; i < tableData.value.length;i++){
  61.         colData.push(deepcopy(tableData.value[i][currentColumn.value.col.prop]))
  62.     }
  63.     currentColumn.value.data = colData
  64.     hideMenu()
  65. }
  66. function pasteCurrentRow(){
  67.     if(curTarget.value.rowIdx === null) return
  68.     const idx = curTarget.value.rowIdx + 1
  69.     currentRow.value.row_index = currentRow.value.row_index + 1
  70.     tableData.value.splice(idx, 0, deepcopy(currentRow.value))
  71.     console.log(tableData.value);
  72. }
  73. function pasteCurrentColumn(){
  74.     const idx = curTarget.value.colIdx + 1
  75.     const obj =  deepcopy(currentColumn.value.col)
  76.     let index = 0
  77.     let key = obj.prop + '('+ ++index +')'
  78.     for(let i =0;i<columnList.value.length;i++){
  79.       if(key == columnList.value[i].prop){
  80.           key = obj.prop + '('+ ++index +')'
  81.       }
  82.     }
  83.     obj.prop = key
  84.     obj.label = key
  85.   columnList.value.splice(idx, 0, obj)
  86.   for(let i =0; i <tableData.value.length;i++){
  87.     console.log(tableData.value[i][obj.prop],deepcopy(currentColumn.value));
  88.     tableData.value[i][obj.prop] = deepcopy(currentColumn.value.data[i])
  89.   }
  90. }
复制代码
5.插入粘贴时prop属性确保唯一(由于业务需求prop不能随意取值,这里的对prop和label进行了同步,prop=label)。

  1. //编辑列名
  2. function changeColumnLabel(column){
  3.     column.show = true;
  4.     for(let i = 0; i< tableData.value.length;i++){
  5.         tableData.value[i][column.label] = tableData.value[i][column.prop]
  6.         //删除原属性数据
  7.         delete tableData.value[i][column.prop]
  8.     }
  9.     column.prop=column.label;
  10.     console.log(tableData);
  11. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4