消毒机设备
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
7.0 KiB

  1. <script setup lang="ts">
  2. import 'simple-keyboard/build/css/index.css'
  3. import Keyboard from 'simple-keyboard'
  4. import layout from 'simple-keyboard-layouts/build/layouts/chinese.js'
  5. import { onUnmounted, ref } from 'vue'
  6. defineOptions({
  7. inheritAttrs: false,
  8. })
  9. const props = defineProps({
  10. layoutName: {
  11. type: String,
  12. default: 'default',
  13. },
  14. // 保留几位小数 layoutName为number时生效
  15. precision: {
  16. type: Number,
  17. default: 2,
  18. },
  19. // 获取焦点打开键盘
  20. isOpen: {
  21. type: Boolean,
  22. default: true,
  23. },
  24. })
  25. const emits = defineEmits(['onChange', 'enter', 'close', 'focus'])
  26. const model = defineModel<string>()
  27. const keyboard = ref<any>(null)
  28. const visible = ref(false)
  29. const inputRef = ref()
  30. const popoverRef = ref()
  31. const entering = ref(false)
  32. const width = ref(1000)
  33. if (props.layoutName === 'number')
  34. width.value = 300
  35. const displayDefault = ref({
  36. '{bksp}': 'backspace',
  37. '{lock}': 'caps',
  38. '{enter}': 'enter',
  39. '{tab}': 'tab',
  40. '{shift}': 'shift',
  41. '{change}': 'en',
  42. '{space}': 'space',
  43. '{clear}': '清空',
  44. '{close}': '关闭',
  45. '{arrowleft}': '←',
  46. '{arrowright}': '→',
  47. })
  48. const open = () => {
  49. if (visible.value)
  50. return
  51. inputRef.value.focus()
  52. emits('focus')
  53. visible.value = true
  54. }
  55. const focusInput = () => {
  56. if (visible.value)
  57. return
  58. emits('focus')
  59. if (props.isOpen)
  60. visible.value = true
  61. }
  62. // const blurInput = debounce(() => {
  63. // if (!entering.value) {
  64. // handleClose()
  65. // } else {
  66. // entering.value = false
  67. // }
  68. // }, 100)
  69. const blurInput = () => {
  70. setTimeout(() => {
  71. if (!entering.value) {
  72. handleClose()
  73. }
  74. else {
  75. entering.value = false
  76. }
  77. }, 300)
  78. }
  79. const afterEnter = () => {
  80. // 存在上一个实例时移除元素
  81. const prevKeyboard = document.querySelectorAll('.init-keyboard')
  82. if (prevKeyboard.length > 0)
  83. prevKeyboard[0]?.remove()
  84. keyboard.value = new Keyboard('simple-keyboard', {
  85. onChange,
  86. onKeyPress,
  87. onInit,
  88. layout: {
  89. // 默认布局
  90. default: [
  91. '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
  92. '{tab} q w e r t y u i o p [ ] \\',
  93. '{lock} a s d f g h j k l ; \' {enter}',
  94. '{change} z x c v b n m , . / {clear}',
  95. '{arrowleft} {arrowright} {space} {close}',
  96. ],
  97. // 大小写
  98. shift: [
  99. '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
  100. '{tab} Q W E R T Y U I O P { } |',
  101. '{lock} A S D F G H J K L : " {enter}',
  102. '{change} Z X C V B N M < > ? {clear}',
  103. '{arrowleft} {arrowright} {space} {close}',
  104. ],
  105. // 数字布局
  106. number: ['7 8 9', '4 5 6', '1 2 3', '. 0 {bksp}', '{arrowleft} {arrowright} {clear} {close}'],
  107. },
  108. layoutName: props.layoutName,
  109. display: displayDefault.value,
  110. theme: 'hg-theme-default init-keyboard', // 添加自定义class处理清空逻辑
  111. })
  112. }
  113. const beforeLeave = () => {
  114. visible.value = false
  115. entering.value = false
  116. inputRef.value.blur()
  117. displayDefault.value['{change}'] = 'en'
  118. document.removeEventListener('click', handlePopClose)
  119. }
  120. const onInit = (keyboard: any) => {
  121. keyboard.setInput(model.value)
  122. keyboard.setCaretPosition(inputRef.value?.ref.selectionEnd)
  123. document.addEventListener('click', handlePopClose)
  124. }
  125. const onChange = (input: any) => {
  126. model.value = input
  127. emits('onChange', input)
  128. }
  129. const onKeyPress = (button: any) => {
  130. if (button === '{lock}')
  131. return handleLock()
  132. if (button === '{change}')
  133. return handleChange()
  134. if (button === '{clear}')
  135. return handleClear()
  136. if (button === '{enter}')
  137. return handleEnter()
  138. if (button === '{close}')
  139. return handleClose()
  140. if (button === '{arrowleft}')
  141. return handleArrow(0)
  142. if (button === '{arrowright}')
  143. return handleArrow(1)
  144. }
  145. const handleLock = () => {
  146. entering.value = true
  147. const currentLayout = keyboard.value.options.layoutName
  148. const shiftToggle = currentLayout === 'default' ? 'shift' : 'default'
  149. keyboard.value.setOptions({
  150. layoutName: shiftToggle,
  151. })
  152. }
  153. const handleChange = () => {
  154. entering.value = true
  155. const layoutCandidates = keyboard.value.options.layoutCandidates
  156. // 切换中英文输入法
  157. if (layoutCandidates !== null && layoutCandidates !== undefined) {
  158. displayDefault.value['{change}'] = 'en'
  159. keyboard.value.setOptions({
  160. layoutName: 'default',
  161. layoutCandidates: null,
  162. display: displayDefault.value,
  163. })
  164. }
  165. else {
  166. displayDefault.value['{change}'] = 'cn'
  167. keyboard.value.setOptions({
  168. layoutName: 'default',
  169. layoutCandidates: (layout as any).layoutCandidates,
  170. display: displayDefault.value,
  171. })
  172. }
  173. }
  174. const handleClear = () => {
  175. keyboard.value.clearInput()
  176. model.value = ''
  177. }
  178. const handleEnter = () => {
  179. emits('enter')
  180. }
  181. const handleClose = () => {
  182. if (props.layoutName === 'number') {
  183. // 处理精度
  184. model.value = model.value?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2').replace(/\.$/, '')
  185. }
  186. popoverRef.value.hide()
  187. emits('close')
  188. }
  189. const handleArrow = (num: number) => {
  190. // 处理左右箭头下标位置
  191. const index = keyboard.value.getCaretPositionEnd()
  192. if (num === 0 && index - 1 >= 0) {
  193. keyboard.value.setCaretPosition(index - 1)
  194. }
  195. else if (num === 1 && index + 1 <= (model.value?.length || 0)) {
  196. keyboard.value.setCaretPosition(index + 1)
  197. }
  198. }
  199. const handlePopClose = (e: any) => {
  200. // 虚拟键盘区域 输入框区域 中文选项区域
  201. if (
  202. (e.target as Element).closest('.keyboard-popper')
  203. || e.target === inputRef.value?.ref
  204. || /hg-candidate-box/.test(e.target.className)
  205. ) {
  206. entering.value = true
  207. const index = keyboard.value.getCaretPositionEnd()
  208. inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
  209. inputRef.value.focus()
  210. }
  211. }
  212. const close = () => {
  213. handleClose()
  214. }
  215. onUnmounted(() => {
  216. // 某些情况下未触发动画关闭时销毁事件。此处销毁做后备处理
  217. document.removeEventListener('click', handlePopClose)
  218. })
  219. defineExpose({ inputRef, visible, open, close })
  220. </script>
  221. <template>
  222. <el-input
  223. ref="inputRef"
  224. v-model="model"
  225. v-bind="$attrs"
  226. @focus="focusInput"
  227. @blur="blurInput"
  228. @keyup.enter="handleEnter"
  229. >
  230. <template v-for="(item, index) in $slots" :key="index" #[index]>
  231. <slot :name="index" />
  232. </template>
  233. </el-input>
  234. <el-popover
  235. ref="popoverRef"
  236. :visible="visible"
  237. :virtual-ref="inputRef"
  238. virtual-triggering
  239. placement="bottom"
  240. :width="width"
  241. :show-arrow="false"
  242. :hide-after="0"
  243. popper-style="padding: 0px;color:#000"
  244. :persistent="false"
  245. popper-class="keyboard-popper"
  246. @after-enter="afterEnter"
  247. @before-leave="beforeLeave"
  248. >
  249. <div class="simple-keyboard" />
  250. </el-popover>
  251. </template>
  252. <style>
  253. .hg-theme-default .hg-button.hg-button-arrowleft,
  254. .hg-theme-default .hg-button.hg-button-arrowright {
  255. max-width: 70px;
  256. }
  257. .hg-theme-default .hg-button.hg-button-close {
  258. max-width: 100px;
  259. }
  260. .hg-layout-number .hg-button.hg-button-close {
  261. max-width: none;
  262. }
  263. .hg-layout-number .hg-button.hg-button-bksp {
  264. max-width: 92px;
  265. }
  266. </style>