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

一、功能

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

二、实现

1.表格与数据结构

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

html 复制代码
<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 />`输入形式。数据结构示例如下:

javascript 复制代码
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"属性我们就可以完善一下表格结构:

html 复制代码
<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上添加失去焦点也可以取消编辑的函数事件。(取消编辑同时添加双击事件和失去焦点事件是因为若只添加双击事件,用户在编辑后必须要双击才能将输入框变为展示标签;若只添加失去焦点事件,用户必须在双击控制输入框显示后,点击输入框聚焦,再点击外部失焦才可以将输入框隐藏)

html 复制代码
<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>
javascript 复制代码
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.清除浏览器默认鼠标右键事件,修改为自定义事件。

html 复制代码
<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>

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

javascript 复制代码
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
  }
}

菜单结构:

javascript 复制代码
<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表格工具功能。

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

javascript 复制代码
// 新增行
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)。

javascript 复制代码
//编辑列名
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);
}
相关推荐
前端拾光者27 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
Json_181790144801 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
风尚云网1 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
木子02041 小时前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing1 小时前
React核心功能详解(一)
前端·react.js·前端框架
捂月1 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
深度混淆1 小时前
实用功能,觊觎(Edge)浏览器的内置截(长)图功能
前端·edge
Smartdaili China1 小时前
如何在 Microsoft Edge 中设置代理: 快速而简单的方法
前端·爬虫·安全·microsoft·edge·社交·动态住宅代理
秦老师Q1 小时前
「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用
前端·chrome·edge
滴水可藏海1 小时前
Chrome离线安装包下载
前端·chrome