一、前言
Vue项目中有许多表格需要能快捷键操作,以提高操作速度。如果直接使用DOM方法进行操作的话,性能较差,且Vue并不推荐直接操作DOM对象。了解到vue-direction-key插件解决了这样一个问题,因此想手写一个类似的指令。
那我们先了解一下vue-direction-key的基本使用
二、vue-direction-key的基本使用
1. 下载
npm i vue-direction-key -S
2.在入口文件main.js中引入插件
import Direction from 'vue-direction-key'Vue.use(Direction)
基本用法
1. 在需要的input框或者el-input框上绑定指定
- v-direction的值是当前表格所在的坐标位置
不绑定指令的input框,键盘事件会跳过当前单元格
<el-table :data="tableData" border style="width: 100%"><el-table-column prop="code" label="商品编号" width="180"><template slot-scope="{ row, $index }"><el-input v-model="row.code" v-direction="{ x: 0, y: $index }"></el-input></template></el-table-column><el-table-column prop="name" label="商品" width="180"><template slot-scope="{ row, $index }"><el-input v-model="row.name" v-direction="{ x: 1, y: $index }"></el-input></template></el-table-column><el-table-column prop="barcodes" label="商品条码"><template slot-scope="{ row }"><!-- 可以不绑定指令,不绑定指令的时候就会跳过该单元格 --><el-input v-model="row.barcodes"></el-input></template></el-table-column><el-table-column prop="specification" label="规格"><template slot-scope="{ row, $index }"><el-input v-model="row.specification" v-direction="{ x: 3, y: $index }"></el-input></template></el-table-column></el-table>...<script>export default {data() {return {tableData: [{code: '20220513001',name: '连衣裙',barcodes: '52132124511',specification: '红色'},{code: '20220513001',name: '连衣裙',barcodes: '52132124511',specification: '红色'},{code: '20220513001',name: '连衣裙',barcodes: '52132124511',specification: '红色'},]}}}</script>
2. 在生命周期钩子中监听键盘事件,并做对应操作
可以在ceated或mounted中处理,官网推荐在created
direction的移动方法中可以接收两个参数,表示对应点的坐标,默认为当前聚焦元素的坐标
- 比如:direction.next(1,2),移动到x=1,y=2坐标的右边也就是,x=2,y=2
created() {// 在源码中,这个代码相当于是在获取操作键盘的实例,它具有上移,下移等方法let direction = this.$getDirection()// 监听键盘操作事件,内部处理就是document.addEventListenerdirection.on('keyup', function (e, val) {// e是event对象,val是当前操作的元素的坐标,如:{x: 0, y: 0}if (e.key === 'ArrowUp') { // 这里的键盘事件可以自己指定console.log('按下 ↑ 键')direction.previousLine() // 上移} else if (e.key === 'ArrowRight' || e.key === 'Enter') {console.log('按下 → 键,或 Enter 键')direction.next() // 右移} else if (e.key === 'ArrowDown') {console.log('按下 ↓ 键')direction.nextLine() // 下移} else if (e.key === 'ArrowLeft') {console.log('按下 ← 键')direction.previous() // 左移}})},
效果图:
多组件共存情况的使用
- 比如:direction.next(1,2),移动到x=1,y=2坐标的右边也就是,x=2,y=2
比如有多个表格,每个表格对应一个坐标体系
- 这种情况,在指令中增加参数来区别,如:v-direction:a=”{x: 0, y: 0}”
- 然后在this.$getDirection(‘a’)中传入这个参数
以下案例是官网的案例
<input type="text" v-direction:a="{x: 0, y: 0}"><input type="text" v-direction:a="{x: 1, y: 0}"><input type="text" v-direction:b="{x: 0, y: 0}"><input type="text" v-direction:b="{x: 1, y: 0}">
created: function () {let a = this.$getDirection('a')a.on('keyup', function (e, val) {if (e.keyCode === 39) {a.next()}if (e.keyCode === 37) {a.previous()}if (e.keyCode === 38) {a.previousLine()}if (e.keyCode === 40) {a.nextLine()}})let b = this.$getDirection('b')b.on('keyup', function (e, val) {if (e.keyCode === 39) {b.next()}if (e.keyCode === 37) {b.previous()}if (e.keyCode === 38) {b.previousLine()}if (e.keyCode === 40) {b.nextLine()}})}
三、手写实现
本次实现不考虑以下问题:
- 多组件共存的情况
- 未使用插件的形式,主要是实现指令和操作类
- 实例操作对象的方法参数,是当前操作元素
- 不对方法监听进行封装,直接在created中使用document.addEventListener
思路分析
通过指令收集表格的坐标体系,采用二维数组的方式收集,比如以下二维数组:
let arr = [[A, B, C], // 第一行,如:A的坐标就是{x:0,y:0}[D, E, F] // 第二行]
在表格坐标体系中的位置如下:

定义一个操作类,并将其实例挂载到Vue原型上
- 自定义指令,定义操作类
```javascript
import Vue from ‘vue’
// 声明一个数组,用于存储需要键盘操作的input元素,这是一个二维数组
const nodeArr = []
// 自定义指令
Vue.directive(‘direction’, {
inserted: function (el, binding) {
// 获取绑定自定义指令的元素的坐标
if (!nodeArr[binding.value.y]) {
} if (el.tagName !== ‘input’) {nodeArr[binding.value.y] = []
} // 将元素,和其坐标,保存在二维数组的对应位置 nodeArr[binding.value.y][binding.value.x] = {// 如果元素不是input,则找其子元素中的inputel = el.querySelector('input')
} } })el,value: binding.value // 指令的值{x:??,y:??}
// 定义一个操作类,该类接收需要操作的二维数组 class Direction { constructor(nodeArr) { this.nodeArr = nodeArr this.x = 0 this.y = 0 } // x轴上前进,右移,接收当前元素作为参数 next(target) { // 获取当前元素的X和Y,以这个目标元素作为参照点 this.getXandY(target) // 处理坐标 // X轴上加1,如果超长换行,最后一个不做处理 if (this.x < this.nodeArr[this.y].length - 1) { this.x += 1 } else { this.y += 1 if (this.y < this.nodeArr.length) { this.x = 0 } else { console.log(‘到底了’) return } } // 获取下一个node const node = this.nodeArr[this.y][this.x] if (node && node.el) { node.el.focus() // 下一个node获取焦点 } else { this.next(null) // 下一个元素为空,直接跳过 } } // x轴上左移 previous(target) { this.getXandY(target) // X轴上减1,到第一个时回退到上一行,到头时不做操作 if (this.x > 0) { this.x -= 1 } else { this.y -= 1 if (this.y >= 0) { this.x = this.nodeArr[this.y].length - 1 } else { console.log(‘到头了’) return } } const node = this.nodeArr[this.y][this.x] if (node && node.el) { node.el.focus() // 下一个node获取焦点 } else { this.previous(null) // 下一个元素为空,直接跳过 } } // 下移 nextLine(target) { this.getXandY(target) // y轴上加1,到底后不做操作 if (this.y < this.nodeArr.length - 1) { this.y += 1 } else { console.log(‘Y轴到底了’) return } const node = this.nodeArr[this.y][this.x] if (node && node.el) { node.el.focus() // 下一个node获取焦点 } else { this.nextLine(null) // 下一个元素为空,直接跳过 } } // 上移 previousLine(target) { this.getXandY(target) // y轴上减1,到头后不做操作 if (this.y > 0) { this.y -= 1 } else { console.log(‘Y轴到头了’) return } const node = this.nodeArr[this.y][this.x] if (node && node.el) { node.el.focus() // 下一个node获取焦点 } else { this.previousLine(null) // 下一个元素为空,直接跳过 } } // 获取目标元素的X和Y getXandY(target) { if (target) { this.nodeArr.forEach(list => { list.forEach(node => { if (node.el === target) { this.x = node.value.x this.y = node.value.y } }) }) } } } // 向外暴露操作类的实例 export default new Direction(nodeArr)
<a name="tuvix"></a>#### main.js中引入,并挂载到VUE原型上```javascriptimport Direction from './directives/direction-key'Vue.prototype.$direction = Direction
使用:
<template><div id="app"><el-table :data="tableData" border style="width: 100%"><el-table-column prop="code" label="商品编号" width="180"><template slot-scope="{ row, $index }"><!-- 类readonlyClass,用于处理input框失去焦点时,不显示外框 --><!-- @focus="row.active = 0" @blur="row.active = 11",都是用于处理样式的,与主代码无关 --><el-input v-model="row.code" :class="{ readonlyClass: row.active !== 0 }" @focus="row.active = 0" @blur="row.active = 11" v-direction="{ x: 0, y: $index }"></el-input></template></el-table-column><el-table-column prop="name" label="商品" width="180"><template slot-scope="{ row, $index }"><el-input v-model="row.name" :class="{ readonlyClass: row.active !== 1 }" @focus="row.active = 1" @blur="row.active = 11" v-direction="{ x: 1, y: $index }"></el-input></template></el-table-column><el-table-column prop="barcodes" label="商品条码"><template slot-scope="{ row }"><!-- 这里没有绑定v-direction ,键盘操作事件时会跳过该单元格--><el-input v-model="row.barcodes" :class="{ readonlyClass: row.active !== 2 }" @focus="row.active = 2" @blur="row.active = 11"></el-input></template></el-table-column><el-table-column prop="specification" label="规格"><template slot-scope="{ row, $index }"><el-input v-model="row.specification" :class="{ readonlyClass: row.active !== 3 }" @focus="row.active = 3" @blur="row.active = 11" v-direction="{ x: 3, y: $index }"></el-input></template></el-table-column><el-table-column prop="warehouse" label="仓库"><template slot-scope="{ row, $index }"><el-input v-model="row.warehouse" :class="{ readonlyClass: row.active !== 4 }" @focus="row.active = 4" @blur="row.active = 11" v-direction="{ x: 4, y: $index }"></el-input></template></el-table-column><el-table-column prop="count" label="数量"><template slot-scope="{ row, $index }"><el-input v-model="row.count" :class="{ readonlyClass: row.active !== 5 }" @focus="row.active = 5" @blur="row.active = 11" v-direction="{ x: 5, y: $index }"></el-input></template></el-table-column><el-table-column prop="stock" label="库存"><template slot-scope="{ row, $index }"><el-input v-model="row.stock" :class="{ readonlyClass: row.active !== 6 }" @focus="row.active = 6" @blur="row.active = 11" v-direction="{ x: 6, y: $index }"></el-input></template></el-table-column><el-table-column prop="unit" label="单位"><template slot-scope="{ row, $index }"><el-input v-model="row.unit" :class="{ readonlyClass: row.active !== 7 }" @focus="row.active = 7" @blur="row.active = 11" v-direction="{ x: 7, y: $index }"></el-input></template></el-table-column><el-table-column prop="weight" label="重量"><template slot-scope="{ row, $index }"><el-input v-model="row.weight" :class="{ readonlyClass: row.active !== 8 }" @focus="row.active = 8" @blur="row.active = 11" v-direction="{ x: 8, y: $index }"></el-input></template></el-table-column><el-table-column prop="price" label="单价"><template slot-scope="{ row, $index }"><el-input v-model="row.price" :class="{ readonlyClass: row.active !== 9 }" @focus="row.active = 9" @blur="row.active = 11" v-direction="{ x: 9, y: $index }"></el-input></template></el-table-column></el-table></div></template><script>export default {// 在created中监听键盘事件created() {document.addEventListener('keyup', e => {if (e.key === 'ArrowUp') {console.log('按下 ↑ 键')// 传入当前元素,作为参照点this.$direction.previousLine(e.target)} else if (e.key === 'ArrowRight' || e.key === 'Enter') {console.log('按下 → 键,或 Enter 键')this.$direction.next(e.target)} else if (e.key === 'ArrowDown') {console.log('按下 ↓ 键')this.$direction.nextLine(e.target)} else if (e.key === 'ArrowLeft') {console.log('按下 ← 键')this.$direction.previous(e.target)}})},data() {return {tableData: [{code: '20220513001',name: '连衣裙',barcodes: '52132124511',specification: '红色',warehouse: '主仓库',count: 5,stock: '53件',unit: '件',weight: '0.3kg',price: 120,active: 1 // 1商品编号获取焦点,11是没有单元格被激活。用于处理获取焦点或失去焦点时候的样式。},{code: '20220513001',name: '连衣裙',barcodes: '52132124518',specification: '红色',warehouse: '主仓库',count: 5,stock: '53件',unit: '件',weight: '0.3kg',price: 120,active: 11},{code: '20220513001',name: '连衣裙',barcodes: '52132124511',specification: '红色',warehouse: '主仓库',count: 5,stock: '53件',unit: '件',weight: '0.3kg',price: 120,active: 11},{code: '20220513001',name: '连衣裙',barcodes: '52132124511',specification: '红色',warehouse: '主仓库',count: 5,stock: '53件',unit: '件',weight: '0.3kg',price: 120,active: 11},{code: '20220513001',name: '连衣裙',barcodes: '52132124511',specification: '红色',warehouse: '主仓库',count: 5,stock: '53件',unit: '件',weight: '0.3kg',price: 120,active: 11},{code: '20220513001',name: '连衣裙',barcodes: '52132124511',specification: '红色',warehouse: '主仓库',count: 5,stock: '53件',unit: '件',weight: '0.3kg',price: 120,active: 11}]}}}</script><style>.el-table__body-wrapper .cell {padding: 0 0 !important;line-height: normal !important;}.el-table__body-wrapper .el-table__cell {padding: 0 0 !important;}.el-table__body-wrapper .el-input input {border-radius: 0;}.el-table__body-wrapper .el-input.readonlyClass input {background: transparent;border-color: transparent;}</style>
效果:

