A8000
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.

444 lines
12 KiB

8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
  1. <template>
  2. <div class="keyboard-wrapper">
  3. <el-input ref="inputRef" v-model="model" @focus="focusInput" @keyup.enter="handleEnter" v-bind="$attrs">
  4. <template v-for="index in slotList" :key="index" #[index]>
  5. <slot :name="index"></slot>
  6. </template>
  7. </el-input>
  8. <el-popover ref="popoverRef" :visible="visible" :virtual-ref="inputRef" virtual-triggering placement="bottom"
  9. :width="width" :show-arrow="false" :hide-after="0" popper-style="padding: 0px;color:#000" :persistent="false"
  10. popper-class="keyboard-popper" @after-enter="afterEnter" @before-leave="beforeLeave">
  11. <div class="simple-keyboard"></div>
  12. </el-popover>
  13. </div>
  14. </template>
  15. <script setup lang="ts">
  16. import { ref, onUnmounted, useSlots } from 'vue'
  17. import Keyboard from 'simple-keyboard'
  18. import 'simple-keyboard/build/css/index.css'
  19. // import layout from 'simple-keyboard-layouts/build/layouts/chinese.js'
  20. defineOptions({
  21. inheritAttrs: false,
  22. })
  23. const model = defineModel<string>()
  24. const emits = defineEmits(['onChange', 'enter', 'close', 'focus'])
  25. const slots = useSlots();
  26. const slotList = ref<any[]>([]); // 使用泛型指定数组类型
  27. slotList.value = Object.values(slots)
  28. const props = defineProps({
  29. layoutName: {
  30. type: String,
  31. default: 'default',
  32. },
  33. // 保留几位小数 layoutName为number时生效
  34. precision: {
  35. type: Number,
  36. default: 2,
  37. },
  38. // 获取焦点打开键盘
  39. isOpen: {
  40. type: Boolean,
  41. default: true,
  42. },
  43. })
  44. const keyboard = ref<any>(null)
  45. const visible = ref(false)
  46. const inputRef = ref()
  47. const popoverRef = ref()
  48. const entering = ref(false)
  49. const width = ref(1200)
  50. if (props.layoutName == 'number') width.value = 300
  51. const displayDefault = ref({
  52. '{bksp}': 'backspace',
  53. '{lock}': 'caps',
  54. '{enter}': 'enter',
  55. '{tab}': 'tab',
  56. '{shift}': 'shift',
  57. '{change}': 'en',
  58. '{space}': 'space',
  59. '{clear}': '清空',
  60. '{close}': '关闭',
  61. '{arrowleft}': '←',
  62. '{arrowright}': '→',
  63. })
  64. const open = () => {
  65. if (visible.value) return
  66. inputRef.value.focus()
  67. emits('focus')
  68. visible.value = true
  69. }
  70. const focusInput = () => {
  71. // 只有在当前键盘没有处于输入状态时,才关闭键盘
  72. if (visible.value && !entering.value) {
  73. handleClose()
  74. }
  75. emits('focus')
  76. if (props.isOpen && !entering.value) {
  77. visible.value = true // 显示键盘
  78. }
  79. }
  80. // const blurInput = debounce(() => {
  81. // if (!entering.value) {
  82. // handleClose()
  83. // } else {
  84. // entering.value = false
  85. // }
  86. // }, 100)
  87. // const blurInput = () => {
  88. // setTimeout(() => {
  89. // if (!entering.value) {
  90. // handleClose()
  91. // } else {
  92. // entering.value = false
  93. // }
  94. // }, 100)
  95. // }
  96. const afterEnter = () => {
  97. // 存在上一个实例时移除元素
  98. const prevKeyboard = document.querySelectorAll('.init-keyboard')
  99. if (prevKeyboard.length > 0) prevKeyboard[0]?.remove()
  100. keyboard.value = new Keyboard('simple-keyboard', {
  101. onChange: onChange,
  102. onKeyPress: onKeyPress,
  103. onInit: onInit,
  104. layout: {
  105. // 默认布局
  106. default: [
  107. '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
  108. '{tab} q w e r t y u i o p [ ] \\',
  109. "{lock} a s d f g h j k l ; ' {enter}",
  110. '{change} z x c v b n m , . / {clear}',
  111. '{arrowleft} {arrowright} {space} {close}',
  112. ],
  113. // 大小写
  114. shift: [
  115. '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
  116. '{tab} Q W E R T Y U I O P { } |',
  117. '{lock} A S D F G H J K L : " {enter}',
  118. '{change} Z X C V B N M < > ? {clear}',
  119. '{arrowleft} {arrowright} {space} {close}',
  120. ],
  121. // 数字布局
  122. number: [
  123. '7 8 9',
  124. '4 5 6',
  125. '1 2 3',
  126. '. 0 {bksp}',
  127. '{arrowleft} {arrowright} {clear} {close}',
  128. ],
  129. },
  130. layoutName: props.layoutName,
  131. display: displayDefault.value,
  132. theme: 'hg-theme-default init-keyboard', // 添加自定义class处理清空逻辑
  133. })
  134. }
  135. const beforeLeave = () => {
  136. visible.value = false
  137. entering.value = false
  138. inputRef.value.blur()
  139. displayDefault.value['{change}'] = 'en'
  140. document.removeEventListener('click', handlePopClose)
  141. }
  142. const onInit = (keyboard: any) => {
  143. keyboard.setInput(model.value) // 初始时同步
  144. keyboard.setCaretPosition(inputRef.value?.ref.selectionEnd)
  145. document.addEventListener('click', handlePopClose)
  146. }
  147. const onChange = (input: any) => {
  148. model.value = input
  149. emits('onChange', input)
  150. }
  151. const onKeyPress = (button: any) => {
  152. if (button === '{lock}') return handleLock()
  153. if (button === '{change}') return handleChange()
  154. if (button === '{clear}') return handleClear()
  155. if (button === '{enter}') return handleEnter()
  156. if (button === '{close}') return handleClose()
  157. if (button === '{arrowleft}') return handleArrow(0)
  158. if (button === '{arrowright}') return handleArrow(1)
  159. }
  160. const handleLock = () => {
  161. entering.value = true
  162. let currentLayout = keyboard.value.options.layoutName
  163. let shiftToggle = currentLayout === 'default' ? 'shift' : 'default'
  164. keyboard.value.setOptions({
  165. layoutName: shiftToggle,
  166. })
  167. }
  168. const handleChange = () => {
  169. entering.value = true
  170. let layoutCandidates = keyboard.value.options.layoutCandidates
  171. // 切换中英文输入法
  172. if (layoutCandidates != null && layoutCandidates != undefined) {
  173. displayDefault.value['{change}'] = 'en'
  174. keyboard.value.setOptions({
  175. layoutName: 'default',
  176. layoutCandidates: null,
  177. display: displayDefault.value,
  178. })
  179. } else {
  180. return
  181. // displayDefault.value['{change}'] = 'cn'
  182. // keyboard.value.setOptions({
  183. // layoutName: 'default',
  184. // layoutCandidates: (layout as any).layoutCandidates,
  185. // display: displayDefault.value,
  186. // })
  187. }
  188. }
  189. const handleClear = () => {
  190. keyboard.value.clearInput()
  191. model.value = ''
  192. }
  193. const handleEnter = () => {
  194. emits('enter')
  195. }
  196. const handleClose = () => {
  197. if (props.layoutName == 'number') {
  198. // 处理精度
  199. model.value = model.value
  200. ?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2')
  201. .replace(/\.$/, '')
  202. }
  203. // 在此处触发关闭事件
  204. popoverRef.value.hide()
  205. emits('close')
  206. // 键盘关闭时,触发 `onChange` 事件,确保父组件值的同步
  207. emits('onChange', model.value)
  208. }
  209. const handleArrow = (num: number) => {
  210. // 处理左右箭头下标位置
  211. const index = keyboard.value.getCaretPositionEnd()
  212. if (num == 0 && index - 1 >= 0) {
  213. keyboard.value.setCaretPosition(index - 1)
  214. } else if (num == 1 && index + 1 <= (model.value?.length || 0)) {
  215. keyboard.value.setCaretPosition(index + 1)
  216. }
  217. }
  218. const handlePopClose = (e: any) => {
  219. // 如果用户点击的是键盘区域,输入框区域,或者候选词框区域,我们就不关闭键盘
  220. if (
  221. (e.target as Element).closest('.keyboard-popper') ||
  222. e.target == inputRef.value?.ref ||
  223. /hg-candidate-box/.test(e.target.className)
  224. ) {
  225. entering.value = true
  226. const index = keyboard.value.getCaretPositionEnd()
  227. inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
  228. inputRef.value.focus()
  229. } else {
  230. // 只有在点击了输入框以外的地方才关闭键盘
  231. handleClose()
  232. }
  233. }
  234. const close = () => {
  235. handleClose()
  236. }
  237. onUnmounted(() => {
  238. // 某些情况下未触发动画关闭时销毁事件。此处销毁做后备处理
  239. document.removeEventListener('click', handlePopClose)
  240. })
  241. defineExpose({ inputRef, visible, open, close })
  242. </script>
  243. <style scoped lang="less">
  244. // 变量定义
  245. @bg-color: #ffffff;
  246. @border-color: #dddddd;
  247. @button-hover-bg: #f0f0f0;
  248. @button-active-bg: #e6e6e6;
  249. @function-key-bg: #ffcc00;
  250. @function-key-border: #ff9900;
  251. @disabled-bg: #f7f7f7;
  252. @disabled-color: #cccccc;
  253. @text-color: #333333;
  254. @box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  255. @transition: all 0.3s ease;
  256. // 覆盖 .simple-keyboard 的样式
  257. ::v-deep(.simple-keyboard) {
  258. width: 100% !important;
  259. /* 键盘宽度占满父容器 */
  260. max-width: 100vw !important;
  261. /* 限制键盘最大宽度 */
  262. height: auto !important;
  263. overflow-x: hidden !important;
  264. /* 禁止水平拖动 */
  265. overflow-y: auto !important;
  266. /* 如果键盘内容超长,允许垂直滚动 */
  267. box-sizing: border-box !important;
  268. }
  269. // 覆盖键盘行的样式
  270. ::v-deep(.hg-rows) {
  271. flex: 1; // 每一行高度平分
  272. display: flex; // 使用弹性布局
  273. flex-direction: column; // 行方向排列
  274. }
  275. // 覆盖键盘每一行的样式
  276. ::v-deep(.hg-row) {
  277. display: flex !important; // 每行按键水平排列
  278. gap: 1px; // 每个按键的间距
  279. flex: 1; // 每行高度自动调整
  280. justify-content: space-evenly; // 每行按键均匀分布
  281. }
  282. // 覆盖键盘按键样式
  283. ::v-deep(.hg-button) {
  284. flex: 1; // 按键宽度均分
  285. height: 80px; // 统一按键高度
  286. font-size: 24px !important; // 增大字体
  287. font-weight: bold !important;
  288. color: @text-color !important; // 按键字体颜色
  289. background-color: @bg-color !important; // 按键背景色
  290. border: 1px solid @border-color !important; // 按键边框
  291. border-radius: 5px !important; // 按键圆角
  292. display: flex !important;
  293. justify-content: center !important;
  294. align-items: center !important;
  295. transition: @transition !important;
  296. &:hover {
  297. background-color: @button-hover-bg !important;
  298. }
  299. &:active {
  300. background-color: @button-active-bg !important;
  301. }
  302. // 功能键样式
  303. &.hg-function {
  304. background-color: @function-key-bg !important;
  305. border-color: @function-key-border !important;
  306. }
  307. // 禁用键样式
  308. &.hg-disabled {
  309. background-color: @disabled-bg !important;
  310. color: @disabled-color !important;
  311. cursor: not-allowed !important;
  312. }
  313. }
  314. // 特殊按键(space、清空、关闭等)宽度调整
  315. ::v-deep(.hg-button-space) {
  316. flex: 3 !important; // 扩展宽度
  317. }
  318. ::v-deep(.hg-button-clear),
  319. ::v-deep(.hg-button-close) {
  320. flex: 2 !important; // 比普通按键略大
  321. font-size: 16px !important;
  322. }
  323. // 禁用按键样式
  324. ::v-deep(.hg-disabled) {
  325. background-color: @disabled-bg !important;
  326. color: @disabled-color !important;
  327. cursor: not-allowed !important;
  328. }
  329. // 行边框调整,让行更清晰
  330. ::v-deep(.hg-row:not(:last-child)) {
  331. border-bottom: 1px solid #eeeeee !important; // 增加行分隔线
  332. }
  333. // el-popover 样式调整
  334. ::v-deep(.el-popover) {
  335. position: fixed !important;
  336. width: 100vw !important;
  337. /* 确保弹出层宽度占满屏幕 */
  338. max-width: 100vw !important;
  339. /* 限制最大宽度 */
  340. height: 100vh !important;
  341. /* 确保高度占满屏幕 */
  342. left: 0 !important;
  343. /* 左对齐屏幕 */
  344. top: 100px !important;
  345. /* 定位 */
  346. padding: 0 !important;
  347. background-color: transparent !important;
  348. transform: none !important;
  349. /* 移除默认偏移 */
  350. overflow-x: hidden !important;
  351. /* 禁止水平滚动 */
  352. overflow-y: auto !important;
  353. /* 垂直滚动,确保键盘内容不超出 */
  354. }
  355. .keyboard-wrapper {
  356. position: relative;
  357. width: 100vw;
  358. /* 确保键盘宽度不超出视口 */
  359. overflow: hidden;
  360. /* 防止键盘内容超出屏幕 */
  361. }
  362. // 输入框样式优化,确保只显示一个框
  363. .el-input {
  364. font-size: 36px;
  365. /* 增大字体 */
  366. font-weight: bold;
  367. /* 字体加粗 */
  368. color: #333;
  369. /* 字体颜色 */
  370. height: 60px;
  371. /* 输入框高度 */
  372. border-radius: 8px;
  373. /* 圆角边框 */
  374. background-color: @bg-color;
  375. /* 背景色 */
  376. box-shadow: @box-shadow;
  377. /* 应用全局阴影变量 */
  378. border: none;
  379. /* 去除边框 */
  380. outline: none;
  381. /* 去除焦点样式 */
  382. &:focus-within {
  383. border: 2px solid #528dfe;
  384. /* 聚焦边框颜色 */
  385. box-shadow: 0 0 5px rgba(82, 141, 254, 0.5);
  386. /* 聚焦时阴影 */
  387. }
  388. .el-input__inner {
  389. font-size: inherit;
  390. font-weight: inherit;
  391. color: inherit;
  392. height: 100%;
  393. /* 占满输入框 */
  394. width: 100%;
  395. /* 宽度占满父容器 */
  396. border: none;
  397. /* 去除内部边框 */
  398. background-color: transparent;
  399. /* 背景透明 */
  400. outline: none;
  401. /* 去除焦点高亮 */
  402. text-align: left;
  403. }
  404. }
  405. </style>