一、前言
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.addEventListener
direction.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() // 左移
}
})
},
效果:
多组件共存情况的使用
- 比如有多个表格,每个表格对应一个坐标体系
- 这种情况,在指令中增加参数来区别,如:
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原型上
- 这个操作类中具有
next,previous等方法,让对应坐标的元素获取焦点
- 这个操作类中具有
实现代码:
direction-key.js
- 自定义指令,定义操作类
import Vue from 'vue'
// 声明一个数组,用于存储需要键盘操作的input元素,这是一个二维数组
const nodeArr = []
// 自定义指令
Vue.directive('direction', {
inserted: function (el, binding) {
// 获取绑定自定义指令的元素的坐标
if (!nodeArr[binding.value.y]) {
nodeArr[binding.value.y] = []
}
if (el.tagName !== 'input') {
// 如果元素不是input,则找其子元素中的input
el = el.querySelector('input')
}
// 将元素,和其坐标,保存在二维数组的对应位置
nodeArr[binding.value.y][binding.value.x] = {
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)
main.js中引入,并挂载到VUE原型上
import 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>