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);
}
相关推荐
.net开发6 分钟前
WPF使用Prism框架首页界面
前端·c#·.net·wpf
名字越长技术越强26 分钟前
vue--vueCLI
前端·javascript·vue.js
是个热心市民28 分钟前
构建一个导航栏web
前端·javascript·python·django·html
J不A秃V头A40 分钟前
报错:npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。
前端·npm·node.js
GDAL40 分钟前
npm入门教程14:npm依赖管理
前端·npm·node.js
余生H42 分钟前
即时可玩web小游戏(二):打砖块(支持移动端版) - 集成InsCode快来阅读并即时体验吧~
前端·javascript·inscode·canvas·h5游戏
5335ld1 小时前
vue+exceljs前端下载、导出xlsx文件
前端·vue.js
IceyWu1 小时前
LivePhoto(实况图片)渲染
vue.js
摇头的金丝猴1 小时前
uniapp vue3 使用echarts-gl 绘画3d图表
前端·uni-app·echarts
清清ww1 小时前
【TS】九天学会TS语法---计划篇
前端·typescript