完善模拟按键组件

This commit is contained in:
fofolee 2024-12-28 12:24:52 +08:00
parent e8d12b95d4
commit 1dafacf3f1

View File

@ -1,317 +1,341 @@
<template> <template>
<div class="key-editor"> <div class="key-editor">
<!-- 按键输入框 --> <div class="row items-center q-gutter-x-sm full-width">
<q-input <!-- 按键选择/输入区域 -->
dense <q-select
outlined v-model="mainKey"
readonly :options="commonKeys"
class="col" dense
> square
<template v-slot:prepend> filled
<!-- 修饰键 --> use-input
<div class="row items-center q-gutter-x-xs"> hide-dropdown-icon
<q-chip new-value-mode="add-unique"
v-for="(active, key) in modifiers" input-debounce="0"
:key="key" emit-value
:color="active ? 'primary' : 'grey-4'" map-options
:text-color="active ? 'white' : 'grey-7'" options-dense
dense behavior="menu"
clickable class="col q-px-sm"
class="modifier-chip" placeholder="选择或输入按键"
@click="toggleModifier(key)" @filter="filterFn"
> @update:model-value="handleKeyInput"
{{ modifierLabels[key] }} @input="handleInput"
</q-chip> >
</div> <template v-slot:prepend>
</template> <!-- 修饰键 -->
<div class="row items-center q-gutter-x-xs no-wrap">
<!-- 主按键显示 --> <q-chip
<template v-slot:default> v-for="(active, key) in modifiers"
<div class="main-key-container"> :key="key"
<div class="main-key"> :color="active ? 'primary' : 'grey-4'"
{{ mainKeyDisplay }} :text-color="active ? 'white' : 'grey-7'"
dense
clickable
class="modifier-chip"
@click="toggleModifier(key)"
>
{{ modifierLabels[key] }}
</q-chip>
</div> </div>
</div> </template>
</template> <!-- 添加自定义选中值显示 -->
<template v-slot:selected>
<template v-slot:append> <q-badge
<q-separator vertical inset /> v-if="mainKey"
<!-- 选择按钮 --> color="primary"
<q-btn text-color="white"
flat class="main-key"
round >
dense {{ mainKeyDisplay }}
icon="edit" </q-badge>
@click="showKeySelect = true" </template>
> </q-select>
<q-tooltip>选择按键</q-tooltip> <!-- 录制按钮 -->
</q-btn> <q-btn
<q-separator vertical inset /> flat
<!-- 录制按钮 --> round
<q-btn dense
flat :icon="isRecording ? 'fiber_manual_record' : 'radio_button_unchecked'"
round :color="isRecording ? 'negative' : 'primary'"
dense @click="toggleRecording"
:icon="isRecording ? 'fiber_manual_record' : 'radio_button_unchecked'" >
:color="isRecording ? 'negative' : 'primary'" <q-tooltip>{{ isRecording ? "停止录制" : "开始录制" }}</q-tooltip>
@click="toggleRecording" </q-btn>
> </div>
<q-tooltip>{{ isRecording ? '停止录制' : '开始录制' }}</q-tooltip>
</q-btn>
</template>
</q-input>
<!-- 按键选择对话框 -->
<q-dialog v-model="showKeySelect">
<q-card style="min-width: 300px">
<q-card-section>
<div class="text-h6">选择按键</div>
</q-card-section>
<q-card-section class="q-gutter-y-md">
<!-- 主按键选择 -->
<q-select
v-model="mainKey"
:options="commonKeys"
label="选择用按键"
dense
outlined
emit-value
map-options
/>
<q-input
v-model="customKey"
label="或输入自定义按键"
dense
outlined
@update:model-value="mainKey = $event"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="确定" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div> </div>
</template> </template>
<script> <script>
import { defineComponent } from 'vue' import { defineComponent } from "vue";
// //
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 const isMac = window.utools.isMacOs();
export default defineComponent({ export default defineComponent({
name: 'KeyEditor', name: "KeyEditor",
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
default: '' default: "",
} },
}, },
data() { data() {
return { return {
isRecording: false, isRecording: false,
showKeySelect: false, showKeySelect: false,
mainKey: '', mainKey: "",
customKey: '',
modifiers: { modifiers: {
control: false, control: false,
alt: false, alt: false,
shift: false, shift: false,
command: false command: false,
},
modifierLabels: isMac ? {
control: '⌃',
alt: '⌥',
shift: '⇧',
command: '⌘'
} : {
control: 'Ctrl',
alt: 'Alt',
shift: 'Shift',
command: 'Win'
}, },
modifierLabels: isMac
? {
control: "⌃",
alt: "⌥",
shift: "⇧",
command: "⌘",
}
: {
control: "Ctrl",
alt: "Alt",
shift: "Shift",
command: "Win",
},
commonKeys: [ commonKeys: [
{ label: 'Enter ↵', value: 'enter' }, { label: "Enter ↵", value: "enter" },
{ label: 'Tab ⇥', value: 'tab' }, { label: "Tab ⇥", value: "tab" },
{ label: 'Space', value: 'space' }, { label: "Space", value: "space" },
{ label: 'Backspace ⌫', value: 'backspace' }, { label: "Backspace ⌫", value: "backspace" },
{ label: 'Delete ⌦', value: 'delete' }, { label: "Delete ⌦", value: "delete" },
{ label: 'Escape ⎋', value: 'escape' }, { label: "Escape ⎋", value: "escape" },
{ label: '↑', value: 'up' }, { label: "↑", value: "up" },
{ label: '↓', value: 'down' }, { label: "↓", value: "down" },
{ label: '←', value: 'left' }, { label: "←", value: "left" },
{ label: '→', value: 'right' }, { label: "→", value: "right" },
{ label: 'Home', value: 'home' }, { label: "Home", value: "home" },
{ label: 'End', value: 'end' }, { label: "End", value: "end" },
{ label: 'Page Up', value: 'pageup' }, { label: "Page Up", value: "pageup" },
{ label: 'Page Down', value: 'pagedown' } { label: "Page Down", value: "pagedown" },
] ],
} };
}, },
computed: { computed: {
mainKeyDisplay() { mainKeyDisplay() {
if (!this.mainKey) return '' if (!this.mainKey) return "";
// //
const specialKeyMap = { const specialKeyMap = {
'enter': '↵', enter: "↵",
'tab': '⇥', tab: "⇥",
'space': '␣', space: "␣",
'backspace': '⌫', backspace: "⌫",
'delete': '⌦', delete: "⌦",
'escape': '⎋', escape: "⎋",
'up': '↑', up: "↑",
'down': '↓', down: "↓",
'left': '←', left: "←",
'right': '→' right: "→",
} };
return specialKeyMap[this.mainKey] || return (
(this.mainKey.length === 1 ? this.mainKey.toUpperCase() : specialKeyMap[this.mainKey] ||
this.mainKey.charAt(0).toUpperCase() + this.mainKey.slice(1)) (this.mainKey.length === 1
} ? this.mainKey.toUpperCase()
: this.mainKey.charAt(0).toUpperCase() + this.mainKey.slice(1))
);
},
}, },
watch: { watch: {
modelValue: { modelValue: {
immediate: true, immediate: true,
handler(val) { handler(val) {
if (val) { if (val) {
this.parseKeyString(val) this.parseKeyString(val);
} }
} },
} },
}, },
methods: { methods: {
toggleModifier(key) { toggleModifier(key) {
this.modifiers[key] = !this.modifiers[key] this.modifiers[key] = !this.modifiers[key];
this.updateValue() this.updateValue();
}, },
toggleRecording() { toggleRecording() {
if (!this.isRecording) { if (!this.isRecording) {
this.startRecording() this.startRecording();
} else { } else {
this.stopRecording() this.stopRecording();
} }
}, },
startRecording() { startRecording() {
this.isRecording = true this.isRecording = true;
let lastKeyTime = 0 let lastKeyTime = 0;
let lastKey = null let lastKey = null;
this.recordEvent = (event) => { this.recordEvent = (event) => {
event.preventDefault() event.preventDefault();
const currentTime = Date.now() const currentTime = Date.now();
// //
Object.keys(this.modifiers).forEach(key => { Object.keys(this.modifiers).forEach((key) => {
this.modifiers[key] = false this.modifiers[key] = false;
}) });
// //
if (isMac) { if (isMac) {
if (event.metaKey) this.modifiers.command = true if (event.metaKey) this.modifiers.command = true;
if (event.ctrlKey) this.modifiers.control = true if (event.ctrlKey) this.modifiers.control = true;
} else { } else {
if (event.ctrlKey) this.modifiers.control = true if (event.ctrlKey) this.modifiers.control = true;
if (event.metaKey || event.winKey) this.modifiers.command = true if (event.metaKey || event.winKey) this.modifiers.command = true;
} }
if (event.altKey) this.modifiers.alt = true if (event.altKey) this.modifiers.alt = true;
if (event.shiftKey) this.modifiers.shift = true if (event.shiftKey) this.modifiers.shift = true;
// //
let key = null let key = null;
// //
if (event.code.startsWith('Key')) { if (event.code.startsWith("Key")) {
key = event.code.slice(-1).toLowerCase() key = event.code.slice(-1).toLowerCase();
} }
// //
else if (event.code.startsWith('Digit')) { else if (event.code.startsWith("Digit")) {
key = event.code.slice(-1) key = event.code.slice(-1);
} }
// //
else if (event.code.startsWith('F') && !isNaN(event.code.slice(1))) { else if (event.code.startsWith("F") && !isNaN(event.code.slice(1))) {
key = event.code.toLowerCase() key = event.code.toLowerCase();
} }
// //
else { else {
const keyMap = { const keyMap = {
'ArrowUp': 'up', ArrowUp: "up",
'ArrowDown': 'down', ArrowDown: "down",
'ArrowLeft': 'left', ArrowLeft: "left",
'ArrowRight': 'right', ArrowRight: "right",
'Enter': 'enter', Enter: "enter",
'Space': 'space', Space: "space",
'Escape': 'escape', Escape: "escape",
'Delete': 'delete', Delete: "delete",
'Backspace': 'backspace', Backspace: "backspace",
'Tab': 'tab', Tab: "tab",
'Home': 'home', Home: "home",
'End': 'end', End: "end",
'PageUp': 'pageup', PageUp: "pageup",
'PageDown': 'pagedown', PageDown: "pagedown",
'Control': 'control', Control: "control",
'Alt': 'alt', Alt: "alt",
'Shift': 'shift', Shift: "shift",
'Meta': 'command' Meta: "command",
} };
key = keyMap[event.code] || event.key.toLowerCase() key = keyMap[event.code] || event.key.toLowerCase();
} }
// //
if (['control', 'alt', 'shift', 'command'].includes(key)) { if (["control", "alt", "shift", "command"].includes(key)) {
if (key === lastKey && (currentTime - lastKeyTime) < 500) { if (key === lastKey && currentTime - lastKeyTime < 500) {
this.mainKey = key this.mainKey = key;
this.modifiers[key] = false // this.modifiers[key] = false; //
this.stopRecording() this.stopRecording();
this.updateValue() this.updateValue();
return return;
} }
lastKey = key lastKey = key;
lastKeyTime = currentTime lastKeyTime = currentTime;
return return;
} }
// //
if (key === 'space' || !['meta', 'control', 'shift', 'alt', 'command'].includes(key)) { if (
this.mainKey = key key === "space" ||
this.stopRecording() !["meta", "control", "shift", "alt", "command"].includes(key)
this.updateValue() ) {
this.mainKey = key;
this.stopRecording();
this.updateValue();
} }
} };
document.addEventListener('keydown', this.recordEvent) document.addEventListener("keydown", this.recordEvent);
}, },
stopRecording() { stopRecording() {
this.isRecording = false this.isRecording = false;
document.removeEventListener('keydown', this.recordEvent) document.removeEventListener("keydown", this.recordEvent);
}, },
updateValue() { updateValue() {
if (!this.mainKey) return if (!this.mainKey) return;
const activeModifiers = Object.entries(this.modifiers) const activeModifiers = Object.entries(this.modifiers)
.filter(([_, active]) => active) .filter(([_, active]) => active)
.map(([key]) => key) .map(([key]) => key)
// Mac command meta // Mac command meta
.map(key => !isMac && key === 'command' ? 'meta' : key) .map((key) => (!isMac && key === "command" ? "meta" : key));
const args = [this.mainKey, ...activeModifiers] const args = [this.mainKey, ...activeModifiers];
this.$emit('update:modelValue', args.join('","')) //
this.$emit("update:modelValue", `"${args.join('","')}"`);
}, },
parseKeyString(val) { parseKeyString(val) {
try { try {
const args = val.split('","') //
const cleanVal = val.replace(/^"|"$/g, "");
//
const args = cleanVal
.split('","')
.map((arg) => arg.replace(/^"|"$/g, ""));
if (args.length > 0) { if (args.length > 0) {
this.mainKey = args[0] this.mainKey = args[0];
Object.keys(this.modifiers).forEach(key => { Object.keys(this.modifiers).forEach((key) => {
// Mac meta command // Mac meta command
const modKey = !isMac && args.includes('meta') ? 'command' : key const modKey = !isMac && args.includes("meta") ? "command" : key;
this.modifiers[key] = args.includes(modKey) this.modifiers[key] = args.includes(modKey);
}) });
} }
} catch (e) { } catch (e) {
console.error('Failed to parse key string:', e) console.error("Failed to parse key string:", e);
} }
} },
} filterFn(val, update, abort) {
}) // 1
if (val.length === 1) {
abort();
return;
}
//
update(() => {
const needle = val.toLowerCase();
const matchedOptions = this.commonKeys.filter(
(key) =>
key.value === needle || key.label.toLowerCase().includes(needle)
);
});
},
handleKeyInput(val) {
if (val === null) {
this.mainKey = "";
} else if (typeof val === "string") {
//
const matchedOption = this.commonKeys.find(
(key) => key.value === val.toLowerCase()
);
if (matchedOption) {
this.mainKey = matchedOption.value;
} else {
this.mainKey = val.charAt(0).toLowerCase();
}
}
this.updateValue();
},
handleInput(val) {
//
if (val) {
this.mainKey = val.charAt(0).toLowerCase();
this.updateValue();
}
},
},
});
</script> </script>
<style scoped> <style scoped>
@ -325,38 +349,9 @@ export default defineComponent({
margin: 0 2px; margin: 0 2px;
} }
.main-key-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-width: 60px;
}
.main-key { .main-key {
height: 24px;
font-size: 13px; font-size: 13px;
font-weight: 500; margin: 0 2px;
color: var(--q-primary);
line-height: 24px;
padding: 0 8px;
}
:deep(.q-field__control) {
padding: 0 8px;
min-height: 36px;
}
:deep(.q-field__prepend) {
padding-right: 0;
}
:deep(.q-field__native) {
display: none;
}
:deep(.q-field__control-container) {
padding: 0;
display: flex;
align-items: center;
} }
</style> </style>