|
@@ -0,0 +1,250 @@
|
|
|
+<template>
|
|
|
+ <div class="keyboard-container">
|
|
|
+ <div :class="keyboardClass" ref="keyboardEl"></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+ import { ref, onMounted, watch, nextTick } from 'vue'
|
|
|
+ import Keyboard from 'simple-keyboard'
|
|
|
+ import 'simple-keyboard/build/css/index.css'
|
|
|
+ import chineseLayout from 'simple-keyboard-layouts/build/layouts/chinese'
|
|
|
+
|
|
|
+ const props = defineProps({
|
|
|
+ keyboardClass: {
|
|
|
+ type: String,
|
|
|
+ default: 'simple-keyboard'
|
|
|
+ },
|
|
|
+ input: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const emit = defineEmits(['onChange', 'onKeyPress', 'onClose'])
|
|
|
+
|
|
|
+ const keyboardEl = ref(null)
|
|
|
+ const keyboard = ref(null)
|
|
|
+ const currentLayout = ref('default')
|
|
|
+ const isChineseMode = ref(false)
|
|
|
+ const isShiftOn = ref(false)
|
|
|
+
|
|
|
+ const displayOptions = {
|
|
|
+ '{bksp}': '⌫',
|
|
|
+ '{lock}': '大写',
|
|
|
+ '{enter}': '确定',
|
|
|
+ '{tab}': '⇄',
|
|
|
+ '{shift}': '⇧',
|
|
|
+ '{change}': '中/英',
|
|
|
+ '{space}': '空格',
|
|
|
+ '{clear}': '清空',
|
|
|
+ '{close}': '关闭'
|
|
|
+ }
|
|
|
+
|
|
|
+ const englishLayout = {
|
|
|
+ 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}',
|
|
|
+ '{shift} z x c v b n m , . / {clear}',
|
|
|
+ '{space} {change} {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}',
|
|
|
+ '{shift} Z X C V B N M < > ? {clear}',
|
|
|
+ '{space} {change} {close}'
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ const chineseLayoutConfig = {
|
|
|
+ ...englishLayout,
|
|
|
+ layoutCandidates: chineseLayout.layoutCandidates
|
|
|
+ }
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ initializeKeyboard()
|
|
|
+ })
|
|
|
+
|
|
|
+ function initializeKeyboard() {
|
|
|
+ keyboard.value = new Keyboard(keyboardEl.value, {
|
|
|
+ onChange: input => {
|
|
|
+ emit('onChange', input)
|
|
|
+ },
|
|
|
+ onKeyPress: button => {
|
|
|
+ handleKeyPress(button)
|
|
|
+ },
|
|
|
+ layout: isChineseMode.value ? chineseLayoutConfig : englishLayout,
|
|
|
+ layoutName: currentLayout.value,
|
|
|
+ display: displayOptions,
|
|
|
+ buttonTheme: [
|
|
|
+ {
|
|
|
+ class: 'hg-function-btn',
|
|
|
+ buttons: '{bksp} {lock} {enter} {tab} {shift} {clear} {close}'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ class: 'hg-mode-btn',
|
|
|
+ buttons: '{change}'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ class: 'hg-space-btn',
|
|
|
+ buttons: '{space}'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ mergeDisplay: true,
|
|
|
+ enableLayoutCandidates: true,
|
|
|
+ useMouseEvents: true,
|
|
|
+ physicalKeyboardHighlight: true,
|
|
|
+ syncInstanceInputs: true
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleKeyPress(button) {
|
|
|
+ emit('onKeyPress', button)
|
|
|
+
|
|
|
+ switch(button) {
|
|
|
+ case '{close}':
|
|
|
+ emit('onClose')
|
|
|
+ break
|
|
|
+ case '{change}':
|
|
|
+ toggleChineseMode()
|
|
|
+ break
|
|
|
+ case '{clear}':
|
|
|
+ emit('onChange', '')
|
|
|
+ keyboard.value?.clearInput()
|
|
|
+ break
|
|
|
+ case '{shift}':
|
|
|
+ case '{lock}':
|
|
|
+ toggleShift()
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function toggleChineseMode() {
|
|
|
+ isChineseMode.value = !isChineseMode.value
|
|
|
+
|
|
|
+ // 更新按钮显示文本
|
|
|
+ displayOptions['{change}'] = isChineseMode.value ? '英/中' : '中/英'
|
|
|
+
|
|
|
+ await nextTick()
|
|
|
+
|
|
|
+ keyboard.value.setOptions({
|
|
|
+ layout: isChineseMode.value ? chineseLayoutConfig : englishLayout,
|
|
|
+ layoutCandidates: isChineseMode.value ? chineseLayout.layoutCandidates : null,
|
|
|
+ display: displayOptions
|
|
|
+ })
|
|
|
+
|
|
|
+ // 强制刷新候选词显示
|
|
|
+ if (isChineseMode.value && keyboard.value.input) {
|
|
|
+ const currentInput = keyboard.value.input
|
|
|
+ keyboard.value.setInput(currentInput + ' ')
|
|
|
+ keyboard.value.setInput(currentInput)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function toggleShift() {
|
|
|
+ isShiftOn.value = !isShiftOn.value
|
|
|
+ currentLayout.value = isShiftOn.value ? 'shift' : 'default'
|
|
|
+
|
|
|
+ keyboard.value.setOptions({
|
|
|
+ layoutName: currentLayout.value
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ watch(() => props.input, (newVal) => {
|
|
|
+ if (keyboard.value && newVal !== keyboard.value.input) {
|
|
|
+ keyboard.value.setInput(newVal)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ defineExpose({
|
|
|
+ setInput: (input) => {
|
|
|
+ if (keyboard.value) keyboard.value.setInput(input)
|
|
|
+ }
|
|
|
+ })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+ .keyboard-container {
|
|
|
+ position: fixed;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ z-index: 1000;
|
|
|
+ background: #f0f0f0;
|
|
|
+ padding: 10px 0;
|
|
|
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.simple-keyboard) {
|
|
|
+ background: #f0f0f0;
|
|
|
+ max-width: 1000px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-button) {
|
|
|
+ height: 50px;
|
|
|
+ min-width: 50px;
|
|
|
+ font-size: 16px;
|
|
|
+ border-radius: 5px;
|
|
|
+ box-shadow: none;
|
|
|
+ background: #fff;
|
|
|
+ color: #333;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-function-btn) {
|
|
|
+ background: #e0e0e0 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-mode-btn) {
|
|
|
+ background: #4a8cff !important;
|
|
|
+ color: white !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-space-btn) {
|
|
|
+ flex-grow: 1;
|
|
|
+ max-width: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-candidate-box) {
|
|
|
+ position: absolute;
|
|
|
+ bottom: calc(100% + 5px);
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 5px;
|
|
|
+ padding: 5px;
|
|
|
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-candidate-box-list) {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-candidate-box-list-item) {
|
|
|
+ padding: 8px 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 3px;
|
|
|
+ background: #f5f5f5;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-candidate-box-list-item:hover) {
|
|
|
+ background: #e0e0e0;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.hg-candidate-box-list-item.active) {
|
|
|
+ background: #4a8cff;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+</style>
|