当前位置: 首页 > article >正文

Vue3+element-ui 实现可编辑表格,鼠标右键自定义菜单(复制行列,粘贴行列,插入删除等)

一、功能

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

二、实现

1.表格与数据结构

表格我直接采用了element-ui -> el-table 的结构。为了实现表项的自定义编辑,还需要借助el-table的内置插槽进行处理。

<el-table
    :data="tableData"
    height="300"
    border
    style="width: 100%;z-index: 2;"
    :header-cell-style="{
    'font-size': '1.2rem',
    'line-height': '2rem',
    'font-weight': '600',
    }">
        <el-table-column label="" type="index" width="40" fixed></el-table-column>
        <el-table-column 
            v-for="(column,index) in columnList" 
            :prop="column.prop" 
            :label="column.label"
            >
            <!-- 自定义表头 -->
            <template #header>
                <p> {{column.label}} </p>
            </template>
            <template #default="{ row }">
                <p> {{column.label}} </p>
            </template>
        </el-table-column>
</el-table>

接下来构建表内数据tableData和columnList的结构。因为想要实现点击单元格可以输入数据,那么就需要添加一个“show”属性控制当前表格是否为编辑模式,也就是说控制当前单元格是`<p> {{column.label}} </p>`展示形式还是`<el-input />`输入形式。数据结构示例如下:

const tableData = ref([
    {
        '日期':{ content: '2024-08-08', show: true },
        '姓名':{ content: '张三', show: true },
        '年龄':{ content: 12, show: true },
    },
    {
        '日期':{ content: '2024-08-08', show: true },
        '姓名':{ content: '张三', show: true },
        '年龄':{ content: 12, show: true },
    }
])
const columnList = ref([
    { prop:"日期", label:"日期", show:true, },
    { prop:"姓名", label:"姓名", show:true, },
    { prop:"年龄", label:"年龄", show:true, }
])

借助“show”属性我们就可以完善一下表格结构:

<el-table
    :data="tableData"
    height="300"
    border
    style="width: 100%;z-index: 2;"
    :header-cell-style="{
    'font-size': '1.2rem',
    'line-height': '2rem',
    'font-weight': '600',
    }">
        <el-table-column label="" type="index" width="40" fixed></el-table-column>
        <el-table-column 
            v-for="(column,index) in columnList" 
            :prop="column.prop" 
            :label="column.label"
            >
            <!-- 自定义表头 -->
            <template #header>
                <p  
                    v-show="column.show"
                    > 
                    {{column.label}} 
                </p>
                <el-input
                    v-show="!column.show"
                    v-model="column.label"
                   >
                </el-input>
            </template>
            <template #default="{ row }">
                <p  
                    v-show="row[column.prop].show"
                    >
                    {{row[column.prop].content}} 
                </p>
                <el-input
                    type="textarea"
                    :autosize="{minRows:1,maxRows:4}"
                    v-show="!row[column.prop].show"
                    v-model="row[column.prop].content"
                   >
                </el-input>
            </template>
        </el-table-column>
</el-table>

2.单元格内编辑输入标签与展示标签切换的函数事件

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

<el-table
    :data="tableData"
    height="300"
    border
    style="width: 100%;z-index: 2;"
    :header-cell-style="{
    'font-size': '1.2rem',
    'line-height': '2rem',
    'font-weight': '600',
    }">
        <el-table-column label="" type="index" width="40" fixed></el-table-column>
        <el-table-column 
            v-for="(column,index) in columnList" 
            :prop="column.prop" 
            :label="column.label"
            >
            <!-- 自定义表头 -->
            <template #header>
                <p  
                    v-show="column.show"
                    @dblclick="$event => handleEdit(column, $event.target)"
                    > 
                    {{column.label}} 
                </p>
                <el-input
                    v-show="!column.show"
                    v-model="column.label"
                   @dblclick="column.show = !column.show"
                   @blur="column.show = true">
                </el-input>
            </template>
            <template #default="{ row }">
                <p  
                    v-show="row[column.prop].show"
                    @dblclick="$event => handleEdit(row[column.prop], $event.target)"
                    >
                    {{row[column.prop].content}} 
                </p>
                <el-input
                    type="textarea"
                    :autosize="{minRows:1,maxRows:4}"
                    v-show="!row[column.prop].show"
                    v-model="row[column.prop].content"
                   @dblclick="row[column.prop].show = !row[column.prop].show"
                   @blur="row[column.prop].show = true">
                </el-input>
            </template>
        </el-table-column>
</el-table>
function handleEdit(cell, pEl) {
    console.log("双击被调用");
    const editIputEl = Array.from(pEl.nextSibling.childNodes).find(n => ['INPUT','TEXTAREA'].includes(n.tagName))
    cell.show = false
    editIputEl && nextTick(() => {
      editIputEl.focus()
    })
}

3.清除浏览器默认鼠标右键事件,修改为自定义事件。

<el-table
    :data="tableData"
    height="700"
    border
    @header-contextmenu="(column, $event) => rightClick(null, column, $event)"
    @row-contextmenu="rightClick"
    :row-class-name="tableRowClassName"
    style="width: 100%;z-index: 4;"
    :header-cell-style="{
    'font-size': '1.2rem',
    'line-height': '3.2rem',
    'font-weight': '600',
    }">
</el-table>

对表行和表头添加鼠标右键点击事件。

const curTarget =  ref(
    {                 // 当前目标信息
        rowIdx: null,              // 行下标
        colIdx: null,              // 列下标
        isHead: undefined          // 当前目标是表头?
    }
)
function rightClick(row, column, $event) {
  // 阻止浏览器自带的右键菜单弹出
  $event.preventDefault()
  if(column.index === null) return
  // 表格容器的位置
  const { x: tbX, y: tbY } = tbContainerRef.value.getBoundingClientRect()
  
  // 当前鼠标位置
  const { x: pX, y: pY } = $event
  // 定位菜单
  const ele = document.getElementById('rightMenu')
  ele.style.top = pY + 'px'
  ele.style.left = pX + 'px'
  // 边界调整
  if(window.innerWidth - 140 < pX - tbX) {
    ele.style.left = 'unset'
    ele.style.right = 0
  }
  ele.style.display="block" // 原生代码,显示
  // 当前目标
  curTarget.value = {
    rowIdx: row ? row.row_index : null,
    colIdx: column.index,
    isHead: !row
  }
}

菜单结构:

<div id="rightMenu" class="rightMenu" >
	<div class="item" @click="copyCurrentRow">
		<span style="margin-left: 4px"> 复制当前行</span>
	</div>
	<div class="item" @click="pasteCurrentRow">
		<span style="margin-left: 4px"> 粘贴到当前行</span>
	</div>
	<div class="item" @click="copyCurrentColumn">
		<span style="margin-left: 4px"> 复制当前列</span>
	</div>
	<div class="item add-column" @click="pasteCurrentColumn">
		<span style="margin-left: 4px"> 粘贴到当前列</span>
		<div class="column-box"></div>
	</div>
        <div class="item delete-table">
                <el-popconfirm title="确定删除该行吗?" @confirm="delRow" placement="top">
	    		<template #reference>
	            		<span style="margin-left: 4px"> 删除当前行</span>
	    	        </template>
                </el-popconfirm>
	</div>
	<div class="item delete-table"  >
        	<el-popconfirm title="确定删除该列吗?" @confirm="delColumn" placement="top">
                	<template #reference>
	            		<span style="margin-left: 4px"> 删除当前列</span>
                         </template>
                 </el-popconfirm>
	</div>
	<div class="item" @click="addRow()">
		<span style="margin-left: 4px"> 上方插入*空白行</span>
	</div>
	<div class="item" @click="addRow(true)">
		<span style="margin-left: 4px"> 下方插入空白行</span>
	</div>
	<div class="item" @click="addColumn()">
		<span style="margin-left: 4px"> 左侧插入空白列</span>
	</div>
	<div class="item" @click="addColumn(true)">
		<span style="margin-left: 4px"> 右侧插入空白列</span>
	</div>
</div>

除了控制菜单显示还要控制菜单隐藏

const hideMenu = (e) => {
    const menu = document.getElementById('rightMenu');
    menu.style.display="none" // 原生代码,显示
};
document.addEventListener("click", hideMenu);

4.复制、粘贴、插入、删除等excel表格工具功能。

复制行列内容时,注意一定要使用深拷贝!

// 新增行
function addRow(later) {
    hideMenu()
    if(curTarget.value.rowIdx === null) return
    const idx = later ? curTarget.value.rowIdx + 1 : curTarget.value.rowIdx
    let obj = {}
    columnList.value.forEach(p => {
      obj[p.prop] = { content: '', show: true }
    })
    tableData.value.splice(idx, 0, obj)
}
// 删除行
const popperVisibleRow = ref(false)
function delRow() {
    hideMenu()
    curTarget.value.rowIdx !== null && tableData.value.splice(curTarget.value.rowIdx, 1)
}
const count_col = ref(0)
// 新增列
function addColumn(later) {
    hideMenu()
  const idx = later ? curTarget.value.colIdx + 1 : curTarget.value.colIdx
  let key = 'col_' + ++count_col.value
  for(let i =0;i<columnList.value.length;i++){
    if(key == columnList.value[i].prop){
        key = 'col_' + ++count_col.value
    }
  }
  let obj = { prop: key, label: key, show: true }
  columnList.value.splice(idx, 0, obj)
  console.log(columnList.value);
  tableData.value.forEach(p => {
    p[obj.prop] = { content: '', show: true }
  })

}
// 删除列
function delColumn() {
    hideMenu()
  let colKey = columnList.value[curTarget.value.colIdx].prop
  columnList.value.splice(curTarget.value.colIdx, 1)
  tableData.value.forEach(p => delete p[colKey] )
}
//复制行列 拷贝时深拷贝不能简单的复制
const currentRow = ref({})
const currentColumn = ref({
    col:[],
    data:[]
})
function copyCurrentRow(){
    console.log(curTarget.value.rowIdx,"curTarget.value.rowIdx");
    if(curTarget.value.rowIdx === null) return
    currentRow.value = deepcopy(tableData.value[curTarget.value.rowIdx])
    hideMenu()
}
function copyCurrentColumn(){
    console.log(curTarget.value.colIdx,"curTarget.value.colIdx");
    const colData = []
    currentColumn.value.col = deepcopy(columnList.value[curTarget.value.colIdx])
    //复制列名,复制列的数据
    for(let i = 0; i < tableData.value.length;i++){
        colData.push(deepcopy(tableData.value[i][currentColumn.value.col.prop]))
    }
    currentColumn.value.data = colData
    hideMenu()
}
function pasteCurrentRow(){
    if(curTarget.value.rowIdx === null) return
    const idx = curTarget.value.rowIdx + 1 
    currentRow.value.row_index = currentRow.value.row_index + 1
    tableData.value.splice(idx, 0, deepcopy(currentRow.value))
    console.log(tableData.value);
}
function pasteCurrentColumn(){
    const idx = curTarget.value.colIdx + 1 
    const obj =  deepcopy(currentColumn.value.col)
    let index = 0
    let key = obj.prop + '('+ ++index +')'
    for(let i =0;i<columnList.value.length;i++){
      if(key == columnList.value[i].prop){
          key = obj.prop + '('+ ++index +')'
      }
    }
    obj.prop = key
    obj.label = key
  columnList.value.splice(idx, 0, obj)
  for(let i =0; i <tableData.value.length;i++){
    console.log(tableData.value[i][obj.prop],deepcopy(currentColumn.value));
    tableData.value[i][obj.prop] = deepcopy(currentColumn.value.data[i])
  }
}

5.插入粘贴时prop属性确保唯一(由于业务需求prop不能随意取值,这里的对prop和label进行了同步,prop=label)。

//编辑列名
function changeColumnLabel(column){
    column.show = true;
    for(let i = 0; i< tableData.value.length;i++){
        tableData.value[i][column.label] = tableData.value[i][column.prop]
        //删除原属性数据
        delete tableData.value[i][column.prop]
    }
    column.prop=column.label;
    console.log(tableData);
}

http://www.kler.cn/a/373418.html

相关文章:

  • SchooWeb2--基于课堂学习到的知识点2
  • 基于Multisim的四位抢答器设计与仿真
  • 正则表达式(Regular Expressions)
  • 几种因素对磁控溅射AlN薄膜择优取向的影响
  • Linux文件系统学习(未完)
  • 爬虫+数据保存2
  • 我自己的资料整理导引(一):概论
  • webpack+react中问题解决
  • 大模型,多模态大模型面试问题记录【时序,Qformer,卷积,感受野,ControlNet,IP-adapter】
  • Redis-事务、锁
  • Upload-labs靶场Pass-20
  • mfc | mfc集成opencv,实现摄像头监控、拍照、视频图像处理(亮度、对比度、色调、饱和度)功能
  • android OpenGL ES详解——双缓冲区、默认缓冲区和帧缓冲区
  • BeaverTail恶意软件在针对开发人员的恶意npm包中重新出现!研究人员发现开源人工智能和人工智能模型的漏洞 | 安全周报1031
  • Python学习的自我理解和想法(22)
  • 使用ubuntu On windows安装docker
  • C语言基本概念----字节与对齐
  • 计数问题[NOIP2013]
  • traceroute或tracepath区别
  • SpringCloud笔记
  • 网络自动化02:基于xlsx传入设备信息与所需执行备份配置命令,使用netmiko自动化登录分发
  • Oracle SQL 使用 ROWNUM 分页查询速度太慢的问题及解决方案!
  • apisix高性能网关实现一机一密
  • Java 文件操作详解
  • 双向链表(数据结构与算法)
  • 用for循环实现计算1+1/2!+1/3!+...的前20项之和