|
|
<template> <div class="keyboard-wrapper"> <el-input ref="inputRef" v-model="model" @focus="focusInput" @keyup.enter="handleEnter" v-bind="$attrs"> <template v-for="index in slotList" :key="index" #[index]> <slot :name="index"></slot> </template> </el-input>
<el-popover ref="popoverRef" :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-enter="afterEnter" @before-leave="beforeLeave"> <div class="simple-keyboard"></div> </el-popover> </div> </template>
<script setup lang="ts"> import { ref, onUnmounted, useSlots } from 'vue' 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 slots = useSlots(); const slotList = ref<any[]>([]); // 使用泛型指定数组类型
slotList.value = Object.values(slots)
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 popoverRef = ref() const entering = ref(false) const width = ref(1200) 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 && !entering.value) { handleClose() }
emits('focus')
if (props.isOpen && !entering.value) { visible.value = true // 显示键盘
} } // const blurInput = debounce(() => {
// if (!entering.value) {
// handleClose()
// } else {
// entering.value = false
// }
// }, 100)
// const blurInput = () => {
// setTimeout(() => {
// if (!entering.value) {
// handleClose()
// } else {
// entering.value = false
// }
// }, 100)
// }
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 beforeLeave = () => { visible.value = false entering.value = false inputRef.value.blur() 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 === '{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 = () => { entering.value = true let currentLayout = keyboard.value.options.layoutName let shiftToggle = currentLayout === 'default' ? 'shift' : 'default'
keyboard.value.setOptions({ layoutName: shiftToggle, }) } const handleChange = () => { entering.value = true 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 { return // 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 = () => { if (props.layoutName == 'number') { // 处理精度
model.value = model.value ?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2') .replace(/\.$/, '') }
// 在此处触发关闭事件
popoverRef.value.hide() emits('close')
// 键盘关闭时,触发 `onChange` 事件,确保父组件值的同步
emits('onChange', model.value) }
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 handlePopClose = (e: any) => { // 如果用户点击的是键盘区域,输入框区域,或者候选词框区域,我们就不关闭键盘
if ( (e.target as Element).closest('.keyboard-popper') || e.target == inputRef.value?.ref || /hg-candidate-box/.test(e.target.className) ) { entering.value = true const index = keyboard.value.getCaretPositionEnd() inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index inputRef.value.focus() } else { // 只有在点击了输入框以外的地方才关闭键盘
handleClose() } }
const close = () => { handleClose() }
onUnmounted(() => { // 某些情况下未触发动画关闭时销毁事件。此处销毁做后备处理
document.removeEventListener('click', handlePopClose) })
defineExpose({ inputRef, visible, open, close }) </script>
<style scoped lang="less"> // 变量定义
@bg-color: #ffffff; @border-color: #dddddd; @button-hover-bg: #f0f0f0; @button-active-bg: #e6e6e6; @function-key-bg: #ffcc00; @function-key-border: #ff9900; @disabled-bg: #f7f7f7; @disabled-color: #cccccc; @text-color: #333333; @box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); @transition: all 0.3s ease;
// 覆盖 .simple-keyboard 的样式
::v-deep(.simple-keyboard) { width: 100% !important; /* 键盘宽度占满父容器 */ max-width: 100vw !important; /* 限制键盘最大宽度 */ height: auto !important; overflow-x: hidden !important; /* 禁止水平拖动 */ overflow-y: auto !important; /* 如果键盘内容超长,允许垂直滚动 */ box-sizing: border-box !important; }
// 覆盖键盘行的样式
::v-deep(.hg-rows) { flex: 1; // 每一行高度平分
display: flex; // 使用弹性布局
flex-direction: column; // 行方向排列
}
// 覆盖键盘每一行的样式
::v-deep(.hg-row) { display: flex !important; // 每行按键水平排列
gap: 1px; // 每个按键的间距
flex: 1; // 每行高度自动调整
justify-content: space-evenly; // 每行按键均匀分布
}
// 覆盖键盘按键样式
::v-deep(.hg-button) { flex: 1; // 按键宽度均分
height: 80px; // 统一按键高度
font-size: 24px !important; // 增大字体
font-weight: bold !important; color: @text-color !important; // 按键字体颜色
background-color: @bg-color !important; // 按键背景色
border: 1px solid @border-color !important; // 按键边框
border-radius: 5px !important; // 按键圆角
display: flex !important; justify-content: center !important; align-items: center !important; transition: @transition !important;
&:hover { background-color: @button-hover-bg !important; }
&:active { background-color: @button-active-bg !important; }
// 功能键样式
&.hg-function { background-color: @function-key-bg !important; border-color: @function-key-border !important; }
// 禁用键样式
&.hg-disabled { background-color: @disabled-bg !important; color: @disabled-color !important; cursor: not-allowed !important; } }
// 特殊按键(space、清空、关闭等)宽度调整
::v-deep(.hg-button-space) { flex: 3 !important; // 扩展宽度
}
::v-deep(.hg-button-clear), ::v-deep(.hg-button-close) { flex: 2 !important; // 比普通按键略大
font-size: 16px !important; }
// 禁用按键样式
::v-deep(.hg-disabled) { background-color: @disabled-bg !important; color: @disabled-color !important; cursor: not-allowed !important; }
// 行边框调整,让行更清晰
::v-deep(.hg-row:not(:last-child)) { border-bottom: 1px solid #eeeeee !important; // 增加行分隔线
}
// el-popover 样式调整
::v-deep(.el-popover) { position: fixed !important; width: 100vw !important; /* 确保弹出层宽度占满屏幕 */ max-width: 100vw !important; /* 限制最大宽度 */ height: 100vh !important; /* 确保高度占满屏幕 */ left: 0 !important; /* 左对齐屏幕 */ top: 100px !important; /* 定位 */ padding: 0 !important; background-color: transparent !important; transform: none !important; /* 移除默认偏移 */ overflow-x: hidden !important; /* 禁止水平滚动 */ overflow-y: auto !important; /* 垂直滚动,确保键盘内容不超出 */ }
.keyboard-wrapper { position: relative; width: 100vw; /* 确保键盘宽度不超出视口 */ overflow: hidden; /* 防止键盘内容超出屏幕 */ }
// 输入框样式优化,确保只显示一个框
.el-input { font-size: 36px; /* 增大字体 */ font-weight: bold; /* 字体加粗 */ color: #333; /* 字体颜色 */ height: 60px; /* 输入框高度 */ border-radius: 8px; /* 圆角边框 */ background-color: @bg-color; /* 背景色 */ box-shadow: @box-shadow; /* 应用全局阴影变量 */ border: none; /* 去除边框 */ outline: none; /* 去除焦点样式 */
&:focus-within { border: 2px solid #528dfe; /* 聚焦边框颜色 */ box-shadow: 0 0 5px rgba(82, 141, 254, 0.5); /* 聚焦时阴影 */ }
.el-input__inner { font-size: inherit; font-weight: inherit; color: inherit; height: 100%; /* 占满输入框 */ width: 100%; /* 宽度占满父容器 */ border: none; /* 去除内部边框 */ background-color: transparent; /* 背景透明 */ outline: none; /* 去除焦点高亮 */ text-align: left; } } </style>
|