实现 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>
相关推荐
Revolution613 分钟前
一个公共表格组件,是怎么一步步失控的
前端·前端工程化
腻害兔19 分钟前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:CRM 客户关系模块深度解析——从线索到回款,一套完整的 B2B 销售闭环是怎么搭的?
java·前端·javascript·vue.js·产品经理·ai编程
whyfail26 分钟前
前端学 Spring Boot(2):一次点击,如何穿过整个后端?
前端·spring boot·后端
AI多Agent协作实战派1 小时前
AI多Agent协作系统实战(二十三):Agent读HEARTBEAT.md不读AGENTS.md——openclaw的文件加载之谜
前端·数据库·人工智能·uni-app
元Y亨H1 小时前
Pandas 解析 Excel 导致的内存溢出(MemoryError)
python·excel
2601_955760072 小时前
如何用 Claude Opus 5 API 批量扩展长尾关键词和文章选题
前端·python·搜索引擎
KaMeidebaby2 小时前
卡梅德生物技术快报|bli亲和力检测gst:告别批量跑胶:BLI实时酶切监测技术加速GST融合蛋白下游流程优化
前端·网络·数据库·人工智能·算法
触底反弹2 小时前
🚀 删了数据刷新又回来?3 组件 × 4 回调 × 3 坑讲透 React 父子通信
前端·javascript·react.js
耳东小鹿4 小时前
对象常用方法
前端
程序员海军4 小时前
一个 AI 应用开发程序员的一天,都在屏幕前忙些什么?
前端·后端·程序员