vue3中使用simple-keyboard实现虚拟键盘(带中文切换数字键盘)

效果图

官网

simple-keyboard官网:https://hodgef.com/simple-keyboard/ 打不开的话请用魔法

不足

中文语言包支持度不够。不过自己可以找语言包替换

依赖安装

npm install simple-keyboard --save
npm install simple-keyboard-layouts --save // 中文语言包

组件代码

vue 复制代码
<template>
  <el-input ref="inputRef" v-model="model" @focus="focusInput" @keyup.enter="handleEnter" v-bind="$attrs">
    <template v-for="(item, index) in $slots" :key="index" #[index]>
      <slot :name="index"></slot>
    </template>
  </el-input>

  <el-popover
    :visible="visible"
    :virtual-ref="inputRef"
    virtual-triggering
    placement="bottom"
    :width="width"
    :show-arrow="false"
    :hide-after="0"
    popper-style="padding: 0px;color:#000"
    :persistent="false"
    popper-class="keyboard-popper"
    @after-leave="afterLeave"
    @after-enter="afterEnter"
  >
    <div v-if="visible" class="simple-keyboard"></div>
  </el-popover>
</template>

<script setup lang="ts">
import Keyboard from 'simple-keyboard'
import 'simple-keyboard/build/css/index.css'
import layout from 'simple-keyboard-layouts/build/layouts/chinese.js'
defineOptions({
  inheritAttrs: false
})
const model = defineModel<string>()
const emits = defineEmits(['onChange', 'enter', 'close', 'focus'])
const props = defineProps({
  layoutName: {
    type: String,
    default: 'default'
  },
  // 保留几位小数 layoutName为number时生效
  precision: {
    type: Number,
    default: 2
  },
  // 获取焦点打开键盘
  isOpen: {
    type: Boolean,
    default: true
  }
})
const keyboard = ref<any>(null)
const visible = ref(false)
const inputRef = ref()
const width = ref(1000)
if (props.layoutName == 'number') width.value = 300
const displayDefault = ref({
  '{bksp}': 'backspace',
  '{lock}': 'caps',
  '{enter}': 'enter',
  '{tab}': 'tab',
  '{shift}': 'shift',
  '{change}': 'en',
  '{space}': 'space',
  '{clear}': '清空',
  '{close}': '关闭',
  '{arrowleft}': '←',
  '{arrowright}': '→'
})

const open = () => {
  if (visible.value) return
  inputRef.value.focus()
  emits('focus')
  visible.value = true
}
const focusInput = () => {
  if (visible.value) return
  emits('focus')
  if (props.isOpen) visible.value = true
}
const afterEnter = () => {
  // 存在上一个实例时移除元素
  const prevKeyboard = document.querySelectorAll('.init-keyboard')
  if (prevKeyboard.length > 0) prevKeyboard[0]?.remove()
  keyboard.value = new Keyboard('simple-keyboard', {
    onChange: onChange,
    onKeyPress: onKeyPress,
    onInit: onInit,
    layout: {
      // 默认布局
      default: [
        '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
        '{tab} q w e r t y u i o p [ ] \\',
        "{lock} a s d f g h j k l ; ' {enter}",
        '{change} z x c v b n m , . / {clear}',
        '{arrowleft} {arrowright} {space} {close}'
      ],
      // 大小写
      shift: [
        '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
        '{tab} Q W E R T Y U I O P { } |',
        '{lock} A S D F G H J K L : " {enter}',
        '{change} Z X C V B N M < > ? {clear}',
        '{arrowleft} {arrowright} {space} {close}'
      ],
      // 数字布局
      number: ['7 8 9', '4 5 6', '1 2 3', '. 0 {bksp}', '{arrowleft} {arrowright} {clear} {close}']
    },
    layoutName: props.layoutName,
    display: displayDefault.value,
    theme: 'hg-theme-default init-keyboard' // 添加自定义class处理清空逻辑
  })
}
const afterLeave = () => {
  displayDefault.value['{change}'] = 'en'
  document.removeEventListener('click', handlePopClose)
}
const onInit = (keyboard: any) => {
  keyboard.setInput(model.value)
  keyboard.setCaretPosition(inputRef.value?.ref.selectionEnd)
  document.addEventListener('click', handlePopClose)
}
const onChange = (input: any) => {
  model.value = input
  emits('onChange', input)
}
const onKeyPress = (button: any) => {
  if (button !== '{close}') handleFocus()
  if (button === '{lock}') return handleLock()
  if (button === '{change}') return handleChange()
  if (button === '{clear}') return handleClear()
  if (button === '{enter}') return handleEnter()
  if (button === '{close}') return handleClose()
  if (button === '{arrowleft}') return handleArrow(0)
  if (button === '{arrowright}') return handleArrow(1)
}
const handleLock = () => {
  let currentLayout = keyboard.value.options.layoutName
  let shiftToggle = currentLayout === 'default' ? 'shift' : 'default'

  keyboard.value.setOptions({
    layoutName: shiftToggle
  })
}
const handleChange = () => {
  let layoutCandidates = keyboard.value.options.layoutCandidates
  // 切换中英文输入法
  if (layoutCandidates != null && layoutCandidates != undefined) {
    displayDefault.value['{change}'] = 'en'
    keyboard.value.setOptions({
      layoutName: 'default',
      layoutCandidates: null,
      display: displayDefault.value
    })
  } else {
    displayDefault.value['{change}'] = 'cn'
    keyboard.value.setOptions({
      layoutName: 'default',
      layoutCandidates: (layout as any).layoutCandidates,
      display: displayDefault.value
    })
  }
}
const handleClear = () => {
  keyboard.value.clearInput()
  model.value = ''
}
const handleEnter = () => {
  emits('enter')
}
const handleClose = () => {
  visible.value = false
  if (props.layoutName == 'number') {
    // 处理精度
    model.value = model.value?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2').replace(/\.$/, '')
  }
  emits('close')
}
const handleArrow = (num: number) => {
  // 处理左右箭头下标位置
  const index = keyboard.value.getCaretPositionEnd()
  if (num == 0 && index - 1 >= 0) {
    keyboard.value.setCaretPosition(index - 1)
  } else if (num == 1 && index + 1 <= (model.value?.length || 0)) {
    keyboard.value.setCaretPosition(index + 1)
  }
}
const handleFocus = () => {
  // 处理焦点显示
  setTimeout(() => {
    const index = keyboard.value.getCaretPositionEnd()
    inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
    inputRef.value.focus()
  }, 0)
}
const handlePopClose = (e: any) => {
  // 虚拟键盘区域 输入框区域 中文选项区域
  if (
    (e.target as Element).closest('.keyboard-popper') ||
    e.target == inputRef.value?.ref ||
    /hg-candidate-box/.test(e.target.className)
  ) {
    return
  }
  handleClose()
}
const close = () => {
  handleClose()
}
defineExpose({ inputRef, visible, open, close })
</script>

<style>
.hg-theme-default .hg-button.hg-button-arrowleft,
.hg-theme-default .hg-button.hg-button-arrowright {
  max-width: 70px;
}
.hg-theme-default .hg-button.hg-button-close {
  max-width: 100px;
}
.hg-layout-number .hg-button.hg-button-close {
  max-width: none;
}
.hg-layout-number .hg-button.hg-button-bksp {
  max-width: 92px;
}
</style>