一、前言

Vue项目中有许多表格需要能快捷键操作,以提高操作速度。如果直接使用DOM方法进行操作的话,性能较差,且Vue并不推荐直接操作DOM对象。了解到vue-direction-key插件解决了这样一个问题,因此想手写一个类似的指令。
那我们先了解一下vue-direction-key的基本使用

二、vue-direction-key的基本使用

1. 下载

  1. npm i vue-direction-key -S

2.在入口文件main.js中引入插件

  1. import Direction from 'vue-direction-key'
  2. Vue.use(Direction)

基本用法

1. 在需要的input框或者el-input框上绑定指定

  • v-direction的值是当前表格所在的坐标位置
  • 不绑定指令的input框,键盘事件会跳过当前单元格

    1. <el-table :data="tableData" border style="width: 100%">
    2. <el-table-column prop="code" label="商品编号" width="180">
    3. <template slot-scope="{ row, $index }">
    4. <el-input v-model="row.code" v-direction="{ x: 0, y: $index }"></el-input>
    5. </template>
    6. </el-table-column>
    7. <el-table-column prop="name" label="商品" width="180">
    8. <template slot-scope="{ row, $index }">
    9. <el-input v-model="row.name" v-direction="{ x: 1, y: $index }"></el-input>
    10. </template>
    11. </el-table-column>
    12. <el-table-column prop="barcodes" label="商品条码">
    13. <template slot-scope="{ row }">
    14. <!-- 可以不绑定指令,不绑定指令的时候就会跳过该单元格 -->
    15. <el-input v-model="row.barcodes"></el-input>
    16. </template>
    17. </el-table-column>
    18. <el-table-column prop="specification" label="规格">
    19. <template slot-scope="{ row, $index }">
    20. <el-input v-model="row.specification" v-direction="{ x: 3, y: $index }"></el-input>
    21. </template>
    22. </el-table-column>
    23. </el-table>
    24. ...
    25. <script>
    26. export default {
    27. data() {
    28. return {
    29. tableData: [
    30. {
    31. code: '20220513001',
    32. name: '连衣裙',
    33. barcodes: '52132124511',
    34. specification: '红色'
    35. },
    36. {
    37. code: '20220513001',
    38. name: '连衣裙',
    39. barcodes: '52132124511',
    40. specification: '红色'
    41. },
    42. {
    43. code: '20220513001',
    44. name: '连衣裙',
    45. barcodes: '52132124511',
    46. specification: '红色'
    47. },
    48. ]
    49. }
    50. }
    51. }
    52. </script>

    2. 在生命周期钩子中监听键盘事件,并做对应操作

  • 可以在ceated或mounted中处理,官网推荐在created

  • direction的移动方法中可以接收两个参数,表示对应点的坐标,默认为当前聚焦元素的坐标

    • 比如:direction.next(1,2),移动到x=1,y=2坐标的右边也就是,x=2,y=2
      1. created() {
      2. // 在源码中,这个代码相当于是在获取操作键盘的实例,它具有上移,下移等方法
      3. let direction = this.$getDirection()
      4. // 监听键盘操作事件,内部处理就是document.addEventListener
      5. direction.on('keyup', function (e, val) {
      6. // e是event对象,val是当前操作的元素的坐标,如:{x: 0, y: 0}
      7. if (e.key === 'ArrowUp') { // 这里的键盘事件可以自己指定
      8. console.log('按下 ↑ 键')
      9. direction.previousLine() // 上移
      10. } else if (e.key === 'ArrowRight' || e.key === 'Enter') {
      11. console.log('按下 → 键,或 Enter 键')
      12. direction.next() // 右移
      13. } else if (e.key === 'ArrowDown') {
      14. console.log('按下 ↓ 键')
      15. direction.nextLine() // 下移
      16. } else if (e.key === 'ArrowLeft') {
      17. console.log('按下 ← 键')
      18. direction.previous() // 左移
      19. }
      20. })
      21. },

      效果图:

      direction.gif

      多组件共存情况的使用

  • 比如有多个表格,每个表格对应一个坐标体系

  • 这种情况,在指令中增加参数来区别,如:v-direction:a=”{x: 0, y: 0}”
  • 然后在this.$getDirection(‘a’)中传入这个参数

    以下案例是官网的案例

  1. <input type="text" v-direction:a="{x: 0, y: 0}">
  2. <input type="text" v-direction:a="{x: 1, y: 0}">
  3. <input type="text" v-direction:b="{x: 0, y: 0}">
  4. <input type="text" v-direction:b="{x: 1, y: 0}">
  1. created: function () {
  2. let a = this.$getDirection('a')
  3. a.on('keyup', function (e, val) {
  4. if (e.keyCode === 39) {
  5. a.next()
  6. }
  7. if (e.keyCode === 37) {
  8. a.previous()
  9. }
  10. if (e.keyCode === 38) {
  11. a.previousLine()
  12. }
  13. if (e.keyCode === 40) {
  14. a.nextLine()
  15. }
  16. })
  17. let b = this.$getDirection('b')
  18. b.on('keyup', function (e, val) {
  19. if (e.keyCode === 39) {
  20. b.next()
  21. }
  22. if (e.keyCode === 37) {
  23. b.previous()
  24. }
  25. if (e.keyCode === 38) {
  26. b.previousLine()
  27. }
  28. if (e.keyCode === 40) {
  29. b.nextLine()
  30. }
  31. })
  32. }

三、手写实现

本次实现不考虑以下问题:

  1. 多组件共存的情况
  2. 未使用插件的形式,主要是实现指令和操作类
  3. 实例操作对象的方法参数,是当前操作元素
  4. 不对方法监听进行封装,直接在created中使用document.addEventListener

思路分析

  1. 通过指令收集表格的坐标体系,采用二维数组的方式收集,比如以下二维数组:

    1. let arr = [
    2. [A, B, C], // 第一行,如:A的坐标就是{x:0,y:0}
    3. [D, E, F] // 第二行
    4. ]

    在表格坐标体系中的位置如下:
    自定义指令实现全键盘操作,参考vue-direction-key源码 - 图2

  2. 定义一个操作类,并将其实例挂载到Vue原型上

    1. 这个操作类中具有next,previous等方法,让对应坐标的元素获取焦点

      实现代码:

      direction-key.js

  • 自定义指令,定义操作类 ```javascript import Vue from ‘vue’ // 声明一个数组,用于存储需要键盘操作的input元素,这是一个二维数组 const nodeArr = [] // 自定义指令 Vue.directive(‘direction’, { inserted: function (el, binding) { // 获取绑定自定义指令的元素的坐标 if (!nodeArr[binding.value.y]) {
    1. nodeArr[binding.value.y] = []
    } if (el.tagName !== ‘input’) {
    1. // 如果元素不是input,则找其子元素中的input
    2. el = el.querySelector('input')
    } // 将元素,和其坐标,保存在二维数组的对应位置 nodeArr[binding.value.y][binding.value.x] = {
    1. el,
    2. 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)

  1. <a name="tuvix"></a>
  2. #### main.js中引入,并挂载到VUE原型上
  3. ```javascript
  4. import Direction from './directives/direction-key'
  5. Vue.prototype.$direction = Direction

使用:

  1. <template>
  2. <div id="app">
  3. <el-table :data="tableData" border style="width: 100%">
  4. <el-table-column prop="code" label="商品编号" width="180">
  5. <template slot-scope="{ row, $index }">
  6. <!-- 类readonlyClass,用于处理input框失去焦点时,不显示外框 -->
  7. <!-- @focus="row.active = 0" @blur="row.active = 11",都是用于处理样式的,与主代码无关 -->
  8. <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>
  9. </template>
  10. </el-table-column>
  11. <el-table-column prop="name" label="商品" width="180">
  12. <template slot-scope="{ row, $index }">
  13. <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>
  14. </template>
  15. </el-table-column>
  16. <el-table-column prop="barcodes" label="商品条码">
  17. <template slot-scope="{ row }">
  18. <!-- 这里没有绑定v-direction ,键盘操作事件时会跳过该单元格-->
  19. <el-input v-model="row.barcodes" :class="{ readonlyClass: row.active !== 2 }" @focus="row.active = 2" @blur="row.active = 11"></el-input>
  20. </template>
  21. </el-table-column>
  22. <el-table-column prop="specification" label="规格">
  23. <template slot-scope="{ row, $index }">
  24. <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>
  25. </template>
  26. </el-table-column>
  27. <el-table-column prop="warehouse" label="仓库">
  28. <template slot-scope="{ row, $index }">
  29. <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>
  30. </template>
  31. </el-table-column>
  32. <el-table-column prop="count" label="数量">
  33. <template slot-scope="{ row, $index }">
  34. <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>
  35. </template>
  36. </el-table-column>
  37. <el-table-column prop="stock" label="库存">
  38. <template slot-scope="{ row, $index }">
  39. <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>
  40. </template>
  41. </el-table-column>
  42. <el-table-column prop="unit" label="单位">
  43. <template slot-scope="{ row, $index }">
  44. <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>
  45. </template>
  46. </el-table-column>
  47. <el-table-column prop="weight" label="重量">
  48. <template slot-scope="{ row, $index }">
  49. <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>
  50. </template>
  51. </el-table-column>
  52. <el-table-column prop="price" label="单价">
  53. <template slot-scope="{ row, $index }">
  54. <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>
  55. </template>
  56. </el-table-column>
  57. </el-table>
  58. </div>
  59. </template>
  60. <script>
  61. export default {
  62. // 在created中监听键盘事件
  63. created() {
  64. document.addEventListener('keyup', e => {
  65. if (e.key === 'ArrowUp') {
  66. console.log('按下 ↑ 键')
  67. // 传入当前元素,作为参照点
  68. this.$direction.previousLine(e.target)
  69. } else if (e.key === 'ArrowRight' || e.key === 'Enter') {
  70. console.log('按下 → 键,或 Enter 键')
  71. this.$direction.next(e.target)
  72. } else if (e.key === 'ArrowDown') {
  73. console.log('按下 ↓ 键')
  74. this.$direction.nextLine(e.target)
  75. } else if (e.key === 'ArrowLeft') {
  76. console.log('按下 ← 键')
  77. this.$direction.previous(e.target)
  78. }
  79. })
  80. },
  81. data() {
  82. return {
  83. tableData: [
  84. {
  85. code: '20220513001',
  86. name: '连衣裙',
  87. barcodes: '52132124511',
  88. specification: '红色',
  89. warehouse: '主仓库',
  90. count: 5,
  91. stock: '53件',
  92. unit: '件',
  93. weight: '0.3kg',
  94. price: 120,
  95. active: 1 // 1商品编号获取焦点,11是没有单元格被激活。用于处理获取焦点或失去焦点时候的样式。
  96. },
  97. {
  98. code: '20220513001',
  99. name: '连衣裙',
  100. barcodes: '52132124518',
  101. specification: '红色',
  102. warehouse: '主仓库',
  103. count: 5,
  104. stock: '53件',
  105. unit: '件',
  106. weight: '0.3kg',
  107. price: 120,
  108. active: 11
  109. },
  110. {
  111. code: '20220513001',
  112. name: '连衣裙',
  113. barcodes: '52132124511',
  114. specification: '红色',
  115. warehouse: '主仓库',
  116. count: 5,
  117. stock: '53件',
  118. unit: '件',
  119. weight: '0.3kg',
  120. price: 120,
  121. active: 11
  122. },
  123. {
  124. code: '20220513001',
  125. name: '连衣裙',
  126. barcodes: '52132124511',
  127. specification: '红色',
  128. warehouse: '主仓库',
  129. count: 5,
  130. stock: '53件',
  131. unit: '件',
  132. weight: '0.3kg',
  133. price: 120,
  134. active: 11
  135. },
  136. {
  137. code: '20220513001',
  138. name: '连衣裙',
  139. barcodes: '52132124511',
  140. specification: '红色',
  141. warehouse: '主仓库',
  142. count: 5,
  143. stock: '53件',
  144. unit: '件',
  145. weight: '0.3kg',
  146. price: 120,
  147. active: 11
  148. },
  149. {
  150. code: '20220513001',
  151. name: '连衣裙',
  152. barcodes: '52132124511',
  153. specification: '红色',
  154. warehouse: '主仓库',
  155. count: 5,
  156. stock: '53件',
  157. unit: '件',
  158. weight: '0.3kg',
  159. price: 120,
  160. active: 11
  161. }
  162. ]
  163. }
  164. }
  165. }
  166. </script>
  167. <style>
  168. .el-table__body-wrapper .cell {
  169. padding: 0 0 !important;
  170. line-height: normal !important;
  171. }
  172. .el-table__body-wrapper .el-table__cell {
  173. padding: 0 0 !important;
  174. }
  175. .el-table__body-wrapper .el-input input {
  176. border-radius: 0;
  177. }
  178. .el-table__body-wrapper .el-input.readonlyClass input {
  179. background: transparent;
  180. border-color: transparent;
  181. }
  182. </style>

效果:

yydirection.gif