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);
}
相关推荐
べJL几秒前
SVG怎么画渐变甜甜圈(进度环)
前端·css
初遇你时动了情1 分钟前
css3滚动边框特效属性 filter、inset应用
前端·css·css3
残花月伴22 分钟前
axios
javascript
Ares码农人生25 分钟前
React 前端框架简介
前端·react.js·前端框架
小汤猿人类26 分钟前
nacos-gateway动态路由
java·前端·gateway
GISer_Jing27 分钟前
前端经典面试合集(二)——Vue/React/Node/工程化工具/计算机网络
前端·vue.js·react.js·node.js
van叶~1 小时前
仓颉语言实战——2.名字、作用域、变量、修饰符
android·java·javascript·仓颉
GesLuck1 小时前
C#控件开发4—仪表盘
前端·经验分享·c#
小林爱1 小时前
【Compose multiplatform教程14】【组件】LazyColumn组件
android·前端·kotlin·android studio·框架·多平台
泯泷1 小时前
JS代码混淆器:JavaScript obfuscator 让你的代码看起来让人痛苦
开发语言·javascript·ecmascript