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

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