消毒机设备
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.

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