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.
370 lines
8.5 KiB
370 lines
8.5 KiB
<script lang="ts" setup>
|
|
import type { Ref } from 'vue'
|
|
import pinyinDict from 'libs/pinyinDict.json'
|
|
import { computed, defineEmits, defineProps, onMounted, ref, watch, watchEffect } from 'vue'
|
|
|
|
const props = defineProps<{
|
|
modelValue: string
|
|
keyboardType: 'text' | 'number'
|
|
isVisible: boolean
|
|
}>()
|
|
const emits = defineEmits<{
|
|
(e: 'update:modelValue', value: string): void
|
|
(e: 'updateKeyboardVisible', value: boolean): void
|
|
(e: 'confirm', value: string): void
|
|
(e: 'close'): void
|
|
}>()
|
|
const languageType = ref('en')
|
|
const inputValue = ref(props.modelValue)
|
|
const cnList = ref<string[]>([])
|
|
const pinyinMap: Record<string, string[]> = pinyinDict
|
|
const pinyinValue = ref('')
|
|
// 拖动相关状态
|
|
const isDragging = ref(false)// 是否正在拖动
|
|
const startX = ref(0)// 触摸起始 X
|
|
const startY = ref(0)// 触摸起始 Y
|
|
const x = ref(0)// 容器偏移 X
|
|
const y = ref(0)// 容器偏移 Y
|
|
const keyboardRef = ref() as Ref<HTMLDivElement> // 软键盘容器 DOM
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('click', (e: any) => {
|
|
if (isOpen.value && !e.target?.name) {
|
|
isOpen.value = false
|
|
emits('updateKeyboardVisible', false)
|
|
}
|
|
})
|
|
})
|
|
|
|
const isOpen = ref(false)
|
|
watchEffect(() => {
|
|
// 在焦点内二次点击时不触发EventListener,做一下延迟处理
|
|
setTimeout(() => {
|
|
isOpen.value = props.isVisible
|
|
}, 100)
|
|
inputValue.value = props.modelValue
|
|
})
|
|
|
|
const activeKey = ref('')
|
|
const keyboardLayout = computed(() => {
|
|
if (props.keyboardType === 'number') {
|
|
return [
|
|
['1', '2', '3'],
|
|
['4', '5', '6'],
|
|
['7', '8', '9'],
|
|
['.', '0', 'del'],
|
|
]
|
|
}
|
|
return [
|
|
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'del'],
|
|
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '/'],
|
|
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', 'enter'],
|
|
['z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '?', '.', ':'],
|
|
['close', ' ', 'en'],
|
|
]
|
|
})
|
|
const specialKeys = ['del', 'enter', ' ']
|
|
const handleKeyCn = (cn: string) => {
|
|
emits('update:modelValue', props.modelValue + cn)
|
|
cnList.value = []
|
|
pinyinValue.value = ''
|
|
}
|
|
const handleKeyPress = (key: string) => {
|
|
activeKey.value = key
|
|
setTimeout(() => {
|
|
activeKey.value = ''
|
|
}, 150)
|
|
|
|
if (key === 'del') {
|
|
if (props.keyboardType === 'text' && languageType.value === 'cn' && pinyinValue.value) {
|
|
pinyinValue.value = pinyinValue.value.slice(0, -1)
|
|
// 中文
|
|
onHandlePinyinToCn(pinyinValue.value)
|
|
}
|
|
else {
|
|
emits('update:modelValue', props.modelValue.slice(0, -1))
|
|
}
|
|
}
|
|
else if (key === 'enter') {
|
|
emits('confirm', props.modelValue)
|
|
closeKeyboard()
|
|
}
|
|
else if (key === 'close') {
|
|
closeKeyboard()
|
|
}
|
|
else if ((key === 'en' || key === 'cn') && props.keyboardType === 'text') {
|
|
languageType.value = key === 'en' ? 'cn' : 'en'
|
|
keyboardLayout.value[4][2] = key === 'en' ? 'cn' : 'en'
|
|
cnList.value = []
|
|
}
|
|
else {
|
|
if (props.keyboardType === 'text' && languageType.value === 'cn') {
|
|
// 中文
|
|
pinyinValue.value = pinyinValue.value + key
|
|
onHandlePinyinToCn(pinyinValue.value)
|
|
}
|
|
else {
|
|
emits('update:modelValue', props.modelValue + key)
|
|
}
|
|
}
|
|
}
|
|
|
|
const onHandlePinyinToCn = (keyValue: string) => {
|
|
const cn: string[] = pinyinMap[keyValue]
|
|
if (cn && cn.length) {
|
|
cnList.value = cn
|
|
}
|
|
else {
|
|
cnList.value = []
|
|
}
|
|
}
|
|
|
|
const closeKeyboard = () => {
|
|
isOpen.value = false
|
|
emits('close')
|
|
}
|
|
|
|
watch(() => props.isVisible, (newVal) => {
|
|
isOpen.value = newVal
|
|
})
|
|
|
|
// 触摸开始:记录初始位置
|
|
const handleTouchStart = (e: TouchEvent) => {
|
|
isDragging.value = true
|
|
const touch = e.touches[0]
|
|
startX.value = touch.clientX - x.value
|
|
startY.value = touch.clientY - y.value
|
|
}
|
|
|
|
// 触摸移动:计算偏移量
|
|
const handleTouchMove = (e: TouchEvent) => {
|
|
if (isDragging.value) {
|
|
const touch = e.touches[0]
|
|
x.value = touch.clientX - startX.value
|
|
y.value = touch.clientY - startY.value
|
|
}
|
|
}
|
|
|
|
// 触摸结束:停止拖动
|
|
const handleTouchEnd = () => {
|
|
isDragging.value = false
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="isOpen" class="soft-keyboard" :class="{ 'keyboard-open': isOpen }">
|
|
<!-- <div class="keyboard-header">
|
|
<button @click="closeKeyboard">
|
|
关闭键盘
|
|
</button>
|
|
</div> -->
|
|
<div
|
|
ref="keyboardRef"
|
|
class="keyboard-container keyboard-body"
|
|
@touchstart="handleTouchStart"
|
|
@touchmove="handleTouchMove"
|
|
@touchend="handleTouchEnd"
|
|
:style="{
|
|
transform: `translate(${x}px, ${y}px)`,
|
|
transition: isDragging ? 'none' : 'transform 0.3s ease',
|
|
width: keyboardType === 'number' ? '50vw' : '',
|
|
height: keyboardType === 'number' ? '45vh' : '50vh',
|
|
}"
|
|
>
|
|
<div>
|
|
<div v-if="keyboardType === 'text'" class="pinyin-container">
|
|
<span v-if="pinyinValue" style="font-size:12px">拼音:{{ pinyinValue }}</span>
|
|
<div v-if="cnList && cnList.length" class="pinyin-cn">
|
|
<div
|
|
v-for="(cnName, cnIndex) in cnList"
|
|
:key="cnIndex"
|
|
class="cn-name"
|
|
@click="(e) => { e.stopPropagation(); handleKeyCn(cnName) }">
|
|
{{ cnName }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-for="(row, index) in keyboardLayout" :key="index" class="keyboard-row">
|
|
<button
|
|
v-for="(key, keyIndex) in row"
|
|
:key="keyIndex"
|
|
:class="{
|
|
'key-space': key === ' ',
|
|
'key-special': specialKeys.includes(key),
|
|
'key-active': activeKey === key,
|
|
'key-number': keyboardType === 'number',
|
|
'key-text': key !== ' ' && keyboardType === 'text',
|
|
}"
|
|
:style="keyboardType === 'number' ? 'height: 10vh' : 'height:4rem'"
|
|
@click="(e) => {
|
|
e.stopPropagation()
|
|
handleKeyPress(key)
|
|
}"
|
|
>
|
|
{{ key === ' ' ? '空格' : key === 'del' ? '删除' : key === 'enter' ? '确认' : key === 'close' ? '关闭' : key === 'cn' ? '英文' : key === 'en' ? '拼音' : key }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.soft-keyboard {
|
|
bottom: -300px;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: #f5f5f5;
|
|
border-top: 1px solid #ddd;
|
|
padding: 10px;
|
|
transition: bottom 0.3s ease;
|
|
user-select: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.keyboard-open {
|
|
bottom: 0;
|
|
}
|
|
|
|
.keyboard-header {
|
|
margin-bottom: 10px;
|
|
position: absolute;
|
|
float:right;
|
|
}
|
|
|
|
.keyboard-header button {
|
|
padding: 8px 15px;
|
|
background-color: #e0e0e0;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.keyboard-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
.pinyin-container{
|
|
display: flex;
|
|
width: 80%;
|
|
height: 4rem;
|
|
.pinyin-cn{
|
|
display: flex;
|
|
width: 1rem;
|
|
position: relative;
|
|
gap:5px;
|
|
.cn-name{
|
|
font-size: 2.5rem;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.keyboard-row {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.keyboard-row button {
|
|
min-width: 50px;
|
|
font-size: 18px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
background-color: #fff;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
height: 10vh;
|
|
}
|
|
|
|
.keyboard-row button:hover {
|
|
/* background-color: #e0e0e0; */
|
|
}
|
|
|
|
.keyboard-row button:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.key-space {
|
|
margin-top: 3.5px;
|
|
width: 75vw;;
|
|
}
|
|
|
|
.key-special {
|
|
background-color: #e0e0e0;
|
|
}
|
|
|
|
.key-active {
|
|
background-color: #a0c4ff;
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.key-number{
|
|
width: 30vw;
|
|
height: 6vh;
|
|
margin: 5px;
|
|
}
|
|
|
|
.key-text {
|
|
width: 8vw;
|
|
height: 5vh;
|
|
margin: 5px;
|
|
}
|
|
.input-w{
|
|
width: 20%;
|
|
height: 4rem;
|
|
font-size: 2rem;
|
|
margin-left: 3rem;
|
|
}
|
|
|
|
.keyboard-container {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 90%;
|
|
border-radius: 16px;
|
|
box-shadow: 0 -4px 12px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
z-index: 9999;
|
|
/* 让拖动更顺滑 */
|
|
will-change: transform;
|
|
touch-action: none; /* 禁止浏览器默认触摸行为(如滚动) */
|
|
background: #c8c8c8;
|
|
}
|
|
|
|
.input-box {
|
|
padding: 16px;
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.input-field {
|
|
width: 100%;
|
|
padding: 12px;
|
|
text-align: right;
|
|
font-size: 18px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
}
|
|
|
|
.keys {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
|
|
.key {
|
|
height: 60px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-size: 20px;
|
|
border: 1px solid #eee;
|
|
background: #fff;
|
|
touch-action: manipulation; /* 加速点击响应 */
|
|
}
|
|
|
|
.key:active {
|
|
background: #f8f8f8;
|
|
}
|
|
</style>
|