完善模拟按键组件

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

View File

@ -1,15 +1,30 @@
<template>
<div class="key-editor">
<!-- 按键输入框 -->
<q-input
<div class="row items-center q-gutter-x-sm full-width">
<!-- 按键选择/输入区域 -->
<q-select
v-model="mainKey"
:options="commonKeys"
dense
outlined
readonly
class="col"
square
filled
use-input
hide-dropdown-icon
new-value-mode="add-unique"
input-debounce="0"
emit-value
map-options
options-dense
behavior="menu"
class="col q-px-sm"
placeholder="选择或输入按键"
@filter="filterFn"
@update:model-value="handleKeyInput"
@input="handleInput"
>
<template v-slot:prepend>
<!-- 修饰键 -->
<div class="row items-center q-gutter-x-xs">
<div class="row items-center q-gutter-x-xs no-wrap">
<q-chip
v-for="(active, key) in modifiers"
:key="key"
@ -24,29 +39,18 @@
</q-chip>
</div>
</template>
<!-- 主按键显示 -->
<template v-slot:default>
<div class="main-key-container">
<div class="main-key">
{{ mainKeyDisplay }}
</div>
</div>
</template>
<template v-slot:append>
<q-separator vertical inset />
<!-- 选择按钮 -->
<q-btn
flat
round
dense
icon="edit"
@click="showKeySelect = true"
<!-- 添加自定义选中值显示 -->
<template v-slot:selected>
<q-badge
v-if="mainKey"
color="primary"
text-color="white"
class="main-key"
>
<q-tooltip>选择按键</q-tooltip>
</q-btn>
<q-separator vertical inset />
{{ mainKeyDisplay }}
</q-badge>
</template>
</q-select>
<!-- 录制按钮 -->
<q-btn
flat
@ -56,262 +60,282 @@
:color="isRecording ? 'negative' : 'primary'"
@click="toggleRecording"
>
<q-tooltip>{{ isRecording ? '停止录制' : '开始录制' }}</q-tooltip>
<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>
<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({
name: 'KeyEditor',
name: "KeyEditor",
props: {
modelValue: {
type: String,
default: ''
}
default: "",
},
},
data() {
return {
isRecording: false,
showKeySelect: false,
mainKey: '',
customKey: '',
mainKey: "",
modifiers: {
control: false,
alt: 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: [
{ label: 'Enter ↵', value: 'enter' },
{ label: 'Tab ⇥', value: 'tab' },
{ label: 'Space', value: 'space' },
{ label: 'Backspace ⌫', value: 'backspace' },
{ label: 'Delete ⌦', value: 'delete' },
{ label: 'Escape ⎋', value: 'escape' },
{ label: '↑', value: 'up' },
{ label: '↓', value: 'down' },
{ label: '←', value: 'left' },
{ label: '→', value: 'right' },
{ label: 'Home', value: 'home' },
{ label: 'End', value: 'end' },
{ label: 'Page Up', value: 'pageup' },
{ label: 'Page Down', value: 'pagedown' }
]
}
{ label: "Enter ↵", value: "enter" },
{ label: "Tab ⇥", value: "tab" },
{ label: "Space", value: "space" },
{ label: "Backspace ⌫", value: "backspace" },
{ label: "Delete ⌦", value: "delete" },
{ label: "Escape ⎋", value: "escape" },
{ label: "↑", value: "up" },
{ label: "↓", value: "down" },
{ label: "←", value: "left" },
{ label: "→", value: "right" },
{ label: "Home", value: "home" },
{ label: "End", value: "end" },
{ label: "Page Up", value: "pageup" },
{ label: "Page Down", value: "pagedown" },
],
};
},
computed: {
mainKeyDisplay() {
if (!this.mainKey) return ''
if (!this.mainKey) return "";
//
const specialKeyMap = {
'enter': '↵',
'tab': '⇥',
'space': '␣',
'backspace': '⌫',
'delete': '⌦',
'escape': '⎋',
'up': '↑',
'down': '↓',
'left': '←',
'right': '→'
}
return specialKeyMap[this.mainKey] ||
(this.mainKey.length === 1 ? this.mainKey.toUpperCase() :
this.mainKey.charAt(0).toUpperCase() + this.mainKey.slice(1))
}
enter: "↵",
tab: "⇥",
space: "␣",
backspace: "⌫",
delete: "⌦",
escape: "⎋",
up: "↑",
down: "↓",
left: "←",
right: "→",
};
return (
specialKeyMap[this.mainKey] ||
(this.mainKey.length === 1
? this.mainKey.toUpperCase()
: this.mainKey.charAt(0).toUpperCase() + this.mainKey.slice(1))
);
},
},
watch: {
modelValue: {
immediate: true,
handler(val) {
if (val) {
this.parseKeyString(val)
}
}
this.parseKeyString(val);
}
},
},
},
methods: {
toggleModifier(key) {
this.modifiers[key] = !this.modifiers[key]
this.updateValue()
this.modifiers[key] = !this.modifiers[key];
this.updateValue();
},
toggleRecording() {
if (!this.isRecording) {
this.startRecording()
this.startRecording();
} else {
this.stopRecording()
this.stopRecording();
}
},
startRecording() {
this.isRecording = true
let lastKeyTime = 0
let lastKey = null
this.isRecording = true;
let lastKeyTime = 0;
let lastKey = null;
this.recordEvent = (event) => {
event.preventDefault()
const currentTime = Date.now()
event.preventDefault();
const currentTime = Date.now();
//
Object.keys(this.modifiers).forEach(key => {
this.modifiers[key] = false
})
Object.keys(this.modifiers).forEach((key) => {
this.modifiers[key] = false;
});
//
if (isMac) {
if (event.metaKey) this.modifiers.command = true
if (event.ctrlKey) this.modifiers.control = true
if (event.metaKey) this.modifiers.command = true;
if (event.ctrlKey) this.modifiers.control = true;
} else {
if (event.ctrlKey) this.modifiers.control = true
if (event.metaKey || event.winKey) this.modifiers.command = true
if (event.ctrlKey) this.modifiers.control = true;
if (event.metaKey || event.winKey) this.modifiers.command = true;
}
if (event.altKey) this.modifiers.alt = true
if (event.shiftKey) this.modifiers.shift = true
if (event.altKey) this.modifiers.alt = true;
if (event.shiftKey) this.modifiers.shift = true;
//
let key = null
let key = null;
//
if (event.code.startsWith('Key')) {
key = event.code.slice(-1).toLowerCase()
if (event.code.startsWith("Key")) {
key = event.code.slice(-1).toLowerCase();
}
//
else if (event.code.startsWith('Digit')) {
key = event.code.slice(-1)
else if (event.code.startsWith("Digit")) {
key = event.code.slice(-1);
}
//
else if (event.code.startsWith('F') && !isNaN(event.code.slice(1))) {
key = event.code.toLowerCase()
else if (event.code.startsWith("F") && !isNaN(event.code.slice(1))) {
key = event.code.toLowerCase();
}
//
else {
const keyMap = {
'ArrowUp': 'up',
'ArrowDown': 'down',
'ArrowLeft': 'left',
'ArrowRight': 'right',
'Enter': 'enter',
'Space': 'space',
'Escape': 'escape',
'Delete': 'delete',
'Backspace': 'backspace',
'Tab': 'tab',
'Home': 'home',
'End': 'end',
'PageUp': 'pageup',
'PageDown': 'pagedown',
'Control': 'control',
'Alt': 'alt',
'Shift': 'shift',
'Meta': 'command'
}
key = keyMap[event.code] || event.key.toLowerCase()
ArrowUp: "up",
ArrowDown: "down",
ArrowLeft: "left",
ArrowRight: "right",
Enter: "enter",
Space: "space",
Escape: "escape",
Delete: "delete",
Backspace: "backspace",
Tab: "tab",
Home: "home",
End: "end",
PageUp: "pageup",
PageDown: "pagedown",
Control: "control",
Alt: "alt",
Shift: "shift",
Meta: "command",
};
key = keyMap[event.code] || event.key.toLowerCase();
}
//
if (['control', 'alt', 'shift', 'command'].includes(key)) {
if (key === lastKey && (currentTime - lastKeyTime) < 500) {
this.mainKey = key
this.modifiers[key] = false //
this.stopRecording()
this.updateValue()
return
if (["control", "alt", "shift", "command"].includes(key)) {
if (key === lastKey && currentTime - lastKeyTime < 500) {
this.mainKey = key;
this.modifiers[key] = false; //
this.stopRecording();
this.updateValue();
return;
}
lastKey = key
lastKeyTime = currentTime
return
lastKey = key;
lastKeyTime = currentTime;
return;
}
//
if (key === 'space' || !['meta', 'control', 'shift', 'alt', 'command'].includes(key)) {
this.mainKey = key
this.stopRecording()
this.updateValue()
if (
key === "space" ||
!["meta", "control", "shift", "alt", "command"].includes(key)
) {
this.mainKey = key;
this.stopRecording();
this.updateValue();
}
}
document.addEventListener('keydown', this.recordEvent)
};
document.addEventListener("keydown", this.recordEvent);
},
stopRecording() {
this.isRecording = false
document.removeEventListener('keydown', this.recordEvent)
this.isRecording = false;
document.removeEventListener("keydown", this.recordEvent);
},
updateValue() {
if (!this.mainKey) return
if (!this.mainKey) return;
const activeModifiers = Object.entries(this.modifiers)
.filter(([_, active]) => active)
.map(([key]) => key)
// Mac command meta
.map(key => !isMac && key === 'command' ? 'meta' : key)
// Mac command meta
.map((key) => (!isMac && key === "command" ? "meta" : key));
const args = [this.mainKey, ...activeModifiers]
this.$emit('update:modelValue', args.join('","'))
const args = [this.mainKey, ...activeModifiers];
//
this.$emit("update:modelValue", `"${args.join('","')}"`);
},
parseKeyString(val) {
try {
const args = val.split('","')
//
const cleanVal = val.replace(/^"|"$/g, "");
//
const args = cleanVal
.split('","')
.map((arg) => arg.replace(/^"|"$/g, ""));
if (args.length > 0) {
this.mainKey = args[0]
Object.keys(this.modifiers).forEach(key => {
this.mainKey = args[0];
Object.keys(this.modifiers).forEach((key) => {
// Mac meta command
const modKey = !isMac && args.includes('meta') ? 'command' : key
this.modifiers[key] = args.includes(modKey)
})
const modKey = !isMac && args.includes("meta") ? "command" : key;
this.modifiers[key] = args.includes(modKey);
});
}
} 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>
<style scoped>
@ -325,38 +349,9 @@ export default defineComponent({
margin: 0 2px;
}
.main-key-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-width: 60px;
}
.main-key {
height: 24px;
font-size: 13px;
font-weight: 500;
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;
margin: 0 2px;
}
</style>