实现 el-table 中键盘方向键导航功能vue2+vue3(类似 Excel)

实现 el-table 中键盘方向键导航功能vue2+vue3(类似 Excel)

功能需求

在 Element UI 的 el-table 表格中实现以下功能:

  • 使用键盘上下左右键在可编辑的 el-input/el-select 之间移动焦点
  • 焦点移动时自动定位到对应单元格
  • 支持光标位置自动调整,提升编辑体验
完整解决方案(vue2)
1. 表格结构修改

在 el-table 中添加键盘事件监听,并为可编辑元素添加定位标识:

VUE 复制代码
<template>
  <el-table :data="tableData" border style="width: 100%" size="small">
    <el-table-column prop="account_id" label="结算账户" width="180" align="center">
      <template slot-scope="scope">
        <el-select v-model="scope.row.account_id" placeholder="请选择账户" clearable filterable @keydown.native.stop="onKeyDown(scope.$index, 'account_id', $event)" :ref="getInputRef(scope.$index, 'account_id')">
          <el-option label="a" value="1" />
          <el-option label="b" value="2" />
        </el-select>
      </template>
    </el-table-column>
    <el-table-column prop="money" label="结算金额" width="180" align="center">
      <template slot-scope="scope">
        <el-input v-model="scope.row.money" clearable @keydown.native.stop="onKeyDown(scope.$index, 'money', $event)" :ref="getInputRef(scope.$index, 'money')"/>
      </template>
    </el-table-column>
    <el-table-column prop="remark" label="备注" align="center">
      <template slot-scope="scope">
        <el-input v-model="scope.row.remark" @keydown.native.stop="onKeyDown(scope.$index, 'remark', $event)" :ref="getInputRef(scope.$index, 'remark')"></el-input>
      </template>
    </el-table-column>
  </el-table>
</template>
2. 核心 JavaScript 逻辑

在 Vue 组件的 methods 中添加焦点导航控制方法:

js 复制代码
<script>
export default {
  data() {
    return {
      tableData: [
        {},
        {},
        {},
        {}
      ],
      columns: [
        {prop: 'account_id'},
        {prop: 'money'},
        {prop: 'remark'}
      ],
      refList: {}
    }
  },
  methods: {
    getInputRef(rowIndex, columnProp) {
      return `input-${rowIndex}-${columnProp}`
    },
    onKeyDown(rowIndex, columnProp, event) {
      // 当前列在columns数组中的索引
      const columnIndex = this.columns.findIndex((c) => c.prop === columnProp)
      // 计算下一个输入框的位置,如果是当前行的最后一个输入框则移到下一行的第一个输入框
      let nextColumnIndex = (columnIndex + 1) % this.columns.length;
      let nextRowIndex;
      
      switch(event.keyCode) {
        case 38:
          nextRowIndex = rowIndex - 1;
          nextColumnIndex = columnIndex;
          break;
        case 40:
          nextRowIndex = rowIndex + 1;
          nextColumnIndex = columnIndex;
          break;
        case 37:
          nextRowIndex = columnIndex === 0 ? rowIndex - 1 : rowIndex
          nextColumnIndex = columnIndex === 0 ? this.columns.length - 1 : columnIndex - 1
          break;
        case 39:
          nextRowIndex = columnIndex === this.columns.length - 1 ? rowIndex + 1 : rowIndex
          break;
      }

      const nextInputRef = `input-${nextRowIndex}-${this.columns[nextColumnIndex].prop}`
      const currentInputRef = `input-${rowIndex}-${this.columns[columnIndex].prop}`
      
      this.$nextTick(() => {
        if (this.$refs[nextInputRef]) {
          this.$refs[nextInputRef].focus()
          if (this.$refs[currentInputRef]) {
            this.$refs[currentInputRef].blur()
          }
        }
      })
    }
  }
}
</script>
完整解决方案(vue3)
vue 复制代码
 <template>
  <el-table :data="tableData" border style="width: 100%" size="small">
    <el-table-column prop="account_id" label="结算账户" width="180" align="center">
      <template #default="{ row, $index,column }">
        <el-select v-model="row.account_id" placeholder="请选择账户" clearable filterable @keydown="onKeyDown($index,column.property,$event)" :ref="(el)=>setRef(el,`input-${$index}-${column.property}`)">
          <el-option label="a" value="1" />
          <el-option label="b" value="2" />
        </el-select>
      </template>
    </el-table-column>
    <el-table-column prop="money" label="结算金额" width="180" align="center">
      <template #default="{ row, $index,column }">
        <el-input v-model="row.money" clearable @keydown="onKeyDown($index,column.property,$event)" :ref="(el)=>setRef(el,`input-${$index}-${column.property}`)"/>
      </template>
    </el-table-column>
    <el-table-column prop="remark" label="备注" align="center">
      <template #default="{ row,$index,column }">
        <el-input v-model="row.remark" @keydown="onKeyDown($index,column.property,$event)" :ref="(el)=>setRef(el,`input-${$index}-${column.property}`)"></el-input>
      </template>
    </el-table-column>
  </el-table>
</template>
<script setup>
import { nextTick, ref } from 'vue'
 
const tableData = [
  {},
  {},
  {},
  {}
]
 
const columns = [
  {prop:'account_id'},
  {prop:'money'},
  {prop:'remark'}
]
const refList = ref({})
const setRef = (el,key)=>{
  refList.value[key] = el
}
function onKeyDown(rowIndex,columnProp,event){
  // 当前列在columns数组中的索引
  const columnIndex = columns.findIndex((c) => c.prop === columnProp)
  // 计算下一个输入框的位置,如果是当前行的最后一个输入框则移到下一行的第一个输入框
  let nextColumnIndex = (columnIndex + 1) % columns.length;
  let nextRowIndex;
  switch(event.keyCode){
    case 38:
      nextRowIndex = rowIndex - 1 ;
      nextColumnIndex = columnIndex;
      break;
    case 40:
      nextRowIndex = rowIndex + 1 ;
      nextColumnIndex = columnIndex;
      break;
    case 37:
      nextRowIndex = columnIndex === 0 ? rowIndex - 1 : rowIndex
      nextColumnIndex = columnIndex === 0 ? columns.length - 1 : columnIndex - 1
      break;
    case 39:
      nextRowIndex = columnIndex === columns.length - 1 ? rowIndex + 1 : rowIndex
      break;
  }
 
  const nextInputRef = `input-${nextRowIndex}-${columns[nextColumnIndex].prop}`
  const currentInputRef = `input-${rowIndex}-${columns[columnIndex].prop}`
  nextTick(() => {
    if (refList.value[nextInputRef]) {
      refList.value[nextInputRef].focus()
      refList.value[currentInputRef].blur()
    }
  })
 
}
</script>
相关推荐
得物技术5 分钟前
基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术
前端·aigc·openai
一只大黑洋6 分钟前
Clipboard.js 复制内容
前端·javascript·vue.js
前端灵派派6 分钟前
openlayer绘制图形
前端
moyu847 分钟前
ES6 Set与Map完全指南:从入门到性能优化
前端
然我9 分钟前
从 “只会聊天” 到 “能办实事”:OpenAI Function Call 彻底重构 AI 交互逻辑(附完整接入指南)
前端·javascript·人工智能
艾小码10 分钟前
深入解析CSS伪类:从基础到高级实战指南
前端·css
鹏多多10 分钟前
vue混入mixins详解和生命周期影响
前端·javascript·vue.js
汪子熙11 分钟前
HTML 中的 Bidirectional Isolate (bdi) 元素深入解析
前端·javascript
luckyCover12 分钟前
面试前来了解下TCP/IP网络模型吧~
前端·面试