@composy/keyboard-vue 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/index.css +797 -0
- package/dist/index.js +1250 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.css +1 -0
- package/dist/index.min.js +1 -0
- package/es/components/KeyboardKey.js +17 -0
- package/es/components/KeyboardKey.js.map +1 -0
- package/es/components/KeyboardKey.vue_vue_type_script_setup_true_lang.js +131 -0
- package/es/components/KeyboardKey.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/es/components/KeyboardPopup.js +17 -0
- package/es/components/KeyboardPopup.js.map +1 -0
- package/es/components/KeyboardPopup.vue_vue_type_script_setup_true_lang.js +164 -0
- package/es/components/KeyboardPopup.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/es/components/LicensePlateKeyboard.js +17 -0
- package/es/components/LicensePlateKeyboard.js.map +1 -0
- package/es/components/LicensePlateKeyboard.vue_vue_type_script_setup_true_lang.js +97 -0
- package/es/components/LicensePlateKeyboard.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/es/components/VirtualKeyboard.js +17 -0
- package/es/components/VirtualKeyboard.js.map +1 -0
- package/es/components/VirtualKeyboard.vue_vue_type_script_setup_true_lang.js +127 -0
- package/es/components/VirtualKeyboard.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/es/components/index.d.ts +9 -0
- package/es/components/index.d.ts.map +1 -0
- package/es/components/index.js +15 -0
- package/es/composables/index.d.ts +7 -0
- package/es/composables/index.d.ts.map +1 -0
- package/es/composables/index.js +13 -0
- package/es/composables/useKeyboard.d.ts +113 -0
- package/es/composables/useKeyboard.d.ts.map +1 -0
- package/es/composables/useKeyboard.js +75 -0
- package/es/composables/useKeyboard.js.map +1 -0
- package/es/composables/useKeyboardPopup.d.ts +73 -0
- package/es/composables/useKeyboardPopup.d.ts.map +1 -0
- package/es/composables/useKeyboardPopup.js +119 -0
- package/es/composables/useKeyboardPopup.js.map +1 -0
- package/es/directives/index.d.ts +6 -0
- package/es/directives/index.d.ts.map +1 -0
- package/es/directives/index.js +12 -0
- package/es/directives/v-keyboard.d.ts +36 -0
- package/es/directives/v-keyboard.d.ts.map +1 -0
- package/es/directives/v-keyboard.js +65 -0
- package/es/directives/v-keyboard.js.map +1 -0
- package/es/index.d.ts +42 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +34 -0
- package/es/index.js.map +1 -0
- package/es/plugins/engine-plugin.d.ts +37 -0
- package/es/plugins/engine-plugin.d.ts.map +1 -0
- package/es/plugins/engine-plugin.js +37 -0
- package/es/plugins/engine-plugin.js.map +1 -0
- package/es/plugins/index.d.ts +6 -0
- package/es/plugins/index.d.ts.map +1 -0
- package/es/plugins/index.js +12 -0
- package/es/style/css.js +1 -0
- package/es/style/index.js +1 -0
- package/es/style/index.less +1 -0
- package/es/styles/index.less +267 -0
- package/es/styles/keyboard.less +399 -0
- package/es/styles/themes.less +233 -0
- package/lib/components/KeyboardKey.cjs +21 -0
- package/lib/components/KeyboardKey.cjs.map +1 -0
- package/lib/components/KeyboardKey.vue_vue_type_script_setup_true_lang.cjs +131 -0
- package/lib/components/KeyboardKey.vue_vue_type_script_setup_true_lang.cjs.map +1 -0
- package/lib/components/KeyboardPopup.cjs +21 -0
- package/lib/components/KeyboardPopup.cjs.map +1 -0
- package/lib/components/KeyboardPopup.vue_vue_type_script_setup_true_lang.cjs +164 -0
- package/lib/components/KeyboardPopup.vue_vue_type_script_setup_true_lang.cjs.map +1 -0
- package/lib/components/LicensePlateKeyboard.cjs +21 -0
- package/lib/components/LicensePlateKeyboard.cjs.map +1 -0
- package/lib/components/LicensePlateKeyboard.vue_vue_type_script_setup_true_lang.cjs +97 -0
- package/lib/components/LicensePlateKeyboard.vue_vue_type_script_setup_true_lang.cjs.map +1 -0
- package/lib/components/VirtualKeyboard.cjs +21 -0
- package/lib/components/VirtualKeyboard.cjs.map +1 -0
- package/lib/components/VirtualKeyboard.vue_vue_type_script_setup_true_lang.cjs +127 -0
- package/lib/components/VirtualKeyboard.vue_vue_type_script_setup_true_lang.cjs.map +1 -0
- package/lib/components/index.cjs +19 -0
- package/lib/components/index.d.ts +9 -0
- package/lib/composables/index.cjs +15 -0
- package/lib/composables/index.d.ts +7 -0
- package/lib/composables/useKeyboard.cjs +76 -0
- package/lib/composables/useKeyboard.cjs.map +1 -0
- package/lib/composables/useKeyboard.d.ts +113 -0
- package/lib/composables/useKeyboardPopup.cjs +120 -0
- package/lib/composables/useKeyboardPopup.cjs.map +1 -0
- package/lib/composables/useKeyboardPopup.d.ts +73 -0
- package/lib/directives/index.cjs +13 -0
- package/lib/directives/index.d.ts +6 -0
- package/lib/directives/v-keyboard.cjs +66 -0
- package/lib/directives/v-keyboard.cjs.map +1 -0
- package/lib/directives/v-keyboard.d.ts +36 -0
- package/lib/index.cjs +537 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.ts +42 -0
- package/lib/package.json +3 -0
- package/lib/plugins/engine-plugin.cjs +37 -0
- package/lib/plugins/engine-plugin.cjs.map +1 -0
- package/lib/plugins/engine-plugin.d.ts +37 -0
- package/lib/plugins/index.cjs +13 -0
- package/lib/plugins/index.d.ts +6 -0
- package/lib/style/css.js +1 -0
- package/lib/style/index.css +2 -0
- package/lib/style/index.js +1 -0
- package/lib/styles/index.css +229 -0
- package/lib/styles/keyboard.css +346 -0
- package/lib/styles/themes.css +213 -0
- package/package.json +141 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/components/KeyboardKey.vue","../src/components/KeyboardKey.vue","../src/components/VirtualKeyboard.vue","../src/components/VirtualKeyboard.vue","../src/composables/useKeyboard.ts","../src/composables/useKeyboardPopup.ts","../src/components/KeyboardPopup.vue","../src/components/KeyboardPopup.vue","../src/components/LicensePlateKeyboard.vue","../src/components/LicensePlateKeyboard.vue","../src/directives/v-keyboard.ts","../src/plugins/engine-plugin.ts","../src/index.ts"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { KeyDefinition } from '@composy/keyboard-core'\n\nconst props = defineProps<{\n /**\n * 按键定义\n */\n keyDef: KeyDefinition\n /**\n * 是否大写状态\n */\n isUpperCase?: boolean\n /**\n * 基础宽度(px)\n */\n baseWidth?: number\n /**\n * 是否使用 flex 布局(用于 numpad 等需要均分宽度的键盘)\n */\n useFlex?: boolean\n}>()\n\nconst emit = defineEmits<{\n press: [key: KeyDefinition]\n showOptions: [options: string[]]\n}>()\n\n/**\n * 获取显示文本\n */\nconst label = computed(() => {\n const keyLabel = props.keyDef.label\n if (typeof keyLabel === 'string') {\n return keyLabel\n }\n return props.isUpperCase ? keyLabel.upper : keyLabel.lower\n})\n\n/**\n * 计算按键宽度\n */\nconst keyWidth = computed(() => {\n // 使用 flex 布局时,不设置固定宽度\n if (props.useFlex) {\n return undefined\n }\n const base = props.baseWidth ?? 36\n const width = props.keyDef.width ?? 1\n const gap = 6\n return `${base * width + (width - 1) * gap}px`\n})\n\n/**\n * 计算按键的 flex 值(用于 numpad 布局)\n */\nconst keyFlex = computed(() => {\n if (!props.useFlex) {\n return undefined\n }\n // width 属性决定占几份\n return props.keyDef.width ?? 1\n})\n\n/**\n * 计算 CSS 类\n */\nconst keyClass = computed(() => {\n const classes = ['keyboard-key', `keyboard-key--${props.keyDef.type}`]\n\n if (props.keyDef.className) {\n classes.push(props.keyDef.className)\n }\n\n if (props.keyDef.disabled) {\n classes.push('keyboard-key--disabled')\n }\n\n // Shift 键激活状态\n if (props.keyDef.type === 'shift' && props.isUpperCase) {\n classes.push('keyboard-key--active')\n }\n\n return classes\n})\n\n/**\n * 是否有下拉选项\n */\nconst hasOptions = computed(() => {\n return props.keyDef.options && props.keyDef.options.length > 0\n})\n\n/**\n * 是否是纯选项按钮(有options但无value,点击只显示选项面板)\n */\nconst isOptionsOnly = computed(() => {\n return hasOptions.value && !props.keyDef.value\n})\n\n/**\n * 处理按键点击\n */\nconst handlePress = (event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (props.keyDef.disabled) {\n return\n }\n\n // 纯选项按钮:点击直接显示选项面板\n if (isOptionsOnly.value) {\n emit('showOptions', props.keyDef.options!)\n return\n }\n\n // 正常按键:触发按键事件\n emit('press', props.keyDef)\n}\n\n/**\n * 处理下拉箭头点击(显示选项面板)\n */\nconst handleArrowClick = (event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (props.keyDef.disabled) {\n return\n }\n\n if (hasOptions.value) {\n emit('showOptions', props.keyDef.options!)\n }\n}\n</script>\n\n<template>\n <div class=\"keyboard-key-wrapper\" :style=\"{ width: keyWidth, flex: keyFlex }\">\n <button\n :class=\"[keyClass, { 'has-options': hasOptions && !isOptionsOnly, 'is-options-only': isOptionsOnly }]\"\n :disabled=\"keyDef.disabled\"\n type=\"button\"\n @mousedown.prevent=\"handlePress\"\n @touchstart.prevent=\"handlePress\"\n >\n <!-- SVG 图标支持 -->\n <span v-if=\"keyDef.icon\" class=\"keyboard-key__icon\" v-html=\"keyDef.icon\"></span>\n <span v-else class=\"keyboard-key__label\">{{ label }}</span>\n <!-- 纯选项按钮内嵌下拉箭头 -->\n <span v-if=\"isOptionsOnly\" class=\"keyboard-key__arrow-inline\">▼</span>\n </button>\n <!-- 有value又有options的按钮:单独可点击的下拉箭头 -->\n <span\n v-if=\"hasOptions && !isOptionsOnly\"\n class=\"keyboard-key__arrow\"\n @mousedown.prevent=\"handleArrowClick\"\n @touchstart.prevent=\"handleArrowClick\"\n >▼</span>\n </div>\n</template>\n\n<style>\n.keyboard-key-wrapper {\n position: relative;\n display: inline-flex;\n}\n\n.keyboard-key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: var(--keyboard-key-height, 42px);\n min-width: 32px;\n width: 100%;\n padding: 0 6px;\n border: none;\n border-radius: var(--keyboard-key-radius, 6px);\n background-color: var(--keyboard-key-bg, #fff);\n color: var(--keyboard-key-color, #333);\n font-size: var(--keyboard-font-size, 16px);\n font-weight: 500;\n cursor: pointer;\n user-select: none;\n touch-action: manipulation;\n box-shadow: var(--keyboard-key-shadow, 0 1px 3px rgba(0, 0, 0, 0.1));\n transition: background-color 0.1s, transform 0.1s;\n}\n\n.keyboard-key.has-options {\n padding-right: 16px;\n}\n\n.keyboard-key:active {\n background-color: var(--keyboard-key-active-bg, #e0e0e0);\n transform: scale(0.95);\n}\n\n.keyboard-key--disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.keyboard-key--disabled:active {\n transform: none;\n background-color: var(--keyboard-key-bg, #fff);\n}\n\n/* 功能键样式 */\n.keyboard-key--backspace,\n.keyboard-key--shift,\n.keyboard-key--tab,\n.keyboard-key--clear,\n.keyboard-key--close {\n background-color: var(--keyboard-fn-key-bg, #d4d6dc);\n color: var(--keyboard-fn-key-color, #333);\n font-size: var(--keyboard-fn-font-size, 14px);\n}\n\n.keyboard-key--backspace:active,\n.keyboard-key--shift:active,\n.keyboard-key--tab:active,\n.keyboard-key--clear:active,\n.keyboard-key--close:active {\n background-color: var(--keyboard-fn-key-active-bg, #b8bac0);\n}\n\n/* Shift 激活状态 */\n.keyboard-key--active {\n background-color: var(--keyboard-key-active-bg, #007aff) !important;\n color: #fff !important;\n}\n\n/* 完成/确认键 */\n.keyboard-key--done,\n.keyboard-key--enter {\n background-color: var(--keyboard-primary-bg, #007aff);\n color: var(--keyboard-primary-color, #fff);\n font-size: var(--keyboard-fn-font-size, 14px);\n font-weight: 600;\n}\n\n.keyboard-key--done:active,\n.keyboard-key--enter:active {\n background-color: var(--keyboard-primary-active-bg, #0062cc);\n}\n\n/* 空格键 */\n.keyboard-key--space {\n background-color: var(--keyboard-key-bg, #fff);\n}\n\n.keyboard-key__icon {\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.keyboard-key__icon svg {\n width: 1em;\n height: 1em;\n}\n\n.keyboard-key__label {\n pointer-events: none;\n line-height: 1;\n}\n\n.keyboard-key__arrow {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 8px;\n opacity: 0.5;\n cursor: pointer;\n user-select: none;\n touch-action: manipulation;\n border-radius: 0 6px 6px 0;\n}\n\n.keyboard-key__arrow:active {\n background-color: rgba(0, 0, 0, 0.1);\n opacity: 1;\n}\n\n/* 纯选项按钮内嵌箭头 */\n.keyboard-key__arrow-inline {\n margin-left: 4px;\n font-size: 8px;\n opacity: 0.6;\n pointer-events: none;\n}\n\n/* 纯选项按钮样式 */\n.keyboard-key.is-options-only {\n font-size: 13px;\n background-color: var(--keyboard-fn-key-bg, #d4d6dc);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { KeyDefinition } from '@composy/keyboard-core'\n\nconst props = defineProps<{\n /**\n * 按键定义\n */\n keyDef: KeyDefinition\n /**\n * 是否大写状态\n */\n isUpperCase?: boolean\n /**\n * 基础宽度(px)\n */\n baseWidth?: number\n /**\n * 是否使用 flex 布局(用于 numpad 等需要均分宽度的键盘)\n */\n useFlex?: boolean\n}>()\n\nconst emit = defineEmits<{\n press: [key: KeyDefinition]\n showOptions: [options: string[]]\n}>()\n\n/**\n * 获取显示文本\n */\nconst label = computed(() => {\n const keyLabel = props.keyDef.label\n if (typeof keyLabel === 'string') {\n return keyLabel\n }\n return props.isUpperCase ? keyLabel.upper : keyLabel.lower\n})\n\n/**\n * 计算按键宽度\n */\nconst keyWidth = computed(() => {\n // 使用 flex 布局时,不设置固定宽度\n if (props.useFlex) {\n return undefined\n }\n const base = props.baseWidth ?? 36\n const width = props.keyDef.width ?? 1\n const gap = 6\n return `${base * width + (width - 1) * gap}px`\n})\n\n/**\n * 计算按键的 flex 值(用于 numpad 布局)\n */\nconst keyFlex = computed(() => {\n if (!props.useFlex) {\n return undefined\n }\n // width 属性决定占几份\n return props.keyDef.width ?? 1\n})\n\n/**\n * 计算 CSS 类\n */\nconst keyClass = computed(() => {\n const classes = ['keyboard-key', `keyboard-key--${props.keyDef.type}`]\n\n if (props.keyDef.className) {\n classes.push(props.keyDef.className)\n }\n\n if (props.keyDef.disabled) {\n classes.push('keyboard-key--disabled')\n }\n\n // Shift 键激活状态\n if (props.keyDef.type === 'shift' && props.isUpperCase) {\n classes.push('keyboard-key--active')\n }\n\n return classes\n})\n\n/**\n * 是否有下拉选项\n */\nconst hasOptions = computed(() => {\n return props.keyDef.options && props.keyDef.options.length > 0\n})\n\n/**\n * 是否是纯选项按钮(有options但无value,点击只显示选项面板)\n */\nconst isOptionsOnly = computed(() => {\n return hasOptions.value && !props.keyDef.value\n})\n\n/**\n * 处理按键点击\n */\nconst handlePress = (event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (props.keyDef.disabled) {\n return\n }\n\n // 纯选项按钮:点击直接显示选项面板\n if (isOptionsOnly.value) {\n emit('showOptions', props.keyDef.options!)\n return\n }\n\n // 正常按键:触发按键事件\n emit('press', props.keyDef)\n}\n\n/**\n * 处理下拉箭头点击(显示选项面板)\n */\nconst handleArrowClick = (event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (props.keyDef.disabled) {\n return\n }\n\n if (hasOptions.value) {\n emit('showOptions', props.keyDef.options!)\n }\n}\n</script>\n\n<template>\n <div class=\"keyboard-key-wrapper\" :style=\"{ width: keyWidth, flex: keyFlex }\">\n <button\n :class=\"[keyClass, { 'has-options': hasOptions && !isOptionsOnly, 'is-options-only': isOptionsOnly }]\"\n :disabled=\"keyDef.disabled\"\n type=\"button\"\n @mousedown.prevent=\"handlePress\"\n @touchstart.prevent=\"handlePress\"\n >\n <!-- SVG 图标支持 -->\n <span v-if=\"keyDef.icon\" class=\"keyboard-key__icon\" v-html=\"keyDef.icon\"></span>\n <span v-else class=\"keyboard-key__label\">{{ label }}</span>\n <!-- 纯选项按钮内嵌下拉箭头 -->\n <span v-if=\"isOptionsOnly\" class=\"keyboard-key__arrow-inline\">▼</span>\n </button>\n <!-- 有value又有options的按钮:单独可点击的下拉箭头 -->\n <span\n v-if=\"hasOptions && !isOptionsOnly\"\n class=\"keyboard-key__arrow\"\n @mousedown.prevent=\"handleArrowClick\"\n @touchstart.prevent=\"handleArrowClick\"\n >▼</span>\n </div>\n</template>\n\n<style>\n.keyboard-key-wrapper {\n position: relative;\n display: inline-flex;\n}\n\n.keyboard-key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: var(--keyboard-key-height, 42px);\n min-width: 32px;\n width: 100%;\n padding: 0 6px;\n border: none;\n border-radius: var(--keyboard-key-radius, 6px);\n background-color: var(--keyboard-key-bg, #fff);\n color: var(--keyboard-key-color, #333);\n font-size: var(--keyboard-font-size, 16px);\n font-weight: 500;\n cursor: pointer;\n user-select: none;\n touch-action: manipulation;\n box-shadow: var(--keyboard-key-shadow, 0 1px 3px rgba(0, 0, 0, 0.1));\n transition: background-color 0.1s, transform 0.1s;\n}\n\n.keyboard-key.has-options {\n padding-right: 16px;\n}\n\n.keyboard-key:active {\n background-color: var(--keyboard-key-active-bg, #e0e0e0);\n transform: scale(0.95);\n}\n\n.keyboard-key--disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.keyboard-key--disabled:active {\n transform: none;\n background-color: var(--keyboard-key-bg, #fff);\n}\n\n/* 功能键样式 */\n.keyboard-key--backspace,\n.keyboard-key--shift,\n.keyboard-key--tab,\n.keyboard-key--clear,\n.keyboard-key--close {\n background-color: var(--keyboard-fn-key-bg, #d4d6dc);\n color: var(--keyboard-fn-key-color, #333);\n font-size: var(--keyboard-fn-font-size, 14px);\n}\n\n.keyboard-key--backspace:active,\n.keyboard-key--shift:active,\n.keyboard-key--tab:active,\n.keyboard-key--clear:active,\n.keyboard-key--close:active {\n background-color: var(--keyboard-fn-key-active-bg, #b8bac0);\n}\n\n/* Shift 激活状态 */\n.keyboard-key--active {\n background-color: var(--keyboard-key-active-bg, #007aff) !important;\n color: #fff !important;\n}\n\n/* 完成/确认键 */\n.keyboard-key--done,\n.keyboard-key--enter {\n background-color: var(--keyboard-primary-bg, #007aff);\n color: var(--keyboard-primary-color, #fff);\n font-size: var(--keyboard-fn-font-size, 14px);\n font-weight: 600;\n}\n\n.keyboard-key--done:active,\n.keyboard-key--enter:active {\n background-color: var(--keyboard-primary-active-bg, #0062cc);\n}\n\n/* 空格键 */\n.keyboard-key--space {\n background-color: var(--keyboard-key-bg, #fff);\n}\n\n.keyboard-key__icon {\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.keyboard-key__icon svg {\n width: 1em;\n height: 1em;\n}\n\n.keyboard-key__label {\n pointer-events: none;\n line-height: 1;\n}\n\n.keyboard-key__arrow {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 8px;\n opacity: 0.5;\n cursor: pointer;\n user-select: none;\n touch-action: manipulation;\n border-radius: 0 6px 6px 0;\n}\n\n.keyboard-key__arrow:active {\n background-color: rgba(0, 0, 0, 0.1);\n opacity: 1;\n}\n\n/* 纯选项按钮内嵌箭头 */\n.keyboard-key__arrow-inline {\n margin-left: 4px;\n font-size: 8px;\n opacity: 0.6;\n pointer-events: none;\n}\n\n/* 纯选项按钮样式 */\n.keyboard-key.is-options-only {\n font-size: 13px;\n background-color: var(--keyboard-fn-key-bg, #d4d6dc);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type {\n KeyboardConfig,\n KeyboardType,\n KeyDefinition,\n} from '@composy/keyboard-core'\nimport { getLayout } from '@composy/keyboard-core'\nimport KeyboardKey from './KeyboardKey.vue'\n\nconst props = withDefaults(defineProps<{\n /**\n * 键盘类型\n */\n type?: KeyboardType | string\n /**\n * 是否大写状态\n */\n isUpperCase?: boolean\n /**\n * 自定义布局配置\n */\n layout?: KeyboardConfig\n /**\n * 按键基础宽度\n */\n keyWidth?: number\n}>(), {\n type: 'qwerty',\n isUpperCase: false,\n keyWidth: 36,\n})\n\nconst emit = defineEmits<{\n keypress: [key: KeyDefinition]\n selectOption: [option: string]\n}>()\n\n/**\n * 选项面板状态\n */\nconst showOptionsPanel = ref(false)\nconst currentOptions = ref<string[]>([])\n\n/**\n * 当前布局配置\n */\nconst currentLayout = computed(() => {\n if (props.layout) {\n return props.layout\n }\n return getLayout(props.type) ?? null\n})\n\n/**\n * 处理按键\n */\nconst handleKeyPress = (key: KeyDefinition) => {\n emit('keypress', key)\n}\n\n/**\n * 显示选项面板\n */\nconst handleShowOptions = (options: string[]) => {\n currentOptions.value = options\n showOptionsPanel.value = true\n}\n\n/**\n * 处理选项选择\n */\nconst handleSelectOption = (option: string, event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n showOptionsPanel.value = false\n emit('selectOption', option)\n}\n\n/**\n * 关闭选项面板\n */\nconst closeOptionsPanel = () => {\n showOptionsPanel.value = false\n}\n\n/**\n * 计算每行的总宽度,用于居中对齐\n */\nconst getRowStyle = (rowIndex: number) => {\n // 可以根据需要添加行样式\n return {}\n}\n</script>\n\n<template>\n <div class=\"virtual-keyboard\">\n <template v-if=\"currentLayout\">\n <div\n v-for=\"(row, rowIndex) in currentLayout.rows\"\n :key=\"rowIndex\"\n class=\"virtual-keyboard__row\"\n :class=\"row.className\"\n :style=\"getRowStyle(rowIndex)\"\n >\n <KeyboardKey\n v-for=\"key in row.keys\"\n :key=\"key.id\"\n :key-def=\"key\"\n :is-upper-case=\"isUpperCase\"\n :base-width=\"keyWidth\"\n :use-flex=\"row.className === 'numpad-row'\"\n @press=\"handleKeyPress\"\n @show-options=\"handleShowOptions\"\n />\n </div>\n </template>\n <div v-else class=\"virtual-keyboard__empty\">\n 键盘布局未找到\n </div>\n\n <!-- 选项面板 -->\n <Transition name=\"options-panel\">\n <div v-if=\"showOptionsPanel\" class=\"keyboard-options-panel\">\n <div class=\"keyboard-options-panel__header\">\n <span class=\"keyboard-options-panel__title\">选择邮箱后缀</span>\n <button class=\"keyboard-options-panel__close\" type=\"button\" @click=\"closeOptionsPanel\" @touchstart.prevent=\"closeOptionsPanel\">\n ×\n </button>\n </div>\n <div class=\"keyboard-options-panel__grid\">\n <button\n v-for=\"option in currentOptions\"\n :key=\"option\"\n class=\"keyboard-options-panel__item\"\n type=\"button\"\n @mousedown.prevent=\"(e) => handleSelectOption(option, e)\"\n @touchstart.prevent=\"(e) => handleSelectOption(option, e)\"\n >\n {{ option }}\n </button>\n </div>\n </div>\n </Transition>\n </div>\n</template>\n\n<style>\n.virtual-keyboard {\n --key-gap: var(--keyboard-key-gap, 6px);\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--key-gap);\n padding: var(--keyboard-padding, 6px);\n background-color: var(--keyboard-bg, #d1d5db);\n border-radius: 0;\n user-select: none;\n -webkit-user-select: none;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;\n}\n\n.virtual-keyboard__row {\n display: flex;\n justify-content: center;\n gap: var(--key-gap);\n width: 100%;\n padding: 0 2px;\n}\n\n/* 数字键盘行样式 - 按钮均匀分布填满宽度 */\n.virtual-keyboard__row.numpad-row {\n justify-content: stretch;\n}\n\n.virtual-keyboard__row.numpad-row .keyboard-key-wrapper {\n flex: 1;\n}\n\n.virtual-keyboard__empty {\n padding: 20px;\n color: #999;\n text-align: center;\n}\n\n/* 全屏选项面板 */\n.keyboard-options-panel {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n background: var(--keyboard-bg, #d1d5db);\n z-index: 10;\n display: flex;\n flex-direction: column;\n padding: 8px;\n}\n\n.keyboard-options-panel__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 8px 12px;\n border-bottom: 1px solid rgba(0, 0, 0, 0.08);\n margin-bottom: 8px;\n}\n\n.keyboard-options-panel__title {\n font-size: 14px;\n font-weight: 600;\n color: var(--keyboard-key-color, #333);\n}\n\n.keyboard-options-panel__close {\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 50%;\n background: var(--keyboard-fn-key-bg, #d4d6dc);\n color: var(--keyboard-key-color, #666);\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.keyboard-options-panel__close:active {\n background: var(--keyboard-fn-key-active-bg, #b8bac0);\n}\n\n.keyboard-options-panel__grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n flex: 1;\n overflow-y: auto;\n padding: 4px 0;\n}\n\n.keyboard-options-panel__item {\n padding: 14px 8px;\n border: none;\n border-radius: 8px;\n background: var(--keyboard-key-bg, #fff);\n color: var(--keyboard-key-color, #333);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n transition: background-color 0.15s, transform 0.1s;\n}\n\n.keyboard-options-panel__item:active {\n background: var(--keyboard-option-active-bg, #e8e8e8);\n transform: scale(0.96);\n}\n\n/* 面板动画 */\n.options-panel-enter-active,\n.options-panel-leave-active {\n transition: opacity 0.2s ease, transform 0.2s ease;\n}\n\n.options-panel-enter-from,\n.options-panel-leave-to {\n opacity: 0;\n transform: translateY(20px);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type {\n KeyboardConfig,\n KeyboardType,\n KeyDefinition,\n} from '@composy/keyboard-core'\nimport { getLayout } from '@composy/keyboard-core'\nimport KeyboardKey from './KeyboardKey.vue'\n\nconst props = withDefaults(defineProps<{\n /**\n * 键盘类型\n */\n type?: KeyboardType | string\n /**\n * 是否大写状态\n */\n isUpperCase?: boolean\n /**\n * 自定义布局配置\n */\n layout?: KeyboardConfig\n /**\n * 按键基础宽度\n */\n keyWidth?: number\n}>(), {\n type: 'qwerty',\n isUpperCase: false,\n keyWidth: 36,\n})\n\nconst emit = defineEmits<{\n keypress: [key: KeyDefinition]\n selectOption: [option: string]\n}>()\n\n/**\n * 选项面板状态\n */\nconst showOptionsPanel = ref(false)\nconst currentOptions = ref<string[]>([])\n\n/**\n * 当前布局配置\n */\nconst currentLayout = computed(() => {\n if (props.layout) {\n return props.layout\n }\n return getLayout(props.type) ?? null\n})\n\n/**\n * 处理按键\n */\nconst handleKeyPress = (key: KeyDefinition) => {\n emit('keypress', key)\n}\n\n/**\n * 显示选项面板\n */\nconst handleShowOptions = (options: string[]) => {\n currentOptions.value = options\n showOptionsPanel.value = true\n}\n\n/**\n * 处理选项选择\n */\nconst handleSelectOption = (option: string, event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n showOptionsPanel.value = false\n emit('selectOption', option)\n}\n\n/**\n * 关闭选项面板\n */\nconst closeOptionsPanel = () => {\n showOptionsPanel.value = false\n}\n\n/**\n * 计算每行的总宽度,用于居中对齐\n */\nconst getRowStyle = (rowIndex: number) => {\n // 可以根据需要添加行样式\n return {}\n}\n</script>\n\n<template>\n <div class=\"virtual-keyboard\">\n <template v-if=\"currentLayout\">\n <div\n v-for=\"(row, rowIndex) in currentLayout.rows\"\n :key=\"rowIndex\"\n class=\"virtual-keyboard__row\"\n :class=\"row.className\"\n :style=\"getRowStyle(rowIndex)\"\n >\n <KeyboardKey\n v-for=\"key in row.keys\"\n :key=\"key.id\"\n :key-def=\"key\"\n :is-upper-case=\"isUpperCase\"\n :base-width=\"keyWidth\"\n :use-flex=\"row.className === 'numpad-row'\"\n @press=\"handleKeyPress\"\n @show-options=\"handleShowOptions\"\n />\n </div>\n </template>\n <div v-else class=\"virtual-keyboard__empty\">\n 键盘布局未找到\n </div>\n\n <!-- 选项面板 -->\n <Transition name=\"options-panel\">\n <div v-if=\"showOptionsPanel\" class=\"keyboard-options-panel\">\n <div class=\"keyboard-options-panel__header\">\n <span class=\"keyboard-options-panel__title\">选择邮箱后缀</span>\n <button class=\"keyboard-options-panel__close\" type=\"button\" @click=\"closeOptionsPanel\" @touchstart.prevent=\"closeOptionsPanel\">\n ×\n </button>\n </div>\n <div class=\"keyboard-options-panel__grid\">\n <button\n v-for=\"option in currentOptions\"\n :key=\"option\"\n class=\"keyboard-options-panel__item\"\n type=\"button\"\n @mousedown.prevent=\"(e) => handleSelectOption(option, e)\"\n @touchstart.prevent=\"(e) => handleSelectOption(option, e)\"\n >\n {{ option }}\n </button>\n </div>\n </div>\n </Transition>\n </div>\n</template>\n\n<style>\n.virtual-keyboard {\n --key-gap: var(--keyboard-key-gap, 6px);\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--key-gap);\n padding: var(--keyboard-padding, 6px);\n background-color: var(--keyboard-bg, #d1d5db);\n border-radius: 0;\n user-select: none;\n -webkit-user-select: none;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;\n}\n\n.virtual-keyboard__row {\n display: flex;\n justify-content: center;\n gap: var(--key-gap);\n width: 100%;\n padding: 0 2px;\n}\n\n/* 数字键盘行样式 - 按钮均匀分布填满宽度 */\n.virtual-keyboard__row.numpad-row {\n justify-content: stretch;\n}\n\n.virtual-keyboard__row.numpad-row .keyboard-key-wrapper {\n flex: 1;\n}\n\n.virtual-keyboard__empty {\n padding: 20px;\n color: #999;\n text-align: center;\n}\n\n/* 全屏选项面板 */\n.keyboard-options-panel {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n background: var(--keyboard-bg, #d1d5db);\n z-index: 10;\n display: flex;\n flex-direction: column;\n padding: 8px;\n}\n\n.keyboard-options-panel__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 8px 12px;\n border-bottom: 1px solid rgba(0, 0, 0, 0.08);\n margin-bottom: 8px;\n}\n\n.keyboard-options-panel__title {\n font-size: 14px;\n font-weight: 600;\n color: var(--keyboard-key-color, #333);\n}\n\n.keyboard-options-panel__close {\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 50%;\n background: var(--keyboard-fn-key-bg, #d4d6dc);\n color: var(--keyboard-key-color, #666);\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.keyboard-options-panel__close:active {\n background: var(--keyboard-fn-key-active-bg, #b8bac0);\n}\n\n.keyboard-options-panel__grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n flex: 1;\n overflow-y: auto;\n padding: 4px 0;\n}\n\n.keyboard-options-panel__item {\n padding: 14px 8px;\n border: none;\n border-radius: 8px;\n background: var(--keyboard-key-bg, #fff);\n color: var(--keyboard-key-color, #333);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n transition: background-color 0.15s, transform 0.1s;\n}\n\n.keyboard-options-panel__item:active {\n background: var(--keyboard-option-active-bg, #e8e8e8);\n transform: scale(0.96);\n}\n\n/* 面板动画 */\n.options-panel-enter-active,\n.options-panel-leave-active {\n transition: opacity 0.2s ease, transform 0.2s ease;\n}\n\n.options-panel-enter-from,\n.options-panel-leave-to {\n opacity: 0;\n transform: translateY(20px);\n}\n</style>\n","/**\n * 键盘管理 Composable\n * @module composables/useKeyboard\n */\n\nimport { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'\nimport type { Ref } from 'vue'\nimport {\n createKeyboardManager,\n type KeyboardConfig,\n type KeyboardEventMap,\n type KeyboardOptions,\n type KeyboardState,\n type KeyboardType,\n type KeyDefinition,\n} from '@composy/keyboard-core'\n\n/**\n * useKeyboard 选项\n */\nexport interface UseKeyboardOptions extends KeyboardOptions {\n /**\n * 是否立即显示\n */\n immediate?: boolean\n}\n\n/**\n * useKeyboard 返回值\n */\nexport interface UseKeyboardReturn {\n /**\n * 键盘是否可见\n */\n visible: Ref<boolean>\n\n /**\n * 当前键盘类型\n */\n type: Ref<KeyboardType>\n\n /**\n * 是否大写状态\n */\n isUpperCase: Ref<boolean>\n\n /**\n * 当前输入值\n */\n value: Ref<string>\n\n /**\n * 光标位置\n */\n cursorPosition: Ref<number>\n\n /**\n * 当前布局配置\n */\n layout: Ref<KeyboardConfig | null>\n\n /**\n * 完整状态\n */\n state: KeyboardState\n\n /**\n * 显示键盘\n */\n show: (input?: HTMLInputElement | HTMLTextAreaElement) => void\n\n /**\n * 隐藏键盘\n */\n hide: () => void\n\n /**\n * 切换显示/隐藏\n */\n toggle: (input?: HTMLInputElement | HTMLTextAreaElement) => void\n\n /**\n * 切换键盘类型\n */\n switchType: (type: KeyboardType | string) => void\n\n /**\n * 切换大小写\n */\n toggleShift: () => void\n\n /**\n * 处理按键\n */\n handleKey: (key: KeyDefinition) => void\n\n /**\n * 获取按键显示文本\n */\n getKeyLabel: (key: KeyDefinition) => string\n\n /**\n * 设置值\n */\n setValue: (value: string) => void\n\n /**\n * 追加值(在光标位置插入文本)\n */\n appendValue: (text: string) => void\n\n /**\n * 设置最大长度\n */\n setMaxLength: (maxLength?: number) => void\n\n /**\n * 绑定输入框\n */\n bindInput: (input: HTMLInputElement | HTMLTextAreaElement) => void\n\n /**\n * 解绑输入框\n */\n unbindInput: () => void\n\n /**\n * 事件监听\n */\n on: <K extends keyof KeyboardEventMap>(\n event: K,\n handler: (data: KeyboardEventMap[K]) => void\n ) => () => void\n\n /**\n * 单次事件监听\n */\n once: <K extends keyof KeyboardEventMap>(\n event: K,\n handler: (data: KeyboardEventMap[K]) => void\n ) => () => void\n\n /**\n * 销毁\n */\n destroy: () => void\n}\n\n/**\n * 键盘管理 Composable\n */\nexport function useKeyboard(options: UseKeyboardOptions = {}): UseKeyboardReturn {\n const { immediate = false, ...keyboardOptions } = options\n\n // 创建键盘管理器\n const manager = createKeyboardManager(keyboardOptions)\n\n // 响应式状态\n const visible = ref(false)\n const type = ref<KeyboardType>(keyboardOptions.type ?? 'qwerty')\n const isUpperCase = ref(keyboardOptions.defaultUpperCase ?? false)\n const value = ref(keyboardOptions.value ?? '')\n const cursorPosition = ref(keyboardOptions.value?.length ?? 0)\n const layout = shallowRef<KeyboardConfig | null>(manager.getLayout())\n\n // 完整状态(响应式对象)\n const state = reactive<KeyboardState>({\n visible: false,\n type: keyboardOptions.type ?? 'qwerty',\n isUpperCase: keyboardOptions.defaultUpperCase ?? false,\n value: keyboardOptions.value ?? '',\n cursorPosition: keyboardOptions.value?.length ?? 0,\n maxLength: keyboardOptions.maxLength,\n inputElement: null,\n })\n\n // 同步管理器状态到 Vue 响应式状态\n const syncState = (newState: KeyboardState) => {\n visible.value = newState.visible\n type.value = newState.type\n isUpperCase.value = newState.isUpperCase\n value.value = newState.value\n cursorPosition.value = newState.cursorPosition\n\n Object.assign(state, newState)\n }\n\n // 监听状态变化\n manager.on('stateChange', syncState)\n\n // 监听键盘类型切换,更新布局\n manager.on('switch', () => {\n layout.value = manager.getLayout()\n })\n\n // 立即显示\n if (immediate) {\n manager.show()\n }\n\n // 组件卸载时销毁\n onBeforeUnmount(() => {\n manager.destroy()\n })\n\n return {\n visible,\n type,\n isUpperCase,\n value,\n cursorPosition,\n layout,\n state,\n show: (input?: HTMLInputElement | HTMLTextAreaElement) => manager.show(input),\n hide: () => manager.hide(),\n toggle: (input?: HTMLInputElement | HTMLTextAreaElement) => manager.toggle(input),\n switchType: (t: KeyboardType | string) => manager.switchType(t),\n toggleShift: () => manager.toggleShift(),\n handleKey: (key: KeyDefinition) => manager.handleKey(key),\n getKeyLabel: (key: KeyDefinition) => manager.getKeyLabel(key),\n setValue: (v: string) => manager.setValue(v),\n appendValue: (text: string) => manager.appendValue(text),\n setMaxLength: (maxLength?: number) => manager.setMaxLength(maxLength),\n bindInput: (input: HTMLInputElement | HTMLTextAreaElement) => manager.bindInput(input),\n unbindInput: () => manager.unbindInput(),\n on: (event, handler) => manager.on(event, handler),\n once: (event, handler) => manager.once(event, handler),\n destroy: () => manager.destroy(),\n }\n}\n","/**\n * 键盘弹出层 Composable\n * @module composables/useKeyboardPopup\n */\n\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport type { CSSProperties, Ref } from 'vue'\nimport {\n createPositionManager,\n isMobile,\n onClickOutside,\n onViewportChange,\n type PopupPlacement,\n type PopupPositionConfig,\n type PopupPositionResult,\n} from '@composy/keyboard-core'\n\n/**\n * useKeyboardPopup 选项\n */\nexport interface UseKeyboardPopupOptions extends PopupPositionConfig {\n /**\n * 是否显示\n */\n visible?: Ref<boolean>\n\n /**\n * 目标元素\n */\n target?: Ref<HTMLElement | null>\n\n /**\n * 弹出层元素\n */\n popup?: Ref<HTMLElement | null>\n\n /**\n * 点击外部关闭\n */\n closeOnClickOutside?: boolean\n\n /**\n * 关闭回调\n */\n onClose?: () => void\n}\n\n/**\n * useKeyboardPopup 返回值\n */\nexport interface UseKeyboardPopupReturn {\n /**\n * 弹出层样式\n */\n popupStyles: Ref<CSSProperties>\n\n /**\n * 遮罩层样式\n */\n maskStyles: Ref<CSSProperties>\n\n /**\n * 当前弹出位置\n */\n placement: Ref<PopupPlacement>\n\n /**\n * 是否移动端\n */\n isMobileDevice: Ref<boolean>\n\n /**\n * 是否显示遮罩\n */\n showMask: Ref<boolean>\n\n /**\n * 是否正在动画中\n */\n isAnimating: Ref<boolean>\n\n /**\n * 更新位置\n */\n updatePosition: () => void\n\n /**\n * 关闭弹出层\n */\n close: () => void\n}\n\n/**\n * 键盘弹出层 Composable\n */\nexport function useKeyboardPopup(options: UseKeyboardPopupOptions = {}): UseKeyboardPopupReturn {\n const {\n visible = ref(false),\n target = ref(null),\n popup = ref(null),\n closeOnClickOutside = true,\n onClose,\n ...positionConfig\n } = options\n\n // 创建位置管理器\n const positionManager = createPositionManager(positionConfig)\n\n // 响应式状态\n const popupStyles = ref<CSSProperties>({})\n const maskStyles = ref<CSSProperties>({})\n const placement = ref<PopupPlacement>('bottom')\n const isMobileDevice = ref(isMobile())\n const showMask = ref(false)\n const isAnimating = ref(false)\n\n // 清理函数\n let cleanupClickOutside: (() => void) | null = null\n let cleanupViewportChange: (() => void) | null = null\n let cleanupScroll: (() => void) | null = null\n\n /**\n * 更新弹出层位置\n */\n const updatePosition = () => {\n if (!popup.value) return\n\n const popupRect = popup.value.getBoundingClientRect()\n const result = positionManager.calculate(target.value, {\n width: popupRect.width || 300,\n height: popupRect.height || 260,\n })\n\n placement.value = result.placement\n showMask.value = positionManager.shouldShowMask()\n\n // 设置弹出层样式\n if (result.placement === 'bottom-sheet') {\n // 移动端底部弹出\n popupStyles.value = {\n position: 'fixed',\n left: '0',\n right: '0',\n bottom: '0',\n maxHeight: `${result.maxHeight}px`,\n zIndex: 1000,\n }\n }\n else {\n // PC 端下拉弹出\n popupStyles.value = {\n position: 'fixed',\n left: `${result.position.x}px`,\n top: `${result.position.y}px`,\n maxHeight: result.maxHeight ? `${result.maxHeight}px` : undefined,\n maxWidth: result.maxWidth ? `${result.maxWidth}px` : undefined,\n zIndex: 1000,\n }\n }\n\n // 设置遮罩层样式\n if (showMask.value) {\n maskStyles.value = {\n position: 'fixed',\n top: '0',\n left: '0',\n right: '0',\n bottom: '0',\n backgroundColor: 'rgba(0, 0, 0, 0.5)',\n zIndex: 999,\n }\n }\n }\n\n /**\n * 关闭弹出层\n */\n const close = () => {\n onClose?.()\n }\n\n // 监听 visible 变化\n watch(visible, (newVisible) => {\n if (newVisible) {\n // 显示时\n isMobileDevice.value = isMobile()\n\n // 延迟计算位置(等待 DOM 渲染)\n requestAnimationFrame(() => {\n updatePosition()\n\n // 添加点击外部关闭监听\n if (closeOnClickOutside && popup.value) {\n cleanupClickOutside = onClickOutside(popup.value, (event) => {\n // 检查是否点击在目标元素上\n if (target.value && target.value.contains(event.target as Node)) {\n return\n }\n close()\n })\n }\n })\n\n // 监听视口变化\n cleanupViewportChange = onViewportChange(() => {\n isMobileDevice.value = isMobile()\n updatePosition()\n })\n\n // PC 端监听滚动事件,跟随滚动\n if (!isMobileDevice.value) {\n const handleScroll = () => {\n requestAnimationFrame(() => {\n updatePosition()\n })\n }\n window.addEventListener('scroll', handleScroll, true)\n cleanupScroll = () => {\n window.removeEventListener('scroll', handleScroll, true)\n }\n }\n }\n else {\n // 隐藏时清理\n cleanupClickOutside?.()\n cleanupClickOutside = null\n cleanupViewportChange?.()\n cleanupViewportChange = null\n cleanupScroll?.()\n cleanupScroll = null\n }\n }, { immediate: true })\n\n // 监听目标元素变化\n watch(target, () => {\n if (visible.value) {\n updatePosition()\n }\n })\n\n // 组件卸载时清理\n onBeforeUnmount(() => {\n cleanupClickOutside?.()\n cleanupViewportChange?.()\n cleanupScroll?.()\n })\n\n return {\n popupStyles,\n maskStyles,\n placement,\n isMobileDevice,\n showMask,\n isAnimating,\n updatePosition,\n close,\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, ref, watch, Teleport, Transition, onMounted, onBeforeUnmount, nextTick } from 'vue'\nimport type { KeyboardType, KeyDefinition, KeyboardOptions } from '@composy/keyboard-core'\nimport { isMobile } from '@composy/keyboard-core'\nimport { useKeyboard } from '../composables/useKeyboard'\nimport { useKeyboardPopup } from '../composables/useKeyboardPopup'\nimport VirtualKeyboard from './VirtualKeyboard.vue'\n\nconst props = withDefaults(defineProps<{\n /**\n * 是否显示 (v-model)\n */\n modelValue?: boolean\n /**\n * 键盘类型\n */\n type?: KeyboardType | string\n /**\n * 绑定的输入值 (v-model:value)\n */\n value?: string\n /**\n * 目标输入框元素\n */\n target?: HTMLInputElement | HTMLTextAreaElement | null\n /**\n * 最大输入长度\n */\n maxLength?: number\n /**\n * 点击外部是否关闭\n */\n closeOnClickOutside?: boolean\n /**\n * 是否启用震动反馈\n */\n enableVibrate?: boolean\n /**\n * 挂载目标\n */\n teleportTo?: string | HTMLElement\n}>(), {\n modelValue: false,\n type: 'qwerty',\n value: '',\n target: null,\n closeOnClickOutside: true,\n enableVibrate: true,\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [visible: boolean]\n 'update:value': [value: string]\n 'confirm': [value: string]\n 'keypress': [key: KeyDefinition]\n}>()\n\n// 弹出层 DOM 引用\nconst popupRef = ref<HTMLElement | null>(null)\n\n// 目标元素引用\nconst targetRef = computed(() => props.target as HTMLElement | null)\n\n// 是否移动端\nconst isMobileDevice = ref(isMobile())\n\n// 创建键盘管理器\nconst keyboard = useKeyboard({\n type: props.type as KeyboardType,\n value: props.value,\n maxLength: props.maxLength || (props.type.startsWith('license-plate') ? 7 : undefined),\n enableVibrate: props.enableVibrate,\n})\n\n// 创建弹出层管理\nconst {\n popupStyles,\n maskStyles,\n placement,\n showMask,\n updatePosition,\n close: closePopup,\n} = useKeyboardPopup({\n visible: computed(() => props.modelValue),\n target: targetRef,\n popup: popupRef,\n closeOnClickOutside: props.closeOnClickOutside,\n onClose: () => {\n emit('update:modelValue', false)\n },\n})\n\n// 监听键盘类型变化\nwatch(() => props.type, (newType) => {\n keyboard.switchType(newType as KeyboardType)\n \n // 车牌键盘需要设置默认 maxLength\n if (newType.startsWith('license-plate') && !props.maxLength) {\n keyboard.setMaxLength(7)\n }\n})\n\n// 监听外部 value 变化\nwatch(() => props.value, (newValue) => {\n if (newValue !== keyboard.value.value) {\n keyboard.setValue(newValue)\n }\n})\n\n// 监听键盘内部 value 变化\nwatch(keyboard.value, (newValue) => {\n emit('update:value', newValue)\n})\n\n// 监听可见性变化\nwatch(() => props.modelValue, (visible) => {\n if (visible) {\n isMobileDevice.value = isMobile()\n if (props.target) {\n keyboard.bindInput(props.target)\n }\n keyboard.show(props.target ?? undefined)\n \n nextTick(() => {\n updatePosition()\n })\n } else {\n keyboard.hide()\n }\n})\n\n// 监听确认事件\nkeyboard.on('confirm', ({ value }) => {\n emit('confirm', value)\n emit('update:modelValue', false)\n})\n\n/**\n * 处理按键\n */\nconst handleKeyPress = (key: KeyDefinition) => {\n keyboard.handleKey(key)\n emit('keypress', key)\n}\n\n/**\n * 处理下拉选项选择(如邮箱后缀)\n */\nconst handleSelectOption = (option: string) => {\n let valueToAppend = option\n \n // 智能处理邮箱后缀:如果当前值已以 @ 结尾,而选项以 @ 开头,则去掉选项的 @\n if (option.startsWith('@') && keyboard.value.value.endsWith('@')) {\n valueToAppend = option.slice(1) // 去掉开头的 @\n }\n \n keyboard.appendValue(valueToAppend)\n}\n\n/**\n * 处理遮罩点击\n */\nconst handleMaskClick = () => {\n if (props.closeOnClickOutside) {\n emit('update:modelValue', false)\n }\n}\n\n/**\n * 计算弹出层类名\n */\nconst popupClass = computed(() => {\n return [\n 'keyboard-popup',\n `keyboard-popup--${placement.value}`,\n {\n 'keyboard-popup--mobile': isMobileDevice.value,\n 'keyboard-popup--desktop': !isMobileDevice.value,\n },\n ]\n})\n</script>\n\n<template>\n <Teleport :to=\"teleportTo\">\n <Transition name=\"keyboard-mask\">\n <div\n v-if=\"modelValue && showMask\"\n class=\"keyboard-mask\"\n :style=\"maskStyles\"\n @click=\"handleMaskClick\"\n />\n </Transition>\n\n <Transition :name=\"isMobileDevice ? 'keyboard-slide' : 'keyboard-fade'\">\n <div\n v-if=\"modelValue\"\n ref=\"popupRef\"\n :class=\"popupClass\"\n :style=\"popupStyles\"\n @mousedown.prevent\n @touchstart.prevent\n >\n <VirtualKeyboard\n :type=\"keyboard.type.value\"\n :is-upper-case=\"keyboard.isUpperCase.value\"\n @keypress=\"handleKeyPress\"\n @select-option=\"handleSelectOption\"\n />\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style>\n.keyboard-popup {\n background-color: var(--keyboard-bg, #d1d5db);\n box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n}\n\n.keyboard-popup--desktop {\n border-radius: 12px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.1);\n min-width: 320px;\n}\n\n.keyboard-popup--mobile {\n border-radius: 0;\n padding-bottom: env(safe-area-inset-bottom, 0);\n}\n\n.keyboard-popup--bottom-sheet {\n border-top-left-radius: 16px;\n border-top-right-radius: 16px;\n}\n\n.keyboard-mask {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n z-index: 999;\n}\n\n/* 遮罩动画 */\n.keyboard-mask-enter-active,\n.keyboard-mask-leave-active {\n transition: opacity 0.3s ease;\n}\n\n.keyboard-mask-enter-from,\n.keyboard-mask-leave-to {\n opacity: 0;\n}\n\n/* PC 端淡入淡出动画 */\n.keyboard-fade-enter-active,\n.keyboard-fade-leave-active {\n transition: opacity 0.2s ease, transform 0.2s ease;\n}\n\n.keyboard-fade-enter-from,\n.keyboard-fade-leave-to {\n opacity: 0;\n transform: translateY(-10px);\n}\n\n/* 移动端滑入滑出动画 */\n.keyboard-slide-enter-active,\n.keyboard-slide-leave-active {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.keyboard-slide-enter-from,\n.keyboard-slide-leave-to {\n transform: translateY(100%);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch, Teleport, Transition, onMounted, onBeforeUnmount, nextTick } from 'vue'\nimport type { KeyboardType, KeyDefinition, KeyboardOptions } from '@composy/keyboard-core'\nimport { isMobile } from '@composy/keyboard-core'\nimport { useKeyboard } from '../composables/useKeyboard'\nimport { useKeyboardPopup } from '../composables/useKeyboardPopup'\nimport VirtualKeyboard from './VirtualKeyboard.vue'\n\nconst props = withDefaults(defineProps<{\n /**\n * 是否显示 (v-model)\n */\n modelValue?: boolean\n /**\n * 键盘类型\n */\n type?: KeyboardType | string\n /**\n * 绑定的输入值 (v-model:value)\n */\n value?: string\n /**\n * 目标输入框元素\n */\n target?: HTMLInputElement | HTMLTextAreaElement | null\n /**\n * 最大输入长度\n */\n maxLength?: number\n /**\n * 点击外部是否关闭\n */\n closeOnClickOutside?: boolean\n /**\n * 是否启用震动反馈\n */\n enableVibrate?: boolean\n /**\n * 挂载目标\n */\n teleportTo?: string | HTMLElement\n}>(), {\n modelValue: false,\n type: 'qwerty',\n value: '',\n target: null,\n closeOnClickOutside: true,\n enableVibrate: true,\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [visible: boolean]\n 'update:value': [value: string]\n 'confirm': [value: string]\n 'keypress': [key: KeyDefinition]\n}>()\n\n// 弹出层 DOM 引用\nconst popupRef = ref<HTMLElement | null>(null)\n\n// 目标元素引用\nconst targetRef = computed(() => props.target as HTMLElement | null)\n\n// 是否移动端\nconst isMobileDevice = ref(isMobile())\n\n// 创建键盘管理器\nconst keyboard = useKeyboard({\n type: props.type as KeyboardType,\n value: props.value,\n maxLength: props.maxLength || (props.type.startsWith('license-plate') ? 7 : undefined),\n enableVibrate: props.enableVibrate,\n})\n\n// 创建弹出层管理\nconst {\n popupStyles,\n maskStyles,\n placement,\n showMask,\n updatePosition,\n close: closePopup,\n} = useKeyboardPopup({\n visible: computed(() => props.modelValue),\n target: targetRef,\n popup: popupRef,\n closeOnClickOutside: props.closeOnClickOutside,\n onClose: () => {\n emit('update:modelValue', false)\n },\n})\n\n// 监听键盘类型变化\nwatch(() => props.type, (newType) => {\n keyboard.switchType(newType as KeyboardType)\n \n // 车牌键盘需要设置默认 maxLength\n if (newType.startsWith('license-plate') && !props.maxLength) {\n keyboard.setMaxLength(7)\n }\n})\n\n// 监听外部 value 变化\nwatch(() => props.value, (newValue) => {\n if (newValue !== keyboard.value.value) {\n keyboard.setValue(newValue)\n }\n})\n\n// 监听键盘内部 value 变化\nwatch(keyboard.value, (newValue) => {\n emit('update:value', newValue)\n})\n\n// 监听可见性变化\nwatch(() => props.modelValue, (visible) => {\n if (visible) {\n isMobileDevice.value = isMobile()\n if (props.target) {\n keyboard.bindInput(props.target)\n }\n keyboard.show(props.target ?? undefined)\n \n nextTick(() => {\n updatePosition()\n })\n } else {\n keyboard.hide()\n }\n})\n\n// 监听确认事件\nkeyboard.on('confirm', ({ value }) => {\n emit('confirm', value)\n emit('update:modelValue', false)\n})\n\n/**\n * 处理按键\n */\nconst handleKeyPress = (key: KeyDefinition) => {\n keyboard.handleKey(key)\n emit('keypress', key)\n}\n\n/**\n * 处理下拉选项选择(如邮箱后缀)\n */\nconst handleSelectOption = (option: string) => {\n let valueToAppend = option\n \n // 智能处理邮箱后缀:如果当前值已以 @ 结尾,而选项以 @ 开头,则去掉选项的 @\n if (option.startsWith('@') && keyboard.value.value.endsWith('@')) {\n valueToAppend = option.slice(1) // 去掉开头的 @\n }\n \n keyboard.appendValue(valueToAppend)\n}\n\n/**\n * 处理遮罩点击\n */\nconst handleMaskClick = () => {\n if (props.closeOnClickOutside) {\n emit('update:modelValue', false)\n }\n}\n\n/**\n * 计算弹出层类名\n */\nconst popupClass = computed(() => {\n return [\n 'keyboard-popup',\n `keyboard-popup--${placement.value}`,\n {\n 'keyboard-popup--mobile': isMobileDevice.value,\n 'keyboard-popup--desktop': !isMobileDevice.value,\n },\n ]\n})\n</script>\n\n<template>\n <Teleport :to=\"teleportTo\">\n <Transition name=\"keyboard-mask\">\n <div\n v-if=\"modelValue && showMask\"\n class=\"keyboard-mask\"\n :style=\"maskStyles\"\n @click=\"handleMaskClick\"\n />\n </Transition>\n\n <Transition :name=\"isMobileDevice ? 'keyboard-slide' : 'keyboard-fade'\">\n <div\n v-if=\"modelValue\"\n ref=\"popupRef\"\n :class=\"popupClass\"\n :style=\"popupStyles\"\n @mousedown.prevent\n @touchstart.prevent\n >\n <VirtualKeyboard\n :type=\"keyboard.type.value\"\n :is-upper-case=\"keyboard.isUpperCase.value\"\n @keypress=\"handleKeyPress\"\n @select-option=\"handleSelectOption\"\n />\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style>\n.keyboard-popup {\n background-color: var(--keyboard-bg, #d1d5db);\n box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n}\n\n.keyboard-popup--desktop {\n border-radius: 12px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.1);\n min-width: 320px;\n}\n\n.keyboard-popup--mobile {\n border-radius: 0;\n padding-bottom: env(safe-area-inset-bottom, 0);\n}\n\n.keyboard-popup--bottom-sheet {\n border-top-left-radius: 16px;\n border-top-right-radius: 16px;\n}\n\n.keyboard-mask {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n z-index: 999;\n}\n\n/* 遮罩动画 */\n.keyboard-mask-enter-active,\n.keyboard-mask-leave-active {\n transition: opacity 0.3s ease;\n}\n\n.keyboard-mask-enter-from,\n.keyboard-mask-leave-to {\n opacity: 0;\n}\n\n/* PC 端淡入淡出动画 */\n.keyboard-fade-enter-active,\n.keyboard-fade-leave-active {\n transition: opacity 0.2s ease, transform 0.2s ease;\n}\n\n.keyboard-fade-enter-from,\n.keyboard-fade-leave-to {\n opacity: 0;\n transform: translateY(-10px);\n}\n\n/* 移动端滑入滑出动画 */\n.keyboard-slide-enter-active,\n.keyboard-slide-leave-active {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.keyboard-slide-enter-from,\n.keyboard-slide-leave-to {\n transform: translateY(100%);\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * 智能车牌号键盘组件\n * 核心逻辑在 KeyboardManager 中实现自动切换\n * Vue 组件只负责 UI 展示和状态同步\n */\nimport { computed, watch } from 'vue'\nimport type { KeyDefinition } from '@composy/keyboard-core'\nimport { useKeyboard } from '../composables/useKeyboard'\nimport VirtualKeyboard from './VirtualKeyboard.vue'\n\nconst props = withDefaults(defineProps<{\n /**\n * 当前车牌号值(双向绑定)\n */\n modelValue?: string\n /**\n * 是否新能源车牌(8位)\n * @default false\n */\n newEnergy?: boolean\n}>(), {\n modelValue: '',\n newEnergy: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n done: [value: string]\n}>()\n\n/**\n * 最大长度\n */\nconst maxLength = computed(() => props.newEnergy ? 8 : 7)\n\n/**\n * 使用 KeyboardManager 管理键盘状态\n */\nconst keyboard = useKeyboard({\n type: 'license-plate-province',\n value: props.modelValue,\n maxLength: maxLength.value,\n enableVibrate: true,\n})\n\n// 监听外部 value 变化\nwatch(() => props.modelValue, (newValue) => {\n if (newValue !== keyboard.value.value) {\n keyboard.setValue(newValue)\n }\n})\n\n// 监听键盘内部 value 变化\nwatch(keyboard.value, (newValue) => {\n emit('update:modelValue', newValue)\n})\n\n// 监听 maxLength 变化\nwatch(maxLength, (newMaxLength) => {\n keyboard.setMaxLength(newMaxLength)\n \n // 如果当前值超过新的最大长度,截断\n if (props.modelValue.length > newMaxLength) {\n const truncated = props.modelValue.slice(0, newMaxLength)\n keyboard.setValue(truncated)\n emit('update:modelValue', truncated)\n }\n})\n\n// 监听确认事件\nkeyboard.on('confirm', ({ value }) => {\n emit('done', value)\n})\n\n/**\n * 处理按键\n */\nconst handleKeyPress = (key: KeyDefinition) => {\n keyboard.handleKey(key)\n}\n\n/**\n * 当前位置提示\n */\nconst positionHint = computed(() => {\n const len = props.modelValue.length\n if (len === 0) return '请选择省份'\n if (len === 1) return '请输入字母'\n if (len < maxLength.value) return `第${len + 1}位`\n return '已完成'\n})\n</script>\n\n<template>\n <div class=\"license-plate-keyboard\">\n <!-- 输入显示区 -->\n <div class=\"license-plate-keyboard__display\">\n <div class=\"license-plate-keyboard__value\">\n <span\n v-for=\"(char, index) in modelValue.padEnd(maxLength, ' ').split('')\"\n :key=\"index\"\n class=\"license-plate-keyboard__char\"\n :class=\"{\n 'license-plate-keyboard__char--filled': char !== ' ',\n 'license-plate-keyboard__char--current': index === modelValue.length,\n 'license-plate-keyboard__char--province': index === 0,\n 'license-plate-keyboard__char--new-energy': newEnergy && index === 7\n }\"\n >\n {{ char === ' ' ? '' : char }}\n </span>\n </div>\n <div class=\"license-plate-keyboard__hint\">{{ positionHint }}</div>\n </div>\n \n <!-- 键盘区 - 由 core 的 KeyboardManager 自动切换布局 -->\n <VirtualKeyboard\n :type=\"keyboard.type.value\"\n :is-upper-case=\"keyboard.isUpperCase.value\"\n @keypress=\"handleKeyPress\"\n />\n </div>\n</template>\n\n<style>\n.license-plate-keyboard {\n display: flex;\n flex-direction: column;\n background: var(--keyboard-bg, #d1d5db);\n}\n\n.license-plate-keyboard__display {\n padding: 14px 20px;\n background: var(--keyboard-display-bg, #f5f5f5);\n border-bottom: 1px solid rgba(0, 0, 0, 0.06);\n}\n\n.license-plate-keyboard__value {\n display: flex;\n justify-content: center;\n gap: 8px;\n margin-bottom: 8px;\n}\n\n.license-plate-keyboard__char {\n width: 40px;\n height: 48px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #fff;\n border: 2px solid #e0e0e0;\n border-radius: 8px;\n font-size: 20px;\n font-weight: 600;\n color: var(--keyboard-key-color, #333);\n transition: border-color 0.2s, background-color 0.2s;\n}\n\n.license-plate-keyboard__char--filled {\n border-color: var(--keyboard-primary-bg, #007aff);\n background: rgba(0, 122, 255, 0.05);\n}\n\n.license-plate-keyboard__char--current {\n border-color: var(--keyboard-primary-bg, #007aff);\n background: rgba(0, 122, 255, 0.1);\n animation: plate-char-pulse 1s infinite;\n}\n\n.license-plate-keyboard__char--province {\n background: rgba(255, 193, 7, 0.1);\n}\n\n.license-plate-keyboard__char--province.license-plate-keyboard__char--filled {\n background: rgba(255, 193, 7, 0.15);\n border-color: #ffc107;\n}\n\n.license-plate-keyboard__char--new-energy {\n background: rgba(34, 197, 94, 0.1);\n}\n\n.license-plate-keyboard__char--new-energy.license-plate-keyboard__char--filled {\n background: rgba(34, 197, 94, 0.15);\n border-color: #22c55e;\n}\n\n@keyframes plate-char-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n}\n\n.license-plate-keyboard__hint {\n text-align: center;\n font-size: 12px;\n color: #888;\n}\n\n/* 车牌键盘专用样式 */\n.license-plate-keyboard :deep(.plate-keyboard-row) {\n gap: 4px;\n}\n\n.license-plate-keyboard :deep(.key-plate-province),\n.license-plate-keyboard :deep(.key-plate-letter),\n.license-plate-keyboard :deep(.key-plate-number) {\n min-width: 30px;\n height: 40px;\n font-size: 15px;\n border-radius: var(--keyboard-key-radius, 6px);\n}\n\n.license-plate-keyboard :deep(.key-plate-special) {\n background: rgba(255, 193, 7, 0.2);\n color: #b8860b;\n}\n\n.license-plate-keyboard :deep(.key-plate-fn) {\n background: var(--keyboard-fn-key-bg, #d4d6dc);\n}\n\n.license-plate-keyboard :deep(.key-plate-switch) {\n background: var(--keyboard-fn-key-bg, #d4d6dc);\n font-size: 13px;\n}\n\n.license-plate-keyboard :deep(.key-plate-done) {\n background: var(--keyboard-primary-bg, #007aff);\n color: #fff;\n font-size: 13px;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * 智能车牌号键盘组件\n * 核心逻辑在 KeyboardManager 中实现自动切换\n * Vue 组件只负责 UI 展示和状态同步\n */\nimport { computed, watch } from 'vue'\nimport type { KeyDefinition } from '@composy/keyboard-core'\nimport { useKeyboard } from '../composables/useKeyboard'\nimport VirtualKeyboard from './VirtualKeyboard.vue'\n\nconst props = withDefaults(defineProps<{\n /**\n * 当前车牌号值(双向绑定)\n */\n modelValue?: string\n /**\n * 是否新能源车牌(8位)\n * @default false\n */\n newEnergy?: boolean\n}>(), {\n modelValue: '',\n newEnergy: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string]\n done: [value: string]\n}>()\n\n/**\n * 最大长度\n */\nconst maxLength = computed(() => props.newEnergy ? 8 : 7)\n\n/**\n * 使用 KeyboardManager 管理键盘状态\n */\nconst keyboard = useKeyboard({\n type: 'license-plate-province',\n value: props.modelValue,\n maxLength: maxLength.value,\n enableVibrate: true,\n})\n\n// 监听外部 value 变化\nwatch(() => props.modelValue, (newValue) => {\n if (newValue !== keyboard.value.value) {\n keyboard.setValue(newValue)\n }\n})\n\n// 监听键盘内部 value 变化\nwatch(keyboard.value, (newValue) => {\n emit('update:modelValue', newValue)\n})\n\n// 监听 maxLength 变化\nwatch(maxLength, (newMaxLength) => {\n keyboard.setMaxLength(newMaxLength)\n \n // 如果当前值超过新的最大长度,截断\n if (props.modelValue.length > newMaxLength) {\n const truncated = props.modelValue.slice(0, newMaxLength)\n keyboard.setValue(truncated)\n emit('update:modelValue', truncated)\n }\n})\n\n// 监听确认事件\nkeyboard.on('confirm', ({ value }) => {\n emit('done', value)\n})\n\n/**\n * 处理按键\n */\nconst handleKeyPress = (key: KeyDefinition) => {\n keyboard.handleKey(key)\n}\n\n/**\n * 当前位置提示\n */\nconst positionHint = computed(() => {\n const len = props.modelValue.length\n if (len === 0) return '请选择省份'\n if (len === 1) return '请输入字母'\n if (len < maxLength.value) return `第${len + 1}位`\n return '已完成'\n})\n</script>\n\n<template>\n <div class=\"license-plate-keyboard\">\n <!-- 输入显示区 -->\n <div class=\"license-plate-keyboard__display\">\n <div class=\"license-plate-keyboard__value\">\n <span\n v-for=\"(char, index) in modelValue.padEnd(maxLength, ' ').split('')\"\n :key=\"index\"\n class=\"license-plate-keyboard__char\"\n :class=\"{\n 'license-plate-keyboard__char--filled': char !== ' ',\n 'license-plate-keyboard__char--current': index === modelValue.length,\n 'license-plate-keyboard__char--province': index === 0,\n 'license-plate-keyboard__char--new-energy': newEnergy && index === 7\n }\"\n >\n {{ char === ' ' ? '' : char }}\n </span>\n </div>\n <div class=\"license-plate-keyboard__hint\">{{ positionHint }}</div>\n </div>\n \n <!-- 键盘区 - 由 core 的 KeyboardManager 自动切换布局 -->\n <VirtualKeyboard\n :type=\"keyboard.type.value\"\n :is-upper-case=\"keyboard.isUpperCase.value\"\n @keypress=\"handleKeyPress\"\n />\n </div>\n</template>\n\n<style>\n.license-plate-keyboard {\n display: flex;\n flex-direction: column;\n background: var(--keyboard-bg, #d1d5db);\n}\n\n.license-plate-keyboard__display {\n padding: 14px 20px;\n background: var(--keyboard-display-bg, #f5f5f5);\n border-bottom: 1px solid rgba(0, 0, 0, 0.06);\n}\n\n.license-plate-keyboard__value {\n display: flex;\n justify-content: center;\n gap: 8px;\n margin-bottom: 8px;\n}\n\n.license-plate-keyboard__char {\n width: 40px;\n height: 48px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #fff;\n border: 2px solid #e0e0e0;\n border-radius: 8px;\n font-size: 20px;\n font-weight: 600;\n color: var(--keyboard-key-color, #333);\n transition: border-color 0.2s, background-color 0.2s;\n}\n\n.license-plate-keyboard__char--filled {\n border-color: var(--keyboard-primary-bg, #007aff);\n background: rgba(0, 122, 255, 0.05);\n}\n\n.license-plate-keyboard__char--current {\n border-color: var(--keyboard-primary-bg, #007aff);\n background: rgba(0, 122, 255, 0.1);\n animation: plate-char-pulse 1s infinite;\n}\n\n.license-plate-keyboard__char--province {\n background: rgba(255, 193, 7, 0.1);\n}\n\n.license-plate-keyboard__char--province.license-plate-keyboard__char--filled {\n background: rgba(255, 193, 7, 0.15);\n border-color: #ffc107;\n}\n\n.license-plate-keyboard__char--new-energy {\n background: rgba(34, 197, 94, 0.1);\n}\n\n.license-plate-keyboard__char--new-energy.license-plate-keyboard__char--filled {\n background: rgba(34, 197, 94, 0.15);\n border-color: #22c55e;\n}\n\n@keyframes plate-char-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n}\n\n.license-plate-keyboard__hint {\n text-align: center;\n font-size: 12px;\n color: #888;\n}\n\n/* 车牌键盘专用样式 */\n.license-plate-keyboard :deep(.plate-keyboard-row) {\n gap: 4px;\n}\n\n.license-plate-keyboard :deep(.key-plate-province),\n.license-plate-keyboard :deep(.key-plate-letter),\n.license-plate-keyboard :deep(.key-plate-number) {\n min-width: 30px;\n height: 40px;\n font-size: 15px;\n border-radius: var(--keyboard-key-radius, 6px);\n}\n\n.license-plate-keyboard :deep(.key-plate-special) {\n background: rgba(255, 193, 7, 0.2);\n color: #b8860b;\n}\n\n.license-plate-keyboard :deep(.key-plate-fn) {\n background: var(--keyboard-fn-key-bg, #d4d6dc);\n}\n\n.license-plate-keyboard :deep(.key-plate-switch) {\n background: var(--keyboard-fn-key-bg, #d4d6dc);\n font-size: 13px;\n}\n\n.license-plate-keyboard :deep(.key-plate-done) {\n background: var(--keyboard-primary-bg, #007aff);\n color: #fff;\n font-size: 13px;\n}\n</style>\n","/**\n * v-keyboard 指令\n * 自动在输入框聚焦时打开虚拟键盘\n * @module directives/v-keyboard\n */\n\nimport type { Directive, DirectiveBinding } from 'vue'\n\n/**\n * 指令绑定值\n */\nexport interface VKeyboardBinding {\n /** 键盘类型 */\n type?: string\n /** 是否启用 */\n enabled?: boolean\n /** 打开键盘回调 */\n onOpen?: (el: HTMLInputElement | HTMLTextAreaElement) => void\n /** 关闭键盘回调 */\n onClose?: () => void\n}\n\nconst KEYBOARD_ATTR = '__v_keyboard__'\n\n/**\n * v-keyboard 指令\n *\n * @example\n * ```html\n * <!-- 基础用法 -->\n * <input v-keyboard />\n *\n * <!-- 指定键盘类型 -->\n * <input v-keyboard=\"{ type: 'number' }\" />\n *\n * <!-- 配合回调 -->\n * <input v-keyboard=\"{ type: 'phone', onOpen: handleOpen }\" />\n * ```\n */\nexport const vKeyboard: Directive<HTMLInputElement | HTMLTextAreaElement, VKeyboardBinding | string | boolean> = {\n mounted(el, binding) {\n const options = parseBinding(binding)\n if (!options.enabled) return\n\n const handleFocus = () => {\n // 阻止原生键盘\n el.blur()\n options.onOpen?.(el)\n }\n\n el.addEventListener('focus', handleFocus)\n el.setAttribute('inputmode', 'none')\n\n // 存储清理函数\n ;(el as any)[KEYBOARD_ATTR] = {\n cleanup: () => {\n el.removeEventListener('focus', handleFocus)\n el.removeAttribute('inputmode')\n },\n }\n },\n\n updated(el, binding) {\n const oldCleanup = (el as any)[KEYBOARD_ATTR]?.cleanup\n if (oldCleanup) oldCleanup()\n\n const options = parseBinding(binding)\n if (!options.enabled) return\n\n const handleFocus = () => {\n el.blur()\n options.onOpen?.(el)\n }\n\n el.addEventListener('focus', handleFocus)\n el.setAttribute('inputmode', 'none')\n\n ;(el as any)[KEYBOARD_ATTR] = {\n cleanup: () => {\n el.removeEventListener('focus', handleFocus)\n el.removeAttribute('inputmode')\n },\n }\n },\n\n unmounted(el) {\n const data = (el as any)[KEYBOARD_ATTR]\n if (data?.cleanup) data.cleanup()\n delete (el as any)[KEYBOARD_ATTR]\n },\n}\n\n/**\n * 解析指令绑定值\n */\nfunction parseBinding(binding: DirectiveBinding<VKeyboardBinding | string | boolean>): VKeyboardBinding & { enabled: boolean } {\n const value = binding.value\n\n if (value === false) {\n return { enabled: false }\n }\n\n if (value === true || value === undefined) {\n return { enabled: true }\n }\n\n if (typeof value === 'string') {\n return { enabled: true, type: value }\n }\n\n return {\n enabled: value.enabled !== false,\n ...value,\n }\n}\n","/**\n * 键盘 Engine 插件\n * 适配 @composy/engine-vue 的插件\n * @module plugins/engine-plugin\n */\n\nimport type { App } from 'vue'\nimport { VirtualKeyboard, KeyboardPopup, LicensePlateKeyboard } from '../components'\nimport { vKeyboard } from '../directives/v-keyboard'\n\n/**\n * Engine 插件接口(兼容 @composy/engine-core Plugin)\n */\nexport interface KeyboardEnginePluginOptions {\n /**\n * 组件前缀\n * @default 'L'\n */\n prefix?: string\n}\n\n/**\n * 键盘 Engine 插件\n *\n * 符合 @composy/engine-core Plugin 接口规范\n * 通过 engine.use(KeyboardEnginePlugin) 安装\n *\n * @example\n * ```typescript\n * import { createVueEngine } from '@composy/engine-vue'\n * import { KeyboardEnginePlugin } from '@composy/keyboard-vue'\n *\n * const engine = createVueEngine({\n * plugins: [KeyboardEnginePlugin],\n * })\n * ```\n */\nexport const KeyboardEnginePlugin = {\n name: 'keyboard',\n version: '0.1.0',\n\n async install(context: unknown, options?: KeyboardEnginePluginOptions) {\n const prefix = options?.prefix ?? 'L'\n\n // 获取 Vue 应用实例(从 engine 上下文)\n const app: App | undefined = context?.framework?.app\n\n if (app) {\n // 注册全局组件\n app.component(`${prefix}Keyboard`, VirtualKeyboard)\n app.component(`${prefix}KeyboardPopup`, KeyboardPopup)\n app.component(`${prefix}LicensePlateKeyboard`, LicensePlateKeyboard)\n\n // 注册指令\n app.directive('keyboard', vKeyboard)\n }\n\n // 注册到服务容器(如果有)\n if (context?.container?.singleton) {\n context.container.singleton('keyboard', {\n name: 'keyboard',\n version: '0.1.0',\n })\n }\n },\n}\n","/**\n * @composy/keyboard-vue\n * LDesign 虚拟键盘 - Vue 3 适配器\n * @packageDocumentation\n */\n\nimport type { App, Plugin } from 'vue'\nimport { KeyboardPopup, VirtualKeyboard, LicensePlateKeyboard } from './components'\nimport { vKeyboard } from './directives/v-keyboard'\n\n// 导出组件\nexport * from './components'\n\n// 导出 Composables\nexport * from './composables'\n\n// 导出插件\nexport * from './plugins'\n\n// 导出指令\nexport * from './directives'\n\n// 从核心包重新导出\nexport {\n // 管理器\n createKeyboardManager,\n createPositionManager,\n createKeyboardRenderer,\n KeyboardManager,\n PositionManager,\n KeyboardRenderer,\n\n // 布局\n getLayout,\n registerLayout,\n registerLayouts,\n hasLayout,\n getAvailableLayouts,\n unregisterLayout,\n qwertyLayout,\n numberLayout,\n integerLayout,\n symbolLayout,\n symbolLayout2,\n licensePlateLayout,\n licensePlateProvinceLayout,\n licensePlateLetterLayout,\n licensePlateAlphanumericLayout,\n PROVINCES,\n PLATE_LETTERS,\n // 场景化键盘\n idcardLayout,\n phoneLayout,\n phoneSimpleLayout,\n amountLayout,\n amountSimpleLayout,\n bankcardLayout,\n verifycodeLayout,\n emailLayout,\n urlLayout,\n searchLayout,\n punctLayout,\n EMAIL_SUFFIXES,\n\n // 工具函数\n EventEmitter,\n isMobile,\n isTouchDevice,\n getDeviceType,\n getViewportInfo,\n vibrate,\n onViewportChange,\n getBoundingRect,\n onClickOutside,\n insertTextAtCursor,\n deleteCharBeforeCursor,\n // 随机键盘\n shuffleArray,\n getRandomNumbers,\n getRandomLetters,\n getRandomAlphanumeric,\n createRandomNumberLayout,\n createRandomPinLayout,\n createRandomLetterLayout,\n createRandomMixedLayout,\n refreshRandomLayout,\n\n // 格式化工具\n formatPhone,\n formatBankCard,\n formatAmount,\n formatIdCard,\n formatLicensePlate,\n formatVerifyCode,\n registerFormatter,\n getFormatter,\n applyFormatter,\n\n // 主题系统\n getTheme,\n registerTheme,\n getAvailableThemes,\n getAllThemes,\n setCurrentTheme,\n getCurrentTheme,\n themeToCSS,\n applyTheme,\n generateThemeStyle,\n mergeTheme,\n createTheme,\n miuiTheme,\n iosTheme,\n materialTheme,\n minimalTheme,\n darkTheme,\n presetThemes,\n defaultTheme,\n} from '@composy/keyboard-core'\n\n// 导出类型\nexport type {\n KeyboardType,\n KeyType,\n KeyDefinition,\n KeyboardRow,\n KeyboardConfig,\n KeyboardState,\n KeyboardEventMap,\n KeyboardOptions,\n DeviceType,\n PopupPlacement,\n Position,\n Size,\n BoundingRect,\n PopupPositionConfig,\n PopupPositionResult,\n ViewportInfo,\n // 主题类型\n ThemeName,\n ThemeColors,\n ThemeSizes,\n ThemeEffects,\n ThemeConfig,\n ThemeCSSVariables,\n // 渲染器类型\n KeyboardRendererOptions,\n // 格式化类型\n FormatterFunction,\n} from '@composy/keyboard-core'\n\n/**\n * 键盘插件配置\n */\nexport interface KeyboardPluginOptions {\n /**\n * 组件前缀\n * @default 'L'\n */\n prefix?: string\n}\n\n/**\n * 原生 Vue 项目键盘插件\n * 用于全局注册键盘组件和指令\n *\n * @example\n * ```typescript\n * import { createApp } from 'vue'\n * import { KeyboardVuePlugin } from '@composy/keyboard-vue'\n *\n * const app = createApp(App)\n * app.use(KeyboardVuePlugin, { prefix: 'L' })\n * ```\n */\nexport const KeyboardVuePlugin: Plugin = {\n install(app: App, options: KeyboardPluginOptions = {}) {\n const prefix = options.prefix ?? 'L'\n\n // 注册组件\n app.component(`${prefix}Keyboard`, VirtualKeyboard)\n app.component(`${prefix}KeyboardPopup`, KeyboardPopup)\n app.component(`${prefix}LicensePlateKeyboard`, LicensePlateKeyboard)\n\n // 注册指令\n app.directive('keyboard', vKeyboard)\n },\n}\n\n/**\n * @deprecated 使用 KeyboardVuePlugin 代替\n */\nexport const KeyboardPlugin = KeyboardVuePlugin\n\n// 默认导出插件\nexport default KeyboardVuePlugin\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* style/index.less */ /*# sourceMappingURL=src/style/index.less.map */ /* styles/index.less */ /** * LDesign Keyboard 样式 * @module styles */ /* 核心样式 */ @import 'keyboard.css';/* 主题样式 */ @import 'themes.css';/* 深色主题 */ .dark,[data-theme="dark"]{--keyboard-bg:#374151;--keyboard-key-bg:#4b5563;--keyboard-key-color:#f9fafb;--keyboard-key-active-bg:#6b7280;--keyboard-fn-key-bg:#1f2937;--keyboard-fn-key-color:#f9fafb;--keyboard-fn-key-active-bg:#111827;--keyboard-primary-bg:#2563eb;--keyboard-primary-active-bg:#1d4ed8;}/* iOS 风格主题 */ .keyboard-theme-ios{--keyboard-bg:#d1d5db;--keyboard-key-bg:#ffffff;--keyboard-key-color:#000000;--keyboard-key-active-bg:#c4c7cc;--keyboard-fn-key-bg:#adb3bc;--keyboard-fn-key-color:#000000;--keyboard-fn-key-active-bg:#8e9399;--keyboard-primary-bg:#007aff;--keyboard-primary-active-bg:#0062cc;--keyboard-key-radius:5px;}/* Android 风格主题 */ .keyboard-theme-android{--keyboard-bg:#263238;--keyboard-key-bg:#37474f;--keyboard-key-color:#ffffff;--keyboard-key-active-bg:#455a64;--keyboard-fn-key-bg:#455a64;--keyboard-fn-key-color:#90a4ae;--keyboard-fn-key-active-bg:#546e7a;--keyboard-primary-bg:#4285f4;--keyboard-primary-active-bg:#3367d6;--keyboard-key-radius:4px;}/* 基础重置 */ .virtual-keyboard,.virtual-keyboard *{box-sizing:border-box;}.virtual-keyboard{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}/* 禁止选中 */ .virtual-keyboard,.keyboard-popup{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;}/* 禁止 iOS 点击高亮 */ .keyboard-key{-webkit-tap-highlight-color:transparent;}/* 移动端优化 */ @media (max-width:768px){:root{--keyboard-key-height:46px;--keyboard-font-size:18px;}.virtual-keyboard{padding:6px 2px;}.keyboard-key{margin:3px;}}/* 横屏优化 */ @media (orientation:landscape) and (max-height:500px){:root{--keyboard-key-height:36px;--keyboard-font-size:14px;}.virtual-keyboard{padding:4px 2px;}}/* 安全区域适配 */ @supports (padding-bottom:env(safe-area-inset-bottom)){.keyboard-popup--mobile{padding-bottom:calc(8px + env(safe-area-inset-bottom));}}/* ========================= 数字键盘专用样式 ========================= */ .number-keyboard-row{display:flex;justify-content:center;width:100%;}.number-keyboard-row .keyboard-key{flex:1;max-width:25%;height:52px;font-size:22px;font-weight:500;}.number-keyboard-row .key-number{background-color:var(--keyboard-key-bg,#fff);color:var(--keyboard-key-color,#333);}.number-keyboard-row .key-side{background-color:var(--keyboard-fn-key-bg,#d4d6dc);color:var(--keyboard-fn-key-color,#333);font-size:14px;}.number-keyboard-row .key-side:active{background-color:var(--keyboard-fn-key-active-bg,#b8bac0);}.number-keyboard-row .key-done{background-color:var(--keyboard-primary-bg,#007aff);color:#fff;}.number-keyboard-row .key-done:active{background-color:var(--keyboard-primary-active-bg,#0062cc);}.number-keyboard-row .key-decimal,.number-keyboard-row .key-double-zero{background-color:var(--keyboard-key-bg,#fff);font-size:18px;}.number-keyboard-row .key-placeholder{background-color:transparent;box-shadow:none;cursor:default;}.number-keyboard-row .key-placeholder:active{transform:none;background-color:transparent;}/* ========================= PC端专用样式 ========================= */ .keyboard-popup--desktop{min-width:320px;}.keyboard-popup--desktop .keyboard-key{height:40px;font-size:15px;border-radius:5px;box-shadow:0 1px 2px rgba(0,0,0,0.08);}.keyboard-popup--desktop .keyboard-key:hover{background-color:var(--keyboard-key-active-bg,#e5e7eb);}.keyboard-popup--desktop .keyboard-key--backspace:hover,.keyboard-popup--desktop .keyboard-key--shift:hover,.keyboard-popup--desktop .keyboard-key--tab:hover,.keyboard-popup--desktop .keyboard-key--clear:hover,.keyboard-popup--desktop .keyboard-key--close:hover{background-color:var(--keyboard-fn-key-active-bg,#b8bac0);}.keyboard-popup--desktop .keyboard-key--done:hover,.keyboard-popup--desktop .keyboard-key--enter:hover{background-color:var(--keyboard-primary-active-bg,#0062cc);}/* PC端数字键盘 */ .keyboard-popup--desktop .number-keyboard-row .keyboard-key{height:44px;font-size:18px;}.keyboard-popup--desktop .number-keyboard-row .key-side{font-size:13px;}/* ========================= 移动端专用样式 ========================= */ .keyboard-popup--mobile{width:100%;}.keyboard-popup--mobile .keyboard-key{height:46px;font-size:18px;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,0.1);}/* 移动端数字键盘 */ .keyboard-popup--mobile .number-keyboard-row .keyboard-key{height:54px;font-size:24px;border-radius:8px;}.keyboard-popup--mobile .number-keyboard-row .key-side{font-size:15px;}/* ========================= 键盘顶部栅栏(移动端) ========================= */ .keyboard-toolbar{display:flex;justify-content:space-between;align-items:center;padding:8px 16px;background-color:var(--keyboard-bg,#d1d5db);border-bottom:1px solid rgba(0,0,0,0.1);}.keyboard-toolbar__title{font-size:14px;color:var(--keyboard-fn-key-color,#666);}.keyboard-toolbar__done{padding:6px 16px;background:none;border:none;color:var(--keyboard-primary-bg,#007aff);font-size:15px;font-weight:500;cursor:pointer;}.keyboard-toolbar__done:active{opacity:0.7;}/*# sourceMappingURL=src/styles/index.less.map */ /* styles/keyboard.less */ /** * LDesign Keyboard - 核心样式 * 简洁现代的默认主题 */ /* ==================== CSS 变量 - 简洁主题 ==================== */:root{/* 键盘背景 */ --keyboard-bg:#f7f7f7;/* 普通按键 */ --keyboard-key-bg:#ffffff;--keyboard-key-color:#333333;--keyboard-key-active-bg:#e5e5e5;--keyboard-key-border:#e0e0e0;/* 功能键 */ --keyboard-fn-bg:#e8e8e8;--keyboard-fn-color:#666666;--keyboard-fn-active-bg:#d8d8d8;/* 主色调按键 */ --keyboard-primary-bg:#2563eb;--keyboard-primary-active-bg:#1d4ed8;--keyboard-primary-color:#ffffff;/* 按键尺寸 */ --keyboard-key-height:48px;--keyboard-key-radius:8px;--keyboard-key-gap:6px;/* 字体 */ --keyboard-font-size:20px;--keyboard-fn-font-size:14px;/* 动画 */ --keyboard-transition:80ms ease-out;}/* ==================== 基础重置 ==================== */ .virtual-keyboard,.virtual-keyboard *,.keyboard-popup,.keyboard-popup *{box-sizing:border-box;margin:0;padding:0;}.virtual-keyboard{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;}/* ==================== 键盘容器 ==================== */ .virtual-keyboard{display:flex;flex-direction:column;padding:8px;background-color:var(--keyboard-bg);width:100%;}.virtual-keyboard__row{display:flex;justify-content:center;width:100%;gap:var(--keyboard-key-gap);margin-bottom:var(--keyboard-key-gap);}.virtual-keyboard__row:last-child{margin-bottom:0;}/* ==================== 按键基础样式 ==================== */ .keyboard-key{display:inline-flex;align-items:center;justify-content:center;flex:1;height:var(--keyboard-key-height);min-width:0;padding:0 4px;border:none;border-radius:var(--keyboard-key-radius);background-color:var(--keyboard-key-bg);color:var(--keyboard-key-color);font-size:var(--keyboard-font-size);font-weight:500;cursor:pointer;touch-action:manipulation;transition:background-color var(--keyboard-transition),transform var(--keyboard-transition);-webkit-tap-highlight-color:transparent;box-shadow:0 1px 0 var(--keyboard-key-border);}.keyboard-key:active{background-color:var(--keyboard-key-active-bg);transform:scale(0.98);}.keyboard-key--disabled{opacity:0;cursor:default;pointer-events:none;}/* ==================== 功能键样式 ==================== */ .keyboard-key--backspace,.keyboard-key--shift,.keyboard-key--tab,.keyboard-key--clear,.keyboard-key--close{background-color:var(--keyboard-fn-bg);color:var(--keyboard-fn-color);font-size:var(--keyboard-fn-font-size);font-weight:400;box-shadow:0 1px 0 var(--keyboard-key-border);}.keyboard-key--backspace:active,.keyboard-key--shift:active,.keyboard-key--tab:active,.keyboard-key--clear:active,.keyboard-key--close:active{background-color:var(--keyboard-fn-active-bg);}/* Shift 激活状态 */ .keyboard-key--shift.keyboard-key--active{background-color:var(--keyboard-primary-bg);color:var(--keyboard-primary-color);}/* ==================== 主色按键(完成/确认) ==================== */ .keyboard-key--done,.keyboard-key--enter{background-color:var(--keyboard-primary-bg);color:var(--keyboard-primary-color);font-size:var(--keyboard-fn-font-size);font-weight:500;box-shadow:none;}.keyboard-key--done:active,.keyboard-key--enter:active{background-color:var(--keyboard-primary-active-bg);}/* ==================== 空格键 ==================== */ .keyboard-key--space{flex:4;}/* ==================== 占位键(透明) ==================== */ .key-placeholder{background-color:transparent !important;box-shadow:none !important;cursor:default;pointer-events:none;}/* ==================== 数字键盘通用样式 ==================== */ .numpad-row{display:flex;gap:var(--keyboard-key-gap);width:100%;}.numpad-row .keyboard-key{flex:1;height:54px;font-size:24px;font-weight:500;}/* 数字键 */ .key-num{background-color:var(--keyboard-key-bg);color:var(--keyboard-key-color);}/* 功能键 */ .key-numpad-fn{background-color:var(--keyboard-fn-bg);color:var(--keyboard-fn-color);font-size:14px;font-weight:400;}/* 完成键 */ .key-numpad-done{background-color:var(--keyboard-primary-bg);color:var(--keyboard-primary-color);font-size:16px;font-weight:500;box-shadow:none;}.key-numpad-done:active{background-color:var(--keyboard-primary-active-bg);}/* ==================== 车牌键盘样式 ==================== */ .virtual-keyboard:has(.plate-keyboard-row){min-width:340px;}.plate-keyboard-row{display:flex;gap:3px;width:100%;flex-wrap:nowrap;}.plate-keyboard-row .keyboard-key{flex:1 1 0;height:42px;min-width:28px;max-width:none;padding:0 2px;font-size:15px;font-weight:500;border-radius:5px;}.key-plate-province,.key-plate-letter,.key-plate-number{background-color:var(--keyboard-key-bg);}.key-plate-fn{background-color:var(--keyboard-fn-bg);font-size:12px;flex:1.2 1 0 !important;}.key-plate-done{background-color:var(--keyboard-primary-bg);color:var(--keyboard-primary-color);font-size:13px;box-shadow:none;flex:1.5 1 0 !important;}.key-plate-switch{background-color:var(--keyboard-fn-bg);font-size:12px;flex:1.3 1 0 !important;}.key-plate-special{background-color:#fef3c7;color:#92400e;}/* ==================== 弹出层样式 ==================== */ .keyboard-popup{position:fixed;z-index:1000;background-color:var(--keyboard-bg);overflow:hidden;}.keyboard-popup--desktop{border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,0.12),0 0 0 1px rgba(0,0,0,0.05);min-width:300px;}.keyboard-popup--mobile{position:fixed;left:0;right:0;bottom:0;border-radius:0;border-top-left-radius:12px;border-top-right-radius:12px;box-shadow:0 -2px 16px rgba(0,0,0,0.08);}/* 安全区域 */ @supports (padding-bottom:env(safe-area-inset-bottom)){.keyboard-popup--mobile{padding-bottom:env(safe-area-inset-bottom);}}/* ==================== 遮罩层 ==================== */ .keyboard-mask{position:fixed;inset:0;background-color:rgba(0,0,0,0.3);z-index:999;}/* ==================== 动画 ==================== */ .keyboard-mask-enter-active,.keyboard-mask-leave-active{transition:opacity 0.2s ease;}.keyboard-mask-enter-from,.keyboard-mask-leave-to{opacity:0;}.keyboard-fade-enter-active,.keyboard-fade-leave-active{transition:opacity 0.15s ease,transform 0.15s ease;}.keyboard-fade-enter-from,.keyboard-fade-leave-to{opacity:0;transform:translateY(-4px) scale(0.98);}.keyboard-slide-enter-active,.keyboard-slide-leave-active{transition:transform 0.25s cubic-bezier(0.32,0.72,0,1);}.keyboard-slide-enter-from,.keyboard-slide-leave-to{transform:translateY(100%);}/* ==================== 移动端适配 ==================== */ @media (max-width:768px){.virtual-keyboard{padding:6px;}.keyboard-key{height:44px;}.numpad-row .keyboard-key{height:52px;font-size:22px;}.plate-keyboard-row .keyboard-key{height:40px;font-size:15px;}}/* ==================== 横屏适配 ==================== */ @media (orientation:landscape) and (max-height:500px){.keyboard-key{height:36px;font-size:16px;}.numpad-row .keyboard-key{height:42px;font-size:18px;}}/*# sourceMappingURL=src/styles/keyboard.less.map */ /* styles/themes.less */ /** * LDesign Keyboard - 主题预设 * 简洁 · 现代 · 优雅 */ /* ==================== MIUI 主题 - 小米风格(默认) ==================== */ [data-theme="miui"],.keyboard-theme-miui{--keyboard-bg:#f5f5f5;--keyboard-key-bg:#ffffff;--keyboard-key-color:#1a1a1a;--keyboard-key-active-bg:#e8e8e8;--keyboard-fn-key-bg:#e0e0e0;--keyboard-fn-key-color:#666666;--keyboard-fn-key-active-bg:#d0d0d0;--keyboard-primary-bg:#ff6700;--keyboard-primary-active-bg:#e55d00;--keyboard-primary-color:#ffffff;--keyboard-key-radius:8px;--keyboard-key-gap:6px;--keyboard-key-height:46px;--keyboard-font-size:18px;--keyboard-fn-font-size:14px;--keyboard-padding:8px;--keyboard-key-shadow:none;}[data-theme="miui"] .keyboard-key,.keyboard-theme-miui .keyboard-key{font-weight:500;}/* ==================== iOS 主题 - 苹果风格 ==================== */ [data-theme="ios"],.keyboard-theme-ios{--keyboard-bg:#d1d3d9;--keyboard-key-bg:#ffffff;--keyboard-key-color:#000000;--keyboard-key-active-bg:#c8cad0;--keyboard-key-border:#a8aab4;--keyboard-fn-key-bg:#adb0b8;--keyboard-fn-key-color:#000000;--keyboard-fn-key-active-bg:#9ea1a9;--keyboard-primary-bg:#007aff;--keyboard-primary-active-bg:#0066d6;--keyboard-primary-color:#ffffff;--keyboard-key-radius:5px;--keyboard-key-gap:6px;--keyboard-key-height:43px;--keyboard-font-size:23px;--keyboard-fn-font-size:15px;--keyboard-padding:4px;--keyboard-key-shadow:0 1px 0 #a8aab4;}[data-theme="ios"] .keyboard-key,.keyboard-theme-ios .keyboard-key{font-weight:400;}[data-theme="ios"] .keyboard-key:active,.keyboard-theme-ios .keyboard-key:active{transform:none;}/* ==================== Material 主题 - 质感设计 ==================== */ [data-theme="material"],.keyboard-theme-material{--keyboard-bg:#263238;--keyboard-key-bg:#37474f;--keyboard-key-color:#ffffff;--keyboard-key-active-bg:#455a64;--keyboard-fn-key-bg:#455a64;--keyboard-fn-key-color:#b0bec5;--keyboard-fn-key-active-bg:#546e7a;--keyboard-primary-bg:#4caf50;--keyboard-primary-active-bg:#43a047;--keyboard-primary-color:#ffffff;--keyboard-key-radius:4px;--keyboard-key-gap:4px;--keyboard-key-height:48px;--keyboard-font-size:20px;--keyboard-fn-font-size:14px;--keyboard-padding:6px;--keyboard-key-shadow:0 1px 3px rgba(0,0,0,0.2);}[data-theme="material"] .keyboard-key,.keyboard-theme-material .keyboard-key{box-shadow:0 1px 2px rgba(0,0,0,0.12);}[data-theme="material"] .keyboard-key:active,.keyboard-theme-material .keyboard-key:active{transform:none;box-shadow:0 2px 4px rgba(0,0,0,0.2);}/* ==================== Minimal 极简主题 - 无边框 ==================== */ [data-theme="minimal"],.keyboard-theme-minimal{--keyboard-bg:rgba(250,250,250,0.95);--keyboard-key-bg:transparent;--keyboard-key-color:#333333;--keyboard-key-active-bg:rgba(0,0,0,0.08);--keyboard-fn-key-bg:transparent;--keyboard-fn-key-color:#888888;--keyboard-fn-key-active-bg:rgba(0,0,0,0.08);--keyboard-primary-bg:#333333;--keyboard-primary-active-bg:#1a1a1a;--keyboard-primary-color:#ffffff;--keyboard-key-radius:12px;--keyboard-key-gap:8px;--keyboard-key-height:44px;--keyboard-font-size:17px;--keyboard-fn-font-size:13px;--keyboard-padding:12px;--keyboard-key-shadow:none;}[data-theme="minimal"] .keyboard-key,.keyboard-theme-minimal .keyboard-key{box-shadow:none;}[data-theme="minimal"] .keyboard-key:active,.keyboard-theme-minimal .keyboard-key:active{transform:scale(0.95);}/* ==================== Dark 深色主题 ==================== */ [data-theme="dark"],.keyboard-theme-dark{--keyboard-bg:#1c1c1e;--keyboard-key-bg:#3a3a3c;--keyboard-key-color:#ffffff;--keyboard-key-active-bg:#4a4a4c;--keyboard-fn-key-bg:#2c2c2e;--keyboard-fn-key-color:#ebebf5;--keyboard-fn-key-active-bg:#3c3c3e;--keyboard-primary-bg:#0a84ff;--keyboard-primary-active-bg:#0070e0;--keyboard-primary-color:#ffffff;--keyboard-key-radius:8px;--keyboard-key-gap:6px;--keyboard-key-height:45px;--keyboard-font-size:18px;--keyboard-fn-font-size:14px;--keyboard-padding:8px;--keyboard-key-shadow:0 1px 2px rgba(0,0,0,0.4);}[data-theme="dark"] .keyboard-key,.keyboard-theme-dark .keyboard-key{box-shadow:0 1px 0 rgba(0,0,0,0.4);}/* 深色车牌键盘特殊样式 */ [data-theme="dark"] .key-plate-special,.keyboard-theme-dark .key-plate-special{background-color:#4a3728;color:#fcd34d;}/* ==================== Green 护眼主题 ==================== */ [data-theme="green"],.keyboard-theme-green{--keyboard-bg:#e8f5e9;--keyboard-key-bg:#ffffff;--keyboard-key-color:#2e7d32;--keyboard-key-active-bg:#c8e6c9;--keyboard-key-border:#a5d6a7;--keyboard-fn-bg:#c8e6c9;--keyboard-fn-color:#388e3c;--keyboard-fn-active-bg:#a5d6a7;--keyboard-primary-bg:#43a047;--keyboard-primary-active-bg:#388e3c;--keyboard-primary-color:#ffffff;--keyboard-key-radius:8px;}/* ==================== Blue 蓝色主题 ==================== */ [data-theme="blue"],.keyboard-theme-blue{--keyboard-bg:#e3f2fd;--keyboard-key-bg:#ffffff;--keyboard-key-color:#1565c0;--keyboard-key-active-bg:#bbdefb;--keyboard-key-border:#90caf9;--keyboard-fn-bg:#bbdefb;--keyboard-fn-color:#1976d2;--keyboard-fn-active-bg:#90caf9;--keyboard-primary-bg:#1976d2;--keyboard-primary-active-bg:#1565c0;--keyboard-primary-color:#ffffff;--keyboard-key-radius:8px;}/* ==================== 主题工具类 ==================== */ .keyboard-theme-light{color-scheme:light;}.keyboard-theme-dark{color-scheme:dark;}/* 平滑主题切换 */ .keyboard-popup,.virtual-keyboard{transition:background-color 0.2s ease;}.keyboard-key{transition:background-color var(--keyboard-transition) ease,transform var(--keyboard-transition) ease;}/*# sourceMappingURL=src/styles/themes.less.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?factory(exports,require("vue"),require("@composy/keyboard-core")):typeof define==="function"&&define.amd?define(["exports","vue","@composy/keyboard-core"],factory):(global=typeof globalThis!=="undefined"?globalThis:global||self,factory(global.LDesignPackagesVue3={},global.Vue,global.ComposyKeyboardCore));})(this,function(exports,vue,_composy_keyboard_core){Object.defineProperties(exports,{__esModule:{value:true},[Symbol.toStringTag]:{value:"Module"}});const _hoisted_1$2=["disabled"];const _hoisted_2$2=["innerHTML"];const _hoisted_3$2={key:1,class:"keyboard-key__label"};const _hoisted_4$2={key:2,class:"keyboard-key__arrow-inline"};var KeyboardKey_vue_vue_type_script_setup_true_lang_default=(0,vue.defineComponent)({__name:"KeyboardKey",props:{keyDef:{type:Object,required:true},isUpperCase:{type:Boolean,required:false},baseWidth:{type:Number,required:false},useFlex:{type:Boolean,required:false}},emits:["press","showOptions"],setup(__props,{emit:__emit}){const props=__props;const emit=__emit;const label=(0,vue.computed)(()=>{const keyLabel=props.keyDef.label;if(typeof keyLabel==="string")return keyLabel;return props.isUpperCase?keyLabel.upper:keyLabel.lower;});const keyWidth=(0,vue.computed)(()=>{if(props.useFlex)return;const base=props.baseWidth??36;const width=props.keyDef.width??1;return `${base*width+(width-1)*6}px`;});const keyFlex=(0,vue.computed)(()=>{if(!props.useFlex)return;return props.keyDef.width??1;});const keyClass=(0,vue.computed)(()=>{const classes=["keyboard-key",`keyboard-key--${props.keyDef.type}`];if(props.keyDef.className)classes.push(props.keyDef.className);if(props.keyDef.disabled)classes.push("keyboard-key--disabled");if(props.keyDef.type==="shift"&&props.isUpperCase)classes.push("keyboard-key--active");return classes;});const hasOptions=(0,vue.computed)(()=>{return props.keyDef.options&&props.keyDef.options.length>0;});const isOptionsOnly=(0,vue.computed)(()=>{return hasOptions.value&&!props.keyDef.value;});const handlePress=(event)=>{event.preventDefault();event.stopPropagation();if(props.keyDef.disabled)return;if(isOptionsOnly.value){emit("showOptions",props.keyDef.options);return;}emit("press",props.keyDef);};const handleArrowClick=(event)=>{event.preventDefault();event.stopPropagation();if(props.keyDef.disabled)return;if(hasOptions.value)emit("showOptions",props.keyDef.options);};return(_ctx,_cache)=>{return(0,vue.openBlock)(),(0,vue.createElementBlock)("div",{class:"keyboard-key-wrapper",style:(0,vue.normalizeStyle)({width:keyWidth.value,flex:keyFlex.value})},[(0,vue.createElementVNode)("button",{class:(0,vue.normalizeClass)([keyClass.value,{"has-options":hasOptions.value&&!isOptionsOnly.value,"is-options-only":isOptionsOnly.value}]),disabled:__props.keyDef.disabled,type:"button",onMousedown:(0,vue.withModifiers)(handlePress,["prevent"]),onTouchstart:(0,vue.withModifiers)(handlePress,["prevent"])},[(0,vue.createCommentVNode)(" SVG 图标支持 "),__props.keyDef.icon?((0,vue.openBlock)(),(0,vue.createElementBlock)("span",{key:0,class:"keyboard-key__icon",innerHTML:__props.keyDef.icon},null,8,_hoisted_2$2)):((0,vue.openBlock)(),(0,vue.createElementBlock)("span",_hoisted_3$2,(0,vue.toDisplayString)(label.value),1)),(0,vue.createCommentVNode)(" 纯选项按钮内嵌下拉箭头 "),isOptionsOnly.value?((0,vue.openBlock)(),(0,vue.createElementBlock)("span",_hoisted_4$2,"▼")):(0,vue.createCommentVNode)("v-if",true)],42,_hoisted_1$2),(0,vue.createCommentVNode)(" 有value又有options的按钮:单独可点击的下拉箭头 "),hasOptions.value&&!isOptionsOnly.value?((0,vue.openBlock)(),(0,vue.createElementBlock)("span",{key:0,class:"keyboard-key__arrow",onMousedown:(0,vue.withModifiers)(handleArrowClick,["prevent"]),onTouchstart:(0,vue.withModifiers)(handleArrowClick,["prevent"])},"▼",32)):(0,vue.createCommentVNode)("v-if",true)],4);};}});KeyboardKey_vue_vue_type_script_setup_true_lang_default.__file="src/components/KeyboardKey.vue";var KeyboardKey_default=KeyboardKey_vue_vue_type_script_setup_true_lang_default;const _hoisted_1$1={class:"virtual-keyboard"};const _hoisted_2$1={key:1,class:"virtual-keyboard__empty"};const _hoisted_3$1={key:0,class:"keyboard-options-panel"};const _hoisted_4$1={class:"keyboard-options-panel__header"};const _hoisted_5={class:"keyboard-options-panel__grid"};const _hoisted_6=["onMousedown","onTouchstart"];var VirtualKeyboard_vue_vue_type_script_setup_true_lang_default=(0,vue.defineComponent)({__name:"VirtualKeyboard",props:{type:{type:String,required:false,default:"qwerty"},isUpperCase:{type:Boolean,required:false,default:false},layout:{type:Object,required:false},keyWidth:{type:Number,required:false,default:36}},emits:["keypress","selectOption"],setup(__props,{emit:__emit}){const props=__props;const emit=__emit;const showOptionsPanel=(0,vue.ref)(false);const currentOptions=(0,vue.ref)([]);const currentLayout=(0,vue.computed)(()=>{if(props.layout)return props.layout;return(0,_composy_keyboard_core.getLayout)(props.type)??null;});const handleKeyPress=(key)=>{emit("keypress",key);};const handleShowOptions=(options)=>{currentOptions.value=options;showOptionsPanel.value=true;};const handleSelectOption=(option,event)=>{event.preventDefault();event.stopPropagation();showOptionsPanel.value=false;emit("selectOption",option);};const closeOptionsPanel=()=>{showOptionsPanel.value=false;};const getRowStyle=(rowIndex)=>{return{};};return(_ctx,_cache)=>{return(0,vue.openBlock)(),(0,vue.createElementBlock)("div",_hoisted_1$1,[currentLayout.value?((0,vue.openBlock)(true),(0,vue.createElementBlock)(vue.Fragment,{key:0},(0,vue.renderList)(currentLayout.value.rows,(row,rowIndex)=>{return(0,vue.openBlock)(),(0,vue.createElementBlock)("div",{key:rowIndex,class:(0,vue.normalizeClass)(["virtual-keyboard__row",row.className]),style:(0,vue.normalizeStyle)(getRowStyle(rowIndex))},[((0,vue.openBlock)(true),(0,vue.createElementBlock)(vue.Fragment,null,(0,vue.renderList)(row.keys,(key)=>{return(0,vue.openBlock)(),(0,vue.createBlock)(KeyboardKey_default,{key:key.id,"key-def":key,"is-upper-case":__props.isUpperCase,"base-width":__props.keyWidth,"use-flex":row.className==="numpad-row",onPress:handleKeyPress,onShowOptions:handleShowOptions},null,8,["key-def","is-upper-case","base-width","use-flex"]);}),128))],6);}),128)):((0,vue.openBlock)(),(0,vue.createElementBlock)("div",_hoisted_2$1," 键盘布局未找到 ")),(0,vue.createCommentVNode)(" 选项面板 "),(0,vue.createVNode)(vue.Transition,{name:"options-panel"},{default:(0,vue.withCtx)(()=>[showOptionsPanel.value?((0,vue.openBlock)(),(0,vue.createElementBlock)("div",_hoisted_3$1,[(0,vue.createElementVNode)("div",_hoisted_4$1,[_cache[0]||(_cache[0]=(0,vue.createElementVNode)("span",{class:"keyboard-options-panel__title"},"选择邮箱后缀",-1)),(0,vue.createElementVNode)("button",{class:"keyboard-options-panel__close",type:"button",onClick:closeOptionsPanel,onTouchstart:(0,vue.withModifiers)(closeOptionsPanel,["prevent"])}," × ",32)]),(0,vue.createElementVNode)("div",_hoisted_5,[((0,vue.openBlock)(true),(0,vue.createElementBlock)(vue.Fragment,null,(0,vue.renderList)(currentOptions.value,(option)=>{return(0,vue.openBlock)(),(0,vue.createElementBlock)("button",{key:option,class:"keyboard-options-panel__item",type:"button",onMousedown:(0,vue.withModifiers)((e)=>handleSelectOption(option,e),["prevent"]),onTouchstart:(0,vue.withModifiers)((e)=>handleSelectOption(option,e),["prevent"])},(0,vue.toDisplayString)(option),41,_hoisted_6);}),128))])])):(0,vue.createCommentVNode)("v-if",true)]),_:1})]);};}});VirtualKeyboard_vue_vue_type_script_setup_true_lang_default.__file="src/components/VirtualKeyboard.vue";var VirtualKeyboard_default=VirtualKeyboard_vue_vue_type_script_setup_true_lang_default;function useKeyboard(options={}){const{immediate=false,...keyboardOptions}=options;const manager=(0,_composy_keyboard_core.createKeyboardManager)(keyboardOptions);const visible=(0,vue.ref)(false);const type=(0,vue.ref)(keyboardOptions.type??"qwerty");const isUpperCase=(0,vue.ref)(keyboardOptions.defaultUpperCase??false);const value=(0,vue.ref)(keyboardOptions.value??"");const cursorPosition=(0,vue.ref)(keyboardOptions.value?.length??0);const layout=(0,vue.shallowRef)(manager.getLayout());const state=(0,vue.reactive)({visible:false,type:keyboardOptions.type??"qwerty",isUpperCase:keyboardOptions.defaultUpperCase??false,value:keyboardOptions.value??"",cursorPosition:keyboardOptions.value?.length??0,maxLength:keyboardOptions.maxLength,inputElement:null});const syncState=(newState)=>{visible.value=newState.visible;type.value=newState.type;isUpperCase.value=newState.isUpperCase;value.value=newState.value;cursorPosition.value=newState.cursorPosition;Object.assign(state,newState);};manager.on("stateChange",syncState);manager.on("switch",()=>{layout.value=manager.getLayout();});if(immediate)manager.show();(0,vue.onBeforeUnmount)(()=>{manager.destroy();});return{visible,type,isUpperCase,value,cursorPosition,layout,state,show:(input)=>manager.show(input),hide:()=>manager.hide(),toggle:(input)=>manager.toggle(input),switchType:(t)=>manager.switchType(t),toggleShift:()=>manager.toggleShift(),handleKey:(key)=>manager.handleKey(key),getKeyLabel:(key)=>manager.getKeyLabel(key),setValue:(v)=>manager.setValue(v),appendValue:(text)=>manager.appendValue(text),setMaxLength:(maxLength)=>manager.setMaxLength(maxLength),bindInput:(input)=>manager.bindInput(input),unbindInput:()=>manager.unbindInput(),on:(event,handler)=>manager.on(event,handler),once:(event,handler)=>manager.once(event,handler),destroy:()=>manager.destroy()};}function useKeyboardPopup(options={}){const{visible=(0,vue.ref)(false),target=(0,vue.ref)(null),popup=(0,vue.ref)(null),closeOnClickOutside=true,onClose,...positionConfig}=options;const positionManager=(0,_composy_keyboard_core.createPositionManager)(positionConfig);const popupStyles=(0,vue.ref)({});const maskStyles=(0,vue.ref)({});const placement=(0,vue.ref)("bottom");const isMobileDevice=(0,vue.ref)((0,_composy_keyboard_core.isMobile)());const showMask=(0,vue.ref)(false);const isAnimating=(0,vue.ref)(false);let cleanupClickOutside=null;let cleanupViewportChange=null;let cleanupScroll=null;const updatePosition=()=>{if(!popup.value)return;const popupRect=popup.value.getBoundingClientRect();const result=positionManager.calculate(target.value,{width:popupRect.width||300,height:popupRect.height||260});placement.value=result.placement;showMask.value=positionManager.shouldShowMask();if(result.placement==="bottom-sheet")popupStyles.value={position:"fixed",left:"0",right:"0",bottom:"0",maxHeight:`${result.maxHeight}px`,zIndex:1e3};else popupStyles.value={position:"fixed",left:`${result.position.x}px`,top:`${result.position.y}px`,maxHeight:result.maxHeight?`${result.maxHeight}px`:void 0,maxWidth:result.maxWidth?`${result.maxWidth}px`:void 0,zIndex:1e3};if(showMask.value)maskStyles.value={position:"fixed",top:"0",left:"0",right:"0",bottom:"0",backgroundColor:"rgba(0,0,0,0.5)",zIndex:999};};const close=()=>{onClose?.();};(0,vue.watch)(visible,(newVisible)=>{if(newVisible){isMobileDevice.value=(0,_composy_keyboard_core.isMobile)();requestAnimationFrame(()=>{updatePosition();if(closeOnClickOutside&&popup.value)cleanupClickOutside=(0,_composy_keyboard_core.onClickOutside)(popup.value,(event)=>{if(target.value&&target.value.contains(event.target))return;close();});});cleanupViewportChange=(0,_composy_keyboard_core.onViewportChange)(()=>{isMobileDevice.value=(0,_composy_keyboard_core.isMobile)();updatePosition();});if(!isMobileDevice.value){const handleScroll=()=>{requestAnimationFrame(()=>{updatePosition();});};window.addEventListener("scroll",handleScroll,true);cleanupScroll=()=>{window.removeEventListener("scroll",handleScroll,true);};}}else{cleanupClickOutside?.();cleanupClickOutside=null;cleanupViewportChange?.();cleanupViewportChange=null;cleanupScroll?.();cleanupScroll=null;}},{immediate:true});(0,vue.watch)(target,()=>{if(visible.value)updatePosition();});(0,vue.onBeforeUnmount)(()=>{cleanupClickOutside?.();cleanupViewportChange?.();cleanupScroll?.();});return{popupStyles,maskStyles,placement,isMobileDevice,showMask,isAnimating,updatePosition,close};}var KeyboardPopup_vue_vue_type_script_setup_true_lang_default=(0,vue.defineComponent)({__name:"KeyboardPopup",props:{modelValue:{type:Boolean,required:false,default:false},type:{type:String,required:false,default:"qwerty"},value:{type:String,required:false,default:""},target:{type:null,required:false,default:null},maxLength:{type:Number,required:false},closeOnClickOutside:{type:Boolean,required:false,default:true},enableVibrate:{type:Boolean,required:false,default:true},teleportTo:{type:null,required:false,default:"body"}},emits:["update:modelValue","update:value","confirm","keypress"],setup(__props,{emit:__emit}){const props=__props;const emit=__emit;const popupRef=(0,vue.ref)(null);const targetRef=(0,vue.computed)(()=>props.target);const isMobileDevice=(0,vue.ref)((0,_composy_keyboard_core.isMobile)());const keyboard=useKeyboard({type:props.type,value:props.value,maxLength:props.maxLength||(props.type.startsWith("license-plate")?7:void 0),enableVibrate:props.enableVibrate});const{popupStyles,maskStyles,placement,showMask,updatePosition,close:closePopup}=useKeyboardPopup({visible:(0,vue.computed)(()=>props.modelValue),target:targetRef,popup:popupRef,closeOnClickOutside:props.closeOnClickOutside,onClose:()=>{emit("update:modelValue",false);}});(0,vue.watch)(()=>props.type,(newType)=>{keyboard.switchType(newType);if(newType.startsWith("license-plate")&&!props.maxLength)keyboard.setMaxLength(7);});(0,vue.watch)(()=>props.value,(newValue)=>{if(newValue!==keyboard.value.value)keyboard.setValue(newValue);});(0,vue.watch)(keyboard.value,(newValue)=>{emit("update:value",newValue);});(0,vue.watch)(()=>props.modelValue,(visible)=>{if(visible){isMobileDevice.value=(0,_composy_keyboard_core.isMobile)();if(props.target)keyboard.bindInput(props.target);keyboard.show(props.target??void 0);(0,vue.nextTick)(()=>{updatePosition();});}else keyboard.hide();});keyboard.on("confirm",({value})=>{emit("confirm",value);emit("update:modelValue",false);});const handleKeyPress=(key)=>{keyboard.handleKey(key);emit("keypress",key);};const handleSelectOption=(option)=>{let valueToAppend=option;if(option.startsWith("@")&&keyboard.value.value.endsWith("@"))valueToAppend=option.slice(1);keyboard.appendValue(valueToAppend);};const handleMaskClick=()=>{if(props.closeOnClickOutside)emit("update:modelValue",false);};const popupClass=(0,vue.computed)(()=>{return["keyboard-popup",`keyboard-popup--${placement.value}`,{"keyboard-popup--mobile":isMobileDevice.value,"keyboard-popup--desktop":!isMobileDevice.value}];});return(_ctx,_cache)=>{return(0,vue.openBlock)(),(0,vue.createBlock)(vue.Teleport,{to:__props.teleportTo},[(0,vue.createVNode)(vue.Transition,{name:"keyboard-mask"},{default:(0,vue.withCtx)(()=>[__props.modelValue&&(0,vue.unref)(showMask)?((0,vue.openBlock)(),(0,vue.createElementBlock)("div",{key:0,class:"keyboard-mask",style:(0,vue.normalizeStyle)((0,vue.unref)(maskStyles)),onClick:handleMaskClick},null,4)):(0,vue.createCommentVNode)("v-if",true)]),_:1}),(0,vue.createVNode)(vue.Transition,{name:isMobileDevice.value?"keyboard-slide":"keyboard-fade"},{default:(0,vue.withCtx)(()=>[__props.modelValue?((0,vue.openBlock)(),(0,vue.createElementBlock)("div",{key:0,ref_key:"popupRef",ref:popupRef,class:(0,vue.normalizeClass)(popupClass.value),style:(0,vue.normalizeStyle)((0,vue.unref)(popupStyles)),onMousedown:_cache[0]||(_cache[0]=(0,vue.withModifiers)(()=>{},["prevent"])),onTouchstart:_cache[1]||(_cache[1]=(0,vue.withModifiers)(()=>{},["prevent"]))},[(0,vue.createVNode)(VirtualKeyboard_default,{type:(0,vue.unref)(keyboard).type.value,"is-upper-case":(0,vue.unref)(keyboard).isUpperCase.value,onKeypress:handleKeyPress,onSelectOption:handleSelectOption},null,8,["type","is-upper-case"])],38)):(0,vue.createCommentVNode)("v-if",true)]),_:1},8,["name"])],8,["to"]);};}});KeyboardPopup_vue_vue_type_script_setup_true_lang_default.__file="src/components/KeyboardPopup.vue";var KeyboardPopup_default=KeyboardPopup_vue_vue_type_script_setup_true_lang_default;const _hoisted_1={class:"license-plate-keyboard"};const _hoisted_2={class:"license-plate-keyboard__display"};const _hoisted_3={class:"license-plate-keyboard__value"};const _hoisted_4={class:"license-plate-keyboard__hint"};var LicensePlateKeyboard_vue_vue_type_script_setup_true_lang_default=(0,vue.defineComponent)({__name:"LicensePlateKeyboard",props:{modelValue:{type:String,required:false,default:""},newEnergy:{type:Boolean,required:false,default:false}},emits:["update:modelValue","done"],setup(__props,{emit:__emit}){const props=__props;const emit=__emit;const maxLength=(0,vue.computed)(()=>props.newEnergy?8:7);const keyboard=useKeyboard({type:"license-plate-province",value:props.modelValue,maxLength:maxLength.value,enableVibrate:true});(0,vue.watch)(()=>props.modelValue,(newValue)=>{if(newValue!==keyboard.value.value)keyboard.setValue(newValue);});(0,vue.watch)(keyboard.value,(newValue)=>{emit("update:modelValue",newValue);});(0,vue.watch)(maxLength,(newMaxLength)=>{keyboard.setMaxLength(newMaxLength);if(props.modelValue.length>newMaxLength){const truncated=props.modelValue.slice(0,newMaxLength);keyboard.setValue(truncated);emit("update:modelValue",truncated);}});keyboard.on("confirm",({value})=>{emit("done",value);});const handleKeyPress=(key)=>{keyboard.handleKey(key);};const positionHint=(0,vue.computed)(()=>{const len=props.modelValue.length;if(len===0)return "请选择省份";if(len===1)return "请输入字母";if(len<maxLength.value)return `\u7B2C${len+1}\u4F4D`;return "已完成";});return(_ctx,_cache)=>{return(0,vue.openBlock)(),(0,vue.createElementBlock)("div",_hoisted_1,[(0,vue.createCommentVNode)(" 输入显示区 "),(0,vue.createElementVNode)("div",_hoisted_2,[(0,vue.createElementVNode)("div",_hoisted_3,[((0,vue.openBlock)(true),(0,vue.createElementBlock)(vue.Fragment,null,(0,vue.renderList)(__props.modelValue.padEnd(maxLength.value," ").split(""),(char,index)=>{return(0,vue.openBlock)(),(0,vue.createElementBlock)("span",{key:index,class:(0,vue.normalizeClass)(["license-plate-keyboard__char",{"license-plate-keyboard__char--filled":char!==" ","license-plate-keyboard__char--current":index===__props.modelValue.length,"license-plate-keyboard__char--province":index===0,"license-plate-keyboard__char--new-energy":__props.newEnergy&&index===7}])},(0,vue.toDisplayString)(char===" "?"":char),3);}),128))]),(0,vue.createElementVNode)("div",_hoisted_4,(0,vue.toDisplayString)(positionHint.value),1)]),(0,vue.createCommentVNode)(" 键盘区-由 core 的 KeyboardManager 自动切换布局 "),(0,vue.createVNode)(VirtualKeyboard_default,{type:(0,vue.unref)(keyboard).type.value,"is-upper-case":(0,vue.unref)(keyboard).isUpperCase.value,onKeypress:handleKeyPress},null,8,["type","is-upper-case"])]);};}});LicensePlateKeyboard_vue_vue_type_script_setup_true_lang_default.__file="src/components/LicensePlateKeyboard.vue";var LicensePlateKeyboard_default=LicensePlateKeyboard_vue_vue_type_script_setup_true_lang_default;const KEYBOARD_ATTR="__v_keyboard__";const vKeyboard={mounted(el,binding){const options=parseBinding(binding);if(!options.enabled)return;const handleFocus=()=>{el.blur();options.onOpen?.(el);};el.addEventListener("focus",handleFocus);el.setAttribute("inputmode","none");el[KEYBOARD_ATTR]={cleanup:()=>{el.removeEventListener("focus",handleFocus);el.removeAttribute("inputmode");}};},updated(el,binding){const oldCleanup=el[KEYBOARD_ATTR]?.cleanup;if(oldCleanup)oldCleanup();const options=parseBinding(binding);if(!options.enabled)return;const handleFocus=()=>{el.blur();options.onOpen?.(el);};el.addEventListener("focus",handleFocus);el.setAttribute("inputmode","none");el[KEYBOARD_ATTR]={cleanup:()=>{el.removeEventListener("focus",handleFocus);el.removeAttribute("inputmode");}};},unmounted(el){const data=el[KEYBOARD_ATTR];if(data?.cleanup)data.cleanup();delete el[KEYBOARD_ATTR];}};function parseBinding(binding){const value=binding.value;if(value===false)return{enabled:false};if(value===true||value===void 0)return{enabled:true};if(typeof value==="string")return{enabled:true,type:value};return{enabled:value.enabled!==false,...value};}const KeyboardEnginePlugin={name:"keyboard",version:"0.1.0",async install(context,options){const prefix=options?.prefix??"L";const app=context?.framework?.app;if(app){app.component(`${prefix}Keyboard`,VirtualKeyboard_default);app.component(`${prefix}KeyboardPopup`,KeyboardPopup_default);app.component(`${prefix}LicensePlateKeyboard`,LicensePlateKeyboard_default);app.directive("keyboard",vKeyboard);}if(context?.container?.singleton)context.container.singleton("keyboard",{name:"keyboard",version:"0.1.0"});}};const KeyboardVuePlugin={install(app,options={}){const prefix=options.prefix??"L";app.component(`${prefix}Keyboard`,VirtualKeyboard_default);app.component(`${prefix}KeyboardPopup`,KeyboardPopup_default);app.component(`${prefix}LicensePlateKeyboard`,LicensePlateKeyboard_default);app.directive("keyboard",vKeyboard);}};const KeyboardPlugin=KeyboardVuePlugin;Object.defineProperty(exports,"EMAIL_SUFFIXES",{enumerable:true,get:function(){return _composy_keyboard_core.EMAIL_SUFFIXES;}});Object.defineProperty(exports,"EventEmitter",{enumerable:true,get:function(){return _composy_keyboard_core.EventEmitter;}});exports.KeyboardEnginePlugin=KeyboardEnginePlugin;exports.KeyboardKey=KeyboardKey_default;Object.defineProperty(exports,"KeyboardManager",{enumerable:true,get:function(){return _composy_keyboard_core.KeyboardManager;}});exports.KeyboardPlugin=KeyboardPlugin;exports.KeyboardPopup=KeyboardPopup_default;Object.defineProperty(exports,"KeyboardRenderer",{enumerable:true,get:function(){return _composy_keyboard_core.KeyboardRenderer;}});exports.KeyboardVuePlugin=KeyboardVuePlugin;exports.default=KeyboardVuePlugin;exports.LicensePlateKeyboard=LicensePlateKeyboard_default;Object.defineProperty(exports,"PLATE_LETTERS",{enumerable:true,get:function(){return _composy_keyboard_core.PLATE_LETTERS;}});Object.defineProperty(exports,"PROVINCES",{enumerable:true,get:function(){return _composy_keyboard_core.PROVINCES;}});Object.defineProperty(exports,"PositionManager",{enumerable:true,get:function(){return _composy_keyboard_core.PositionManager;}});exports.VirtualKeyboard=VirtualKeyboard_default;Object.defineProperty(exports,"amountLayout",{enumerable:true,get:function(){return _composy_keyboard_core.amountLayout;}});Object.defineProperty(exports,"amountSimpleLayout",{enumerable:true,get:function(){return _composy_keyboard_core.amountSimpleLayout;}});Object.defineProperty(exports,"applyFormatter",{enumerable:true,get:function(){return _composy_keyboard_core.applyFormatter;}});Object.defineProperty(exports,"applyTheme",{enumerable:true,get:function(){return _composy_keyboard_core.applyTheme;}});Object.defineProperty(exports,"bankcardLayout",{enumerable:true,get:function(){return _composy_keyboard_core.bankcardLayout;}});Object.defineProperty(exports,"createKeyboardManager",{enumerable:true,get:function(){return _composy_keyboard_core.createKeyboardManager;}});Object.defineProperty(exports,"createKeyboardRenderer",{enumerable:true,get:function(){return _composy_keyboard_core.createKeyboardRenderer;}});Object.defineProperty(exports,"createPositionManager",{enumerable:true,get:function(){return _composy_keyboard_core.createPositionManager;}});Object.defineProperty(exports,"createRandomLetterLayout",{enumerable:true,get:function(){return _composy_keyboard_core.createRandomLetterLayout;}});Object.defineProperty(exports,"createRandomMixedLayout",{enumerable:true,get:function(){return _composy_keyboard_core.createRandomMixedLayout;}});Object.defineProperty(exports,"createRandomNumberLayout",{enumerable:true,get:function(){return _composy_keyboard_core.createRandomNumberLayout;}});Object.defineProperty(exports,"createRandomPinLayout",{enumerable:true,get:function(){return _composy_keyboard_core.createRandomPinLayout;}});Object.defineProperty(exports,"createTheme",{enumerable:true,get:function(){return _composy_keyboard_core.createTheme;}});Object.defineProperty(exports,"darkTheme",{enumerable:true,get:function(){return _composy_keyboard_core.darkTheme;}});Object.defineProperty(exports,"defaultTheme",{enumerable:true,get:function(){return _composy_keyboard_core.defaultTheme;}});Object.defineProperty(exports,"deleteCharBeforeCursor",{enumerable:true,get:function(){return _composy_keyboard_core.deleteCharBeforeCursor;}});Object.defineProperty(exports,"emailLayout",{enumerable:true,get:function(){return _composy_keyboard_core.emailLayout;}});Object.defineProperty(exports,"formatAmount",{enumerable:true,get:function(){return _composy_keyboard_core.formatAmount;}});Object.defineProperty(exports,"formatBankCard",{enumerable:true,get:function(){return _composy_keyboard_core.formatBankCard;}});Object.defineProperty(exports,"formatIdCard",{enumerable:true,get:function(){return _composy_keyboard_core.formatIdCard;}});Object.defineProperty(exports,"formatLicensePlate",{enumerable:true,get:function(){return _composy_keyboard_core.formatLicensePlate;}});Object.defineProperty(exports,"formatPhone",{enumerable:true,get:function(){return _composy_keyboard_core.formatPhone;}});Object.defineProperty(exports,"formatVerifyCode",{enumerable:true,get:function(){return _composy_keyboard_core.formatVerifyCode;}});Object.defineProperty(exports,"generateThemeStyle",{enumerable:true,get:function(){return _composy_keyboard_core.generateThemeStyle;}});Object.defineProperty(exports,"getAllThemes",{enumerable:true,get:function(){return _composy_keyboard_core.getAllThemes;}});Object.defineProperty(exports,"getAvailableLayouts",{enumerable:true,get:function(){return _composy_keyboard_core.getAvailableLayouts;}});Object.defineProperty(exports,"getAvailableThemes",{enumerable:true,get:function(){return _composy_keyboard_core.getAvailableThemes;}});Object.defineProperty(exports,"getBoundingRect",{enumerable:true,get:function(){return _composy_keyboard_core.getBoundingRect;}});Object.defineProperty(exports,"getCurrentTheme",{enumerable:true,get:function(){return _composy_keyboard_core.getCurrentTheme;}});Object.defineProperty(exports,"getDeviceType",{enumerable:true,get:function(){return _composy_keyboard_core.getDeviceType;}});Object.defineProperty(exports,"getFormatter",{enumerable:true,get:function(){return _composy_keyboard_core.getFormatter;}});Object.defineProperty(exports,"getLayout",{enumerable:true,get:function(){return _composy_keyboard_core.getLayout;}});Object.defineProperty(exports,"getRandomAlphanumeric",{enumerable:true,get:function(){return _composy_keyboard_core.getRandomAlphanumeric;}});Object.defineProperty(exports,"getRandomLetters",{enumerable:true,get:function(){return _composy_keyboard_core.getRandomLetters;}});Object.defineProperty(exports,"getRandomNumbers",{enumerable:true,get:function(){return _composy_keyboard_core.getRandomNumbers;}});Object.defineProperty(exports,"getTheme",{enumerable:true,get:function(){return _composy_keyboard_core.getTheme;}});Object.defineProperty(exports,"getViewportInfo",{enumerable:true,get:function(){return _composy_keyboard_core.getViewportInfo;}});Object.defineProperty(exports,"hasLayout",{enumerable:true,get:function(){return _composy_keyboard_core.hasLayout;}});Object.defineProperty(exports,"idcardLayout",{enumerable:true,get:function(){return _composy_keyboard_core.idcardLayout;}});Object.defineProperty(exports,"insertTextAtCursor",{enumerable:true,get:function(){return _composy_keyboard_core.insertTextAtCursor;}});Object.defineProperty(exports,"integerLayout",{enumerable:true,get:function(){return _composy_keyboard_core.integerLayout;}});Object.defineProperty(exports,"iosTheme",{enumerable:true,get:function(){return _composy_keyboard_core.iosTheme;}});Object.defineProperty(exports,"isMobile",{enumerable:true,get:function(){return _composy_keyboard_core.isMobile;}});Object.defineProperty(exports,"isTouchDevice",{enumerable:true,get:function(){return _composy_keyboard_core.isTouchDevice;}});Object.defineProperty(exports,"licensePlateAlphanumericLayout",{enumerable:true,get:function(){return _composy_keyboard_core.licensePlateAlphanumericLayout;}});Object.defineProperty(exports,"licensePlateLayout",{enumerable:true,get:function(){return _composy_keyboard_core.licensePlateLayout;}});Object.defineProperty(exports,"licensePlateLetterLayout",{enumerable:true,get:function(){return _composy_keyboard_core.licensePlateLetterLayout;}});Object.defineProperty(exports,"licensePlateProvinceLayout",{enumerable:true,get:function(){return _composy_keyboard_core.licensePlateProvinceLayout;}});Object.defineProperty(exports,"materialTheme",{enumerable:true,get:function(){return _composy_keyboard_core.materialTheme;}});Object.defineProperty(exports,"mergeTheme",{enumerable:true,get:function(){return _composy_keyboard_core.mergeTheme;}});Object.defineProperty(exports,"minimalTheme",{enumerable:true,get:function(){return _composy_keyboard_core.minimalTheme;}});Object.defineProperty(exports,"miuiTheme",{enumerable:true,get:function(){return _composy_keyboard_core.miuiTheme;}});Object.defineProperty(exports,"numberLayout",{enumerable:true,get:function(){return _composy_keyboard_core.numberLayout;}});Object.defineProperty(exports,"onClickOutside",{enumerable:true,get:function(){return _composy_keyboard_core.onClickOutside;}});Object.defineProperty(exports,"onViewportChange",{enumerable:true,get:function(){return _composy_keyboard_core.onViewportChange;}});Object.defineProperty(exports,"phoneLayout",{enumerable:true,get:function(){return _composy_keyboard_core.phoneLayout;}});Object.defineProperty(exports,"phoneSimpleLayout",{enumerable:true,get:function(){return _composy_keyboard_core.phoneSimpleLayout;}});Object.defineProperty(exports,"presetThemes",{enumerable:true,get:function(){return _composy_keyboard_core.presetThemes;}});Object.defineProperty(exports,"punctLayout",{enumerable:true,get:function(){return _composy_keyboard_core.punctLayout;}});Object.defineProperty(exports,"qwertyLayout",{enumerable:true,get:function(){return _composy_keyboard_core.qwertyLayout;}});Object.defineProperty(exports,"refreshRandomLayout",{enumerable:true,get:function(){return _composy_keyboard_core.refreshRandomLayout;}});Object.defineProperty(exports,"registerFormatter",{enumerable:true,get:function(){return _composy_keyboard_core.registerFormatter;}});Object.defineProperty(exports,"registerLayout",{enumerable:true,get:function(){return _composy_keyboard_core.registerLayout;}});Object.defineProperty(exports,"registerLayouts",{enumerable:true,get:function(){return _composy_keyboard_core.registerLayouts;}});Object.defineProperty(exports,"registerTheme",{enumerable:true,get:function(){return _composy_keyboard_core.registerTheme;}});Object.defineProperty(exports,"searchLayout",{enumerable:true,get:function(){return _composy_keyboard_core.searchLayout;}});Object.defineProperty(exports,"setCurrentTheme",{enumerable:true,get:function(){return _composy_keyboard_core.setCurrentTheme;}});Object.defineProperty(exports,"shuffleArray",{enumerable:true,get:function(){return _composy_keyboard_core.shuffleArray;}});Object.defineProperty(exports,"symbolLayout",{enumerable:true,get:function(){return _composy_keyboard_core.symbolLayout;}});Object.defineProperty(exports,"symbolLayout2",{enumerable:true,get:function(){return _composy_keyboard_core.symbolLayout2;}});Object.defineProperty(exports,"themeToCSS",{enumerable:true,get:function(){return _composy_keyboard_core.themeToCSS;}});Object.defineProperty(exports,"unregisterLayout",{enumerable:true,get:function(){return _composy_keyboard_core.unregisterLayout;}});Object.defineProperty(exports,"urlLayout",{enumerable:true,get:function(){return _composy_keyboard_core.urlLayout;}});exports.useKeyboard=useKeyboard;exports.useKeyboardPopup=useKeyboardPopup;exports.vKeyboard=vKeyboard;Object.defineProperty(exports,"verifycodeLayout",{enumerable:true,get:function(){return _composy_keyboard_core.verifycodeLayout;}});Object.defineProperty(exports,"vibrate",{enumerable:true,get:function(){return _composy_keyboard_core.vibrate;}});});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/keyboard-vue v0.0.1 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-08-17 11:42:14 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
import KeyboardKey_vue_vue_type_script_setup_true_lang_default from "./KeyboardKey.vue_vue_type_script_setup_true_lang.js";
|
|
11
|
+
//#region src/components/KeyboardKey.vue
|
|
12
|
+
KeyboardKey_vue_vue_type_script_setup_true_lang_default.__file = "src/components/KeyboardKey.vue";
|
|
13
|
+
var KeyboardKey_default = KeyboardKey_vue_vue_type_script_setup_true_lang_default;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { KeyboardKey_default as default };
|
|
16
|
+
/*! End of @composy/keyboard-vue | Powered by @composy/builder */
|
|
17
|
+
//# sourceMappingURL=KeyboardKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KeyboardKey.js","names":[],"sources":["../../src/components/KeyboardKey.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { KeyDefinition } from '@composy/keyboard-core'\n\nconst props = defineProps<{\n /**\n * 按键定义\n */\n keyDef: KeyDefinition\n /**\n * 是否大写状态\n */\n isUpperCase?: boolean\n /**\n * 基础宽度(px)\n */\n baseWidth?: number\n /**\n * 是否使用 flex 布局(用于 numpad 等需要均分宽度的键盘)\n */\n useFlex?: boolean\n}>()\n\nconst emit = defineEmits<{\n press: [key: KeyDefinition]\n showOptions: [options: string[]]\n}>()\n\n/**\n * 获取显示文本\n */\nconst label = computed(() => {\n const keyLabel = props.keyDef.label\n if (typeof keyLabel === 'string') {\n return keyLabel\n }\n return props.isUpperCase ? keyLabel.upper : keyLabel.lower\n})\n\n/**\n * 计算按键宽度\n */\nconst keyWidth = computed(() => {\n // 使用 flex 布局时,不设置固定宽度\n if (props.useFlex) {\n return undefined\n }\n const base = props.baseWidth ?? 36\n const width = props.keyDef.width ?? 1\n const gap = 6\n return `${base * width + (width - 1) * gap}px`\n})\n\n/**\n * 计算按键的 flex 值(用于 numpad 布局)\n */\nconst keyFlex = computed(() => {\n if (!props.useFlex) {\n return undefined\n }\n // width 属性决定占几份\n return props.keyDef.width ?? 1\n})\n\n/**\n * 计算 CSS 类\n */\nconst keyClass = computed(() => {\n const classes = ['keyboard-key', `keyboard-key--${props.keyDef.type}`]\n\n if (props.keyDef.className) {\n classes.push(props.keyDef.className)\n }\n\n if (props.keyDef.disabled) {\n classes.push('keyboard-key--disabled')\n }\n\n // Shift 键激活状态\n if (props.keyDef.type === 'shift' && props.isUpperCase) {\n classes.push('keyboard-key--active')\n }\n\n return classes\n})\n\n/**\n * 是否有下拉选项\n */\nconst hasOptions = computed(() => {\n return props.keyDef.options && props.keyDef.options.length > 0\n})\n\n/**\n * 是否是纯选项按钮(有options但无value,点击只显示选项面板)\n */\nconst isOptionsOnly = computed(() => {\n return hasOptions.value && !props.keyDef.value\n})\n\n/**\n * 处理按键点击\n */\nconst handlePress = (event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (props.keyDef.disabled) {\n return\n }\n\n // 纯选项按钮:点击直接显示选项面板\n if (isOptionsOnly.value) {\n emit('showOptions', props.keyDef.options!)\n return\n }\n\n // 正常按键:触发按键事件\n emit('press', props.keyDef)\n}\n\n/**\n * 处理下拉箭头点击(显示选项面板)\n */\nconst handleArrowClick = (event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (props.keyDef.disabled) {\n return\n }\n\n if (hasOptions.value) {\n emit('showOptions', props.keyDef.options!)\n }\n}\n</script>\n\n<template>\n <div class=\"keyboard-key-wrapper\" :style=\"{ width: keyWidth, flex: keyFlex }\">\n <button\n :class=\"[keyClass, { 'has-options': hasOptions && !isOptionsOnly, 'is-options-only': isOptionsOnly }]\"\n :disabled=\"keyDef.disabled\"\n type=\"button\"\n @mousedown.prevent=\"handlePress\"\n @touchstart.prevent=\"handlePress\"\n >\n <!-- SVG 图标支持 -->\n <span v-if=\"keyDef.icon\" class=\"keyboard-key__icon\" v-html=\"keyDef.icon\"></span>\n <span v-else class=\"keyboard-key__label\">{{ label }}</span>\n <!-- 纯选项按钮内嵌下拉箭头 -->\n <span v-if=\"isOptionsOnly\" class=\"keyboard-key__arrow-inline\">▼</span>\n </button>\n <!-- 有value又有options的按钮:单独可点击的下拉箭头 -->\n <span\n v-if=\"hasOptions && !isOptionsOnly\"\n class=\"keyboard-key__arrow\"\n @mousedown.prevent=\"handleArrowClick\"\n @touchstart.prevent=\"handleArrowClick\"\n >▼</span>\n </div>\n</template>\n\n<style>\n.keyboard-key-wrapper {\n position: relative;\n display: inline-flex;\n}\n\n.keyboard-key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: var(--keyboard-key-height, 42px);\n min-width: 32px;\n width: 100%;\n padding: 0 6px;\n border: none;\n border-radius: var(--keyboard-key-radius, 6px);\n background-color: var(--keyboard-key-bg, #fff);\n color: var(--keyboard-key-color, #333);\n font-size: var(--keyboard-font-size, 16px);\n font-weight: 500;\n cursor: pointer;\n user-select: none;\n touch-action: manipulation;\n box-shadow: var(--keyboard-key-shadow, 0 1px 3px rgba(0, 0, 0, 0.1));\n transition: background-color 0.1s, transform 0.1s;\n}\n\n.keyboard-key.has-options {\n padding-right: 16px;\n}\n\n.keyboard-key:active {\n background-color: var(--keyboard-key-active-bg, #e0e0e0);\n transform: scale(0.95);\n}\n\n.keyboard-key--disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.keyboard-key--disabled:active {\n transform: none;\n background-color: var(--keyboard-key-bg, #fff);\n}\n\n/* 功能键样式 */\n.keyboard-key--backspace,\n.keyboard-key--shift,\n.keyboard-key--tab,\n.keyboard-key--clear,\n.keyboard-key--close {\n background-color: var(--keyboard-fn-key-bg, #d4d6dc);\n color: var(--keyboard-fn-key-color, #333);\n font-size: var(--keyboard-fn-font-size, 14px);\n}\n\n.keyboard-key--backspace:active,\n.keyboard-key--shift:active,\n.keyboard-key--tab:active,\n.keyboard-key--clear:active,\n.keyboard-key--close:active {\n background-color: var(--keyboard-fn-key-active-bg, #b8bac0);\n}\n\n/* Shift 激活状态 */\n.keyboard-key--active {\n background-color: var(--keyboard-key-active-bg, #007aff) !important;\n color: #fff !important;\n}\n\n/* 完成/确认键 */\n.keyboard-key--done,\n.keyboard-key--enter {\n background-color: var(--keyboard-primary-bg, #007aff);\n color: var(--keyboard-primary-color, #fff);\n font-size: var(--keyboard-fn-font-size, 14px);\n font-weight: 600;\n}\n\n.keyboard-key--done:active,\n.keyboard-key--enter:active {\n background-color: var(--keyboard-primary-active-bg, #0062cc);\n}\n\n/* 空格键 */\n.keyboard-key--space {\n background-color: var(--keyboard-key-bg, #fff);\n}\n\n.keyboard-key__icon {\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.keyboard-key__icon svg {\n width: 1em;\n height: 1em;\n}\n\n.keyboard-key__label {\n pointer-events: none;\n line-height: 1;\n}\n\n.keyboard-key__arrow {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 8px;\n opacity: 0.5;\n cursor: pointer;\n user-select: none;\n touch-action: manipulation;\n border-radius: 0 6px 6px 0;\n}\n\n.keyboard-key__arrow:active {\n background-color: rgba(0, 0, 0, 0.1);\n opacity: 1;\n}\n\n/* 纯选项按钮内嵌箭头 */\n.keyboard-key__arrow-inline {\n margin-left: 4px;\n font-size: 8px;\n opacity: 0.6;\n pointer-events: none;\n}\n\n/* 纯选项按钮样式 */\n.keyboard-key.is-options-only {\n font-size: 13px;\n background-color: var(--keyboard-fn-key-bg, #d4d6dc);\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/keyboard-vue v0.0.1 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-08-17 11:42:14 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
import { computed, createCommentVNode, createElementBlock, createElementVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, toDisplayString, withModifiers } from "vue";
|
|
11
|
+
//#region src/components/KeyboardKey.vue?vue&type=script&setup=true&lang.ts
|
|
12
|
+
const _hoisted_1 = ["disabled"];
|
|
13
|
+
const _hoisted_2 = ["innerHTML"];
|
|
14
|
+
const _hoisted_3 = {
|
|
15
|
+
key: 1,
|
|
16
|
+
class: "keyboard-key__label"
|
|
17
|
+
};
|
|
18
|
+
const _hoisted_4 = {
|
|
19
|
+
key: 2,
|
|
20
|
+
class: "keyboard-key__arrow-inline"
|
|
21
|
+
};
|
|
22
|
+
var KeyboardKey_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
|
|
23
|
+
__name: "KeyboardKey",
|
|
24
|
+
props: {
|
|
25
|
+
keyDef: {
|
|
26
|
+
type: Object,
|
|
27
|
+
required: true
|
|
28
|
+
},
|
|
29
|
+
isUpperCase: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
required: false
|
|
32
|
+
},
|
|
33
|
+
baseWidth: {
|
|
34
|
+
type: Number,
|
|
35
|
+
required: false
|
|
36
|
+
},
|
|
37
|
+
useFlex: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
required: false
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
emits: ["press", "showOptions"],
|
|
43
|
+
setup(__props, { emit: __emit }) {
|
|
44
|
+
const props = __props;
|
|
45
|
+
const emit = __emit;
|
|
46
|
+
const label = computed(() => {
|
|
47
|
+
const keyLabel = props.keyDef.label;
|
|
48
|
+
if (typeof keyLabel === "string") return keyLabel;
|
|
49
|
+
return props.isUpperCase ? keyLabel.upper : keyLabel.lower;
|
|
50
|
+
});
|
|
51
|
+
const keyWidth = computed(() => {
|
|
52
|
+
if (props.useFlex) return;
|
|
53
|
+
const base = props.baseWidth ?? 36;
|
|
54
|
+
const width = props.keyDef.width ?? 1;
|
|
55
|
+
return `${base * width + (width - 1) * 6}px`;
|
|
56
|
+
});
|
|
57
|
+
const keyFlex = computed(() => {
|
|
58
|
+
if (!props.useFlex) return;
|
|
59
|
+
return props.keyDef.width ?? 1;
|
|
60
|
+
});
|
|
61
|
+
const keyClass = computed(() => {
|
|
62
|
+
const classes = ["keyboard-key", `keyboard-key--${props.keyDef.type}`];
|
|
63
|
+
if (props.keyDef.className) classes.push(props.keyDef.className);
|
|
64
|
+
if (props.keyDef.disabled) classes.push("keyboard-key--disabled");
|
|
65
|
+
if (props.keyDef.type === "shift" && props.isUpperCase) classes.push("keyboard-key--active");
|
|
66
|
+
return classes;
|
|
67
|
+
});
|
|
68
|
+
const hasOptions = computed(() => {
|
|
69
|
+
return props.keyDef.options && props.keyDef.options.length > 0;
|
|
70
|
+
});
|
|
71
|
+
const isOptionsOnly = computed(() => {
|
|
72
|
+
return hasOptions.value && !props.keyDef.value;
|
|
73
|
+
});
|
|
74
|
+
const handlePress = (event) => {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
event.stopPropagation();
|
|
77
|
+
if (props.keyDef.disabled) return;
|
|
78
|
+
if (isOptionsOnly.value) {
|
|
79
|
+
emit("showOptions", props.keyDef.options);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
emit("press", props.keyDef);
|
|
83
|
+
};
|
|
84
|
+
const handleArrowClick = (event) => {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
event.stopPropagation();
|
|
87
|
+
if (props.keyDef.disabled) return;
|
|
88
|
+
if (hasOptions.value) emit("showOptions", props.keyDef.options);
|
|
89
|
+
};
|
|
90
|
+
return (_ctx, _cache) => {
|
|
91
|
+
return openBlock(), createElementBlock("div", {
|
|
92
|
+
class: "keyboard-key-wrapper",
|
|
93
|
+
style: normalizeStyle({
|
|
94
|
+
width: keyWidth.value,
|
|
95
|
+
flex: keyFlex.value
|
|
96
|
+
})
|
|
97
|
+
}, [
|
|
98
|
+
createElementVNode("button", {
|
|
99
|
+
class: normalizeClass([keyClass.value, {
|
|
100
|
+
"has-options": hasOptions.value && !isOptionsOnly.value,
|
|
101
|
+
"is-options-only": isOptionsOnly.value
|
|
102
|
+
}]),
|
|
103
|
+
disabled: __props.keyDef.disabled,
|
|
104
|
+
type: "button",
|
|
105
|
+
onMousedown: withModifiers(handlePress, ["prevent"]),
|
|
106
|
+
onTouchstart: withModifiers(handlePress, ["prevent"])
|
|
107
|
+
}, [
|
|
108
|
+
createCommentVNode(" SVG 图标支持 "),
|
|
109
|
+
__props.keyDef.icon ? (openBlock(), createElementBlock("span", {
|
|
110
|
+
key: 0,
|
|
111
|
+
class: "keyboard-key__icon",
|
|
112
|
+
innerHTML: __props.keyDef.icon
|
|
113
|
+
}, null, 8, _hoisted_2)) : (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(label.value), 1)),
|
|
114
|
+
createCommentVNode(" 纯选项按钮内嵌下拉箭头 "),
|
|
115
|
+
isOptionsOnly.value ? (openBlock(), createElementBlock("span", _hoisted_4, "▼")) : createCommentVNode("v-if", true)
|
|
116
|
+
], 42, _hoisted_1),
|
|
117
|
+
createCommentVNode(" 有value又有options的按钮:单独可点击的下拉箭头 "),
|
|
118
|
+
hasOptions.value && !isOptionsOnly.value ? (openBlock(), createElementBlock("span", {
|
|
119
|
+
key: 0,
|
|
120
|
+
class: "keyboard-key__arrow",
|
|
121
|
+
onMousedown: withModifiers(handleArrowClick, ["prevent"]),
|
|
122
|
+
onTouchstart: withModifiers(handleArrowClick, ["prevent"])
|
|
123
|
+
}, "▼", 32)) : createCommentVNode("v-if", true)
|
|
124
|
+
], 4);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
//#endregion
|
|
129
|
+
export { KeyboardKey_vue_vue_type_script_setup_true_lang_default as default };
|
|
130
|
+
/*! End of @composy/keyboard-vue | Powered by @composy/builder */
|
|
131
|
+
//# sourceMappingURL=KeyboardKey.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KeyboardKey.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/KeyboardKey.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { KeyDefinition } from '@composy/keyboard-core'\n\nconst props = defineProps<{\n /**\n * 按键定义\n */\n keyDef: KeyDefinition\n /**\n * 是否大写状态\n */\n isUpperCase?: boolean\n /**\n * 基础宽度(px)\n */\n baseWidth?: number\n /**\n * 是否使用 flex 布局(用于 numpad 等需要均分宽度的键盘)\n */\n useFlex?: boolean\n}>()\n\nconst emit = defineEmits<{\n press: [key: KeyDefinition]\n showOptions: [options: string[]]\n}>()\n\n/**\n * 获取显示文本\n */\nconst label = computed(() => {\n const keyLabel = props.keyDef.label\n if (typeof keyLabel === 'string') {\n return keyLabel\n }\n return props.isUpperCase ? keyLabel.upper : keyLabel.lower\n})\n\n/**\n * 计算按键宽度\n */\nconst keyWidth = computed(() => {\n // 使用 flex 布局时,不设置固定宽度\n if (props.useFlex) {\n return undefined\n }\n const base = props.baseWidth ?? 36\n const width = props.keyDef.width ?? 1\n const gap = 6\n return `${base * width + (width - 1) * gap}px`\n})\n\n/**\n * 计算按键的 flex 值(用于 numpad 布局)\n */\nconst keyFlex = computed(() => {\n if (!props.useFlex) {\n return undefined\n }\n // width 属性决定占几份\n return props.keyDef.width ?? 1\n})\n\n/**\n * 计算 CSS 类\n */\nconst keyClass = computed(() => {\n const classes = ['keyboard-key', `keyboard-key--${props.keyDef.type}`]\n\n if (props.keyDef.className) {\n classes.push(props.keyDef.className)\n }\n\n if (props.keyDef.disabled) {\n classes.push('keyboard-key--disabled')\n }\n\n // Shift 键激活状态\n if (props.keyDef.type === 'shift' && props.isUpperCase) {\n classes.push('keyboard-key--active')\n }\n\n return classes\n})\n\n/**\n * 是否有下拉选项\n */\nconst hasOptions = computed(() => {\n return props.keyDef.options && props.keyDef.options.length > 0\n})\n\n/**\n * 是否是纯选项按钮(有options但无value,点击只显示选项面板)\n */\nconst isOptionsOnly = computed(() => {\n return hasOptions.value && !props.keyDef.value\n})\n\n/**\n * 处理按键点击\n */\nconst handlePress = (event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (props.keyDef.disabled) {\n return\n }\n\n // 纯选项按钮:点击直接显示选项面板\n if (isOptionsOnly.value) {\n emit('showOptions', props.keyDef.options!)\n return\n }\n\n // 正常按键:触发按键事件\n emit('press', props.keyDef)\n}\n\n/**\n * 处理下拉箭头点击(显示选项面板)\n */\nconst handleArrowClick = (event: MouseEvent | TouchEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (props.keyDef.disabled) {\n return\n }\n\n if (hasOptions.value) {\n emit('showOptions', props.keyDef.options!)\n }\n}\n</script>\n\n<template>\n <div class=\"keyboard-key-wrapper\" :style=\"{ width: keyWidth, flex: keyFlex }\">\n <button\n :class=\"[keyClass, { 'has-options': hasOptions && !isOptionsOnly, 'is-options-only': isOptionsOnly }]\"\n :disabled=\"keyDef.disabled\"\n type=\"button\"\n @mousedown.prevent=\"handlePress\"\n @touchstart.prevent=\"handlePress\"\n >\n <!-- SVG 图标支持 -->\n <span v-if=\"keyDef.icon\" class=\"keyboard-key__icon\" v-html=\"keyDef.icon\"></span>\n <span v-else class=\"keyboard-key__label\">{{ label }}</span>\n <!-- 纯选项按钮内嵌下拉箭头 -->\n <span v-if=\"isOptionsOnly\" class=\"keyboard-key__arrow-inline\">▼</span>\n </button>\n <!-- 有value又有options的按钮:单独可点击的下拉箭头 -->\n <span\n v-if=\"hasOptions && !isOptionsOnly\"\n class=\"keyboard-key__arrow\"\n @mousedown.prevent=\"handleArrowClick\"\n @touchstart.prevent=\"handleArrowClick\"\n >▼</span>\n </div>\n</template>\n\n<style>\n.keyboard-key-wrapper {\n position: relative;\n display: inline-flex;\n}\n\n.keyboard-key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: var(--keyboard-key-height, 42px);\n min-width: 32px;\n width: 100%;\n padding: 0 6px;\n border: none;\n border-radius: var(--keyboard-key-radius, 6px);\n background-color: var(--keyboard-key-bg, #fff);\n color: var(--keyboard-key-color, #333);\n font-size: var(--keyboard-font-size, 16px);\n font-weight: 500;\n cursor: pointer;\n user-select: none;\n touch-action: manipulation;\n box-shadow: var(--keyboard-key-shadow, 0 1px 3px rgba(0, 0, 0, 0.1));\n transition: background-color 0.1s, transform 0.1s;\n}\n\n.keyboard-key.has-options {\n padding-right: 16px;\n}\n\n.keyboard-key:active {\n background-color: var(--keyboard-key-active-bg, #e0e0e0);\n transform: scale(0.95);\n}\n\n.keyboard-key--disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.keyboard-key--disabled:active {\n transform: none;\n background-color: var(--keyboard-key-bg, #fff);\n}\n\n/* 功能键样式 */\n.keyboard-key--backspace,\n.keyboard-key--shift,\n.keyboard-key--tab,\n.keyboard-key--clear,\n.keyboard-key--close {\n background-color: var(--keyboard-fn-key-bg, #d4d6dc);\n color: var(--keyboard-fn-key-color, #333);\n font-size: var(--keyboard-fn-font-size, 14px);\n}\n\n.keyboard-key--backspace:active,\n.keyboard-key--shift:active,\n.keyboard-key--tab:active,\n.keyboard-key--clear:active,\n.keyboard-key--close:active {\n background-color: var(--keyboard-fn-key-active-bg, #b8bac0);\n}\n\n/* Shift 激活状态 */\n.keyboard-key--active {\n background-color: var(--keyboard-key-active-bg, #007aff) !important;\n color: #fff !important;\n}\n\n/* 完成/确认键 */\n.keyboard-key--done,\n.keyboard-key--enter {\n background-color: var(--keyboard-primary-bg, #007aff);\n color: var(--keyboard-primary-color, #fff);\n font-size: var(--keyboard-fn-font-size, 14px);\n font-weight: 600;\n}\n\n.keyboard-key--done:active,\n.keyboard-key--enter:active {\n background-color: var(--keyboard-primary-active-bg, #0062cc);\n}\n\n/* 空格键 */\n.keyboard-key--space {\n background-color: var(--keyboard-key-bg, #fff);\n}\n\n.keyboard-key__icon {\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.keyboard-key__icon svg {\n width: 1em;\n height: 1em;\n}\n\n.keyboard-key__label {\n pointer-events: none;\n line-height: 1;\n}\n\n.keyboard-key__arrow {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 8px;\n opacity: 0.5;\n cursor: pointer;\n user-select: none;\n touch-action: manipulation;\n border-radius: 0 6px 6px 0;\n}\n\n.keyboard-key__arrow:active {\n background-color: rgba(0, 0, 0, 0.1);\n opacity: 1;\n}\n\n/* 纯选项按钮内嵌箭头 */\n.keyboard-key__arrow-inline {\n margin-left: 4px;\n font-size: 8px;\n opacity: 0.6;\n pointer-events: none;\n}\n\n/* 纯选项按钮样式 */\n.keyboard-key.is-options-only {\n font-size: 13px;\n background-color: var(--keyboard-fn-key-bg, #d4d6dc);\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAIA,MAAM,QAAQ;EAmBd,MAAM,OAAO;EAQb,MAAM,QAAQ,eAAe;GAC3B,MAAM,WAAW,MAAM,OAAO;GAC9B,IAAI,OAAO,aAAa,UACtB,OAAO;GAET,OAAO,MAAM,cAAc,SAAS,QAAQ,SAAS;EACvD,CAAC;EAKD,MAAM,WAAW,eAAe;GAE9B,IAAI,MAAM,SACR;GAEF,MAAM,OAAO,MAAM,aAAa;GAChC,MAAM,QAAQ,MAAM,OAAO,SAAS;GAEpC,OAAO,GAAG,OAAO,SAAS,QAAQ,KAAK,EAAG;EAC5C,CAAC;EAKD,MAAM,UAAU,eAAe;GAC7B,IAAI,CAAC,MAAM,SACT;GAGF,OAAO,MAAM,OAAO,SAAS;EAC/B,CAAC;EAKD,MAAM,WAAW,eAAe;GAC9B,MAAM,UAAU,CAAC,gBAAgB,iBAAiB,MAAM,OAAO,MAAM;GAErE,IAAI,MAAM,OAAO,WACf,QAAQ,KAAK,MAAM,OAAO,SAAS;GAGrC,IAAI,MAAM,OAAO,UACf,QAAQ,KAAK,wBAAwB;GAIvC,IAAI,MAAM,OAAO,SAAS,WAAW,MAAM,aACzC,QAAQ,KAAK,sBAAsB;GAGrC,OAAO;EACT,CAAC;EAKD,MAAM,aAAa,eAAe;GAChC,OAAO,MAAM,OAAO,WAAW,MAAM,OAAO,QAAQ,SAAS;EAC/D,CAAC;EAKD,MAAM,gBAAgB,eAAe;GACnC,OAAO,WAAW,SAAS,CAAC,MAAM,OAAO;EAC3C,CAAC;EAKD,MAAM,eAAe,UAAmC;GACtD,MAAM,eAAe;GACrB,MAAM,gBAAgB;GAEtB,IAAI,MAAM,OAAO,UACf;GAIF,IAAI,cAAc,OAAO;IACvB,KAAK,eAAe,MAAM,OAAO,OAAQ;IACzC;GACF;GAGA,KAAK,SAAS,MAAM,MAAM;EAC5B;EAKA,MAAM,oBAAoB,UAAmC;GAC3D,MAAM,eAAe;GACrB,MAAM,gBAAgB;GAEtB,IAAI,MAAM,OAAO,UACf;GAGF,IAAI,WAAW,OACb,KAAK,eAAe,MAAM,OAAO,OAAQ;EAE7C;;uBAIE,mBAqBM,OAAA;IArBD,OAAM;IAAwB,OAAK,eAAA;KAAA,OAAW,SAAA;KAAQ,MAAQ,QAAA;IAAO,CAAA;;IACxE,mBAYS,UAAA;KAXN,OAAK,eAAA,CAAG,SAAA,OAAQ;MAAA,eAAmB,WAAA,SAAU,CAAK,cAAA;MAAa,mBAAqB,cAAA;KAAa,CAAA,CAAA;KACjG,UAAU,QAAA,OAAO;KAClB,MAAK;KACJ,aAAS,cAAU,aAAW,CAAA,SAAA,CAAA;KAC9B,cAAU,cAAU,aAAW,CAAA,SAAA,CAAA;;KAEhC,mBAAA,YAAA;KACY,QAAA,OAAO,QAAA,UAAA,GAAnB,mBAAgF,QAAA;;MAAvD,OAAM;MAAqB,WAAQ,QAAA,OAAO;8CACnE,mBAA2D,QAA3D,YAA2D,gBAAf,MAAA,KAAK,GAAA,CAAA;KACjD,mBAAA,eAAA;KACY,cAAA,SAAA,UAAA,GAAZ,mBAAsE,QAAtE,YAA8D,GAAC,KAAA,mBAAA,QAAA,IAAA;;IAEjE,mBAAA,iCAAA;IAEQ,WAAA,SAAU,CAAK,cAAA,SAAA,UAAA,GADvB,mBAKS,QAAA;;KAHP,OAAM;KACL,aAAS,cAAU,kBAAgB,CAAA,SAAA,CAAA;KACnC,cAAU,cAAU,kBAAgB,CAAA,SAAA,CAAA;OACtC,KAAC,EAAA,KAAA,mBAAA,QAAA,IAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/keyboard-vue v0.0.1 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-08-17 11:42:14 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
import KeyboardPopup_vue_vue_type_script_setup_true_lang_default from "./KeyboardPopup.vue_vue_type_script_setup_true_lang.js";
|
|
11
|
+
//#region src/components/KeyboardPopup.vue
|
|
12
|
+
KeyboardPopup_vue_vue_type_script_setup_true_lang_default.__file = "src/components/KeyboardPopup.vue";
|
|
13
|
+
var KeyboardPopup_default = KeyboardPopup_vue_vue_type_script_setup_true_lang_default;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { KeyboardPopup_default as default };
|
|
16
|
+
/*! End of @composy/keyboard-vue | Powered by @composy/builder */
|
|
17
|
+
//# sourceMappingURL=KeyboardPopup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KeyboardPopup.js","names":[],"sources":["../../src/components/KeyboardPopup.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watch, Teleport, Transition, onMounted, onBeforeUnmount, nextTick } from 'vue'\nimport type { KeyboardType, KeyDefinition, KeyboardOptions } from '@composy/keyboard-core'\nimport { isMobile } from '@composy/keyboard-core'\nimport { useKeyboard } from '../composables/useKeyboard'\nimport { useKeyboardPopup } from '../composables/useKeyboardPopup'\nimport VirtualKeyboard from './VirtualKeyboard.vue'\n\nconst props = withDefaults(defineProps<{\n /**\n * 是否显示 (v-model)\n */\n modelValue?: boolean\n /**\n * 键盘类型\n */\n type?: KeyboardType | string\n /**\n * 绑定的输入值 (v-model:value)\n */\n value?: string\n /**\n * 目标输入框元素\n */\n target?: HTMLInputElement | HTMLTextAreaElement | null\n /**\n * 最大输入长度\n */\n maxLength?: number\n /**\n * 点击外部是否关闭\n */\n closeOnClickOutside?: boolean\n /**\n * 是否启用震动反馈\n */\n enableVibrate?: boolean\n /**\n * 挂载目标\n */\n teleportTo?: string | HTMLElement\n}>(), {\n modelValue: false,\n type: 'qwerty',\n value: '',\n target: null,\n closeOnClickOutside: true,\n enableVibrate: true,\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [visible: boolean]\n 'update:value': [value: string]\n 'confirm': [value: string]\n 'keypress': [key: KeyDefinition]\n}>()\n\n// 弹出层 DOM 引用\nconst popupRef = ref<HTMLElement | null>(null)\n\n// 目标元素引用\nconst targetRef = computed(() => props.target as HTMLElement | null)\n\n// 是否移动端\nconst isMobileDevice = ref(isMobile())\n\n// 创建键盘管理器\nconst keyboard = useKeyboard({\n type: props.type as KeyboardType,\n value: props.value,\n maxLength: props.maxLength || (props.type.startsWith('license-plate') ? 7 : undefined),\n enableVibrate: props.enableVibrate,\n})\n\n// 创建弹出层管理\nconst {\n popupStyles,\n maskStyles,\n placement,\n showMask,\n updatePosition,\n close: closePopup,\n} = useKeyboardPopup({\n visible: computed(() => props.modelValue),\n target: targetRef,\n popup: popupRef,\n closeOnClickOutside: props.closeOnClickOutside,\n onClose: () => {\n emit('update:modelValue', false)\n },\n})\n\n// 监听键盘类型变化\nwatch(() => props.type, (newType) => {\n keyboard.switchType(newType as KeyboardType)\n \n // 车牌键盘需要设置默认 maxLength\n if (newType.startsWith('license-plate') && !props.maxLength) {\n keyboard.setMaxLength(7)\n }\n})\n\n// 监听外部 value 变化\nwatch(() => props.value, (newValue) => {\n if (newValue !== keyboard.value.value) {\n keyboard.setValue(newValue)\n }\n})\n\n// 监听键盘内部 value 变化\nwatch(keyboard.value, (newValue) => {\n emit('update:value', newValue)\n})\n\n// 监听可见性变化\nwatch(() => props.modelValue, (visible) => {\n if (visible) {\n isMobileDevice.value = isMobile()\n if (props.target) {\n keyboard.bindInput(props.target)\n }\n keyboard.show(props.target ?? undefined)\n \n nextTick(() => {\n updatePosition()\n })\n } else {\n keyboard.hide()\n }\n})\n\n// 监听确认事件\nkeyboard.on('confirm', ({ value }) => {\n emit('confirm', value)\n emit('update:modelValue', false)\n})\n\n/**\n * 处理按键\n */\nconst handleKeyPress = (key: KeyDefinition) => {\n keyboard.handleKey(key)\n emit('keypress', key)\n}\n\n/**\n * 处理下拉选项选择(如邮箱后缀)\n */\nconst handleSelectOption = (option: string) => {\n let valueToAppend = option\n \n // 智能处理邮箱后缀:如果当前值已以 @ 结尾,而选项以 @ 开头,则去掉选项的 @\n if (option.startsWith('@') && keyboard.value.value.endsWith('@')) {\n valueToAppend = option.slice(1) // 去掉开头的 @\n }\n \n keyboard.appendValue(valueToAppend)\n}\n\n/**\n * 处理遮罩点击\n */\nconst handleMaskClick = () => {\n if (props.closeOnClickOutside) {\n emit('update:modelValue', false)\n }\n}\n\n/**\n * 计算弹出层类名\n */\nconst popupClass = computed(() => {\n return [\n 'keyboard-popup',\n `keyboard-popup--${placement.value}`,\n {\n 'keyboard-popup--mobile': isMobileDevice.value,\n 'keyboard-popup--desktop': !isMobileDevice.value,\n },\n ]\n})\n</script>\n\n<template>\n <Teleport :to=\"teleportTo\">\n <Transition name=\"keyboard-mask\">\n <div\n v-if=\"modelValue && showMask\"\n class=\"keyboard-mask\"\n :style=\"maskStyles\"\n @click=\"handleMaskClick\"\n />\n </Transition>\n\n <Transition :name=\"isMobileDevice ? 'keyboard-slide' : 'keyboard-fade'\">\n <div\n v-if=\"modelValue\"\n ref=\"popupRef\"\n :class=\"popupClass\"\n :style=\"popupStyles\"\n @mousedown.prevent\n @touchstart.prevent\n >\n <VirtualKeyboard\n :type=\"keyboard.type.value\"\n :is-upper-case=\"keyboard.isUpperCase.value\"\n @keypress=\"handleKeyPress\"\n @select-option=\"handleSelectOption\"\n />\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style>\n.keyboard-popup {\n background-color: var(--keyboard-bg, #d1d5db);\n box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n}\n\n.keyboard-popup--desktop {\n border-radius: 12px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.1);\n min-width: 320px;\n}\n\n.keyboard-popup--mobile {\n border-radius: 0;\n padding-bottom: env(safe-area-inset-bottom, 0);\n}\n\n.keyboard-popup--bottom-sheet {\n border-top-left-radius: 16px;\n border-top-right-radius: 16px;\n}\n\n.keyboard-mask {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.5);\n z-index: 999;\n}\n\n/* 遮罩动画 */\n.keyboard-mask-enter-active,\n.keyboard-mask-leave-active {\n transition: opacity 0.3s ease;\n}\n\n.keyboard-mask-enter-from,\n.keyboard-mask-leave-to {\n opacity: 0;\n}\n\n/* PC 端淡入淡出动画 */\n.keyboard-fade-enter-active,\n.keyboard-fade-leave-active {\n transition: opacity 0.2s ease, transform 0.2s ease;\n}\n\n.keyboard-fade-enter-from,\n.keyboard-fade-leave-to {\n opacity: 0;\n transform: translateY(-10px);\n}\n\n/* 移动端滑入滑出动画 */\n.keyboard-slide-enter-active,\n.keyboard-slide-leave-active {\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.keyboard-slide-enter-from,\n.keyboard-slide-leave-to {\n transform: translateY(100%);\n}\n</style>\n"],"mappings":""}
|