forked from gzt/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
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>
|