乌市泽哥 发表于 2024-11-3 17:49:48

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.show"
                  >
                  {{row.content}}
                </p>
                <el-input
                  type="textarea"
                  :autosize="{minRows:1,maxRows:4}"
                  v-show="!row.show"
                  v-model="row.content"
                   >
                </el-input>
            </template>
      </el-table-column>
</el-table> 2.单位格内编辑输入标签与展示标签切换的函数事件

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

https://img-blog.csdnimg.cn/img_convert/d8a5d082e7195a1da9e994ee26c34f1a.gif
<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.show"
                  @dblclick="$event => handleEdit(row, $event.target)"
                  >
                  {{row.content}}
                </p>
                <el-input
                  type="textarea"
                  :autosize="{minRows:1,maxRows:4}"
                  v-show="!row.show"
                  v-model="row.content"
                   @dblclick="row.show = !row.show"
                   @blur="row.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> 对表行和表头添加鼠标右键点击事件。

https://img-blog.csdnimg.cn/img_convert/66a33276ce296e008f2691359b05dbdb.png
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 = { 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.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 = { content: '', show: true }
})

}
// 删除列
function delColumn() {
    hideMenu()
let colKey = columnList.value.prop
columnList.value.splice(curTarget.value.colIdx, 1)
tableData.value.forEach(p => delete p )
}
//复制行列 拷贝时深拷贝不能简单的复制
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)
    hideMenu()
}
function copyCurrentColumn(){
    console.log(curTarget.value.colIdx,"curTarget.value.colIdx");
    const colData = []
    currentColumn.value.col = deepcopy(columnList.value)
    //复制列名,复制列的数据
    for(let i = 0; i < tableData.value.length;i++){
      colData.push(deepcopy(tableData.value))
    }
    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.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,deepcopy(currentColumn.value));
    tableData.value = deepcopy(currentColumn.value.data)
}
} 5.插入粘贴时prop属性确保唯一(由于业务需求prop不能随意取值,这里的对prop和label进行了同步,prop=label)。

//编辑列名
function changeColumnLabel(column){
    column.show = true;
    for(let i = 0; i< tableData.value.length;i++){
      tableData.value = tableData.value
      //删除原属性数据
      delete tableData.value
    }
    column.prop=column.label;
    console.log(tableData);
}
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Vue3+element-ui 实现可编辑表格,鼠标右键自界说菜单(复制行列,粘贴行列