mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-30 13:02:46 +08:00
744 lines
19 KiB
Vue
744 lines
19 KiB
Vue
<template>
|
|
<div class="key-editor">
|
|
<div class="row items-center q-gutter-x-sm full-width q-mb-sm">
|
|
<!-- 按键选择/输入区域 -->
|
|
<q-select
|
|
ref="mainKeyInput"
|
|
v-model="argvs.mainKey"
|
|
:options="commonKeys"
|
|
dense
|
|
filled
|
|
use-input
|
|
hide-dropdown-icon
|
|
new-value-mode="add-unique"
|
|
input-debounce="0"
|
|
emit-value
|
|
map-options
|
|
options-dense
|
|
behavior="menu"
|
|
class="col"
|
|
:input-style="{
|
|
textAlign: 'center',
|
|
}"
|
|
placeholder="选择或输入按键"
|
|
@update:model-value="handleKeyInput"
|
|
@input="handleInput"
|
|
>
|
|
<template v-slot:prepend>
|
|
<!-- 修饰键 -->
|
|
<div class="row items-center q-gutter-x-xs no-wrap q-pa-sm">
|
|
<q-chip
|
|
v-for="(active, key) in argvs.modifiers"
|
|
:key="key"
|
|
:class="{ 'modifier-chip-active': active }"
|
|
clickable
|
|
class="modifier-chip"
|
|
@click="toggleModifier(key)"
|
|
>
|
|
{{ modifierLabels[key] }}
|
|
</q-chip>
|
|
</div>
|
|
</template>
|
|
<!-- 添加自定义选中值显示 -->
|
|
<template v-slot:selected>
|
|
<q-badge
|
|
v-if="argvs.mainKey"
|
|
color="primary"
|
|
text-color="white"
|
|
class="main-key"
|
|
>
|
|
{{ mainKeyDisplay }}
|
|
</q-badge>
|
|
</template>
|
|
<template v-slot:append>
|
|
<q-btn
|
|
flat
|
|
dense
|
|
round
|
|
icon="more_vert"
|
|
color="primary"
|
|
class="q-ml-sm"
|
|
@click.stop
|
|
>
|
|
<q-menu anchor="bottom right" self="top right">
|
|
<q-list style="min-width: 200px">
|
|
<template
|
|
v-for="(shortcut, index) in commonShortcuts"
|
|
:key="index"
|
|
>
|
|
<template v-if="shortcut.header">
|
|
<q-item-label
|
|
v-if="shortcut.show === undefined || shortcut.show"
|
|
header
|
|
class="q-mt-sm"
|
|
>
|
|
{{ shortcut.label }}
|
|
</q-item-label>
|
|
<q-separator
|
|
v-if="shortcut.show === undefined || shortcut.show"
|
|
/>
|
|
</template>
|
|
<q-item
|
|
v-else-if="shortcut.show === undefined || shortcut.show"
|
|
clickable
|
|
v-close-popup
|
|
@click="applyShortcut(shortcut)"
|
|
>
|
|
<q-item-section>
|
|
<q-item-label>{{ shortcut.label }}</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-item-label caption>
|
|
{{ formatShortcut(shortcut) }}
|
|
</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
</template>
|
|
</q-list>
|
|
</q-menu>
|
|
</q-btn>
|
|
</template>
|
|
</q-select>
|
|
<!-- 录制按钮 -->
|
|
<q-btn
|
|
flat
|
|
round
|
|
dense
|
|
:icon="isRecording ? 'fiber_manual_record' : 'radio_button_unchecked'"
|
|
:color="isRecording ? 'negative' : 'primary'"
|
|
@click="toggleRecording"
|
|
>
|
|
<q-tooltip>{{ isRecording ? "停止录制" : "开始录制" }}</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
<!-- 按键控制区 -->
|
|
<div class="row q-gutter-x-sm full-width">
|
|
<!-- 重复次数 -->
|
|
<div class="col">
|
|
<number-input
|
|
v-model="argvs.repeatCount"
|
|
label="重复次数"
|
|
icon="repeat"
|
|
@update:model-value="(val) => updateValue({ repeatCount: val })"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 重复间隔 -->
|
|
<div class="col">
|
|
<number-input
|
|
v-model="argvs.repeatInterval"
|
|
label="重复间隔(ms)"
|
|
icon="timer"
|
|
:disable="argvs.repeatCount <= 1"
|
|
@update:model-value="(val) => updateValue({ repeatInterval: val })"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 按键后延迟 -->
|
|
<div class="col">
|
|
<number-input
|
|
v-model="argvs.keyDelay"
|
|
label="按键后延迟(ms)"
|
|
icon="hourglass_empty"
|
|
@update:model-value="(val) => updateValue({ keyDelay: val })"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { defineComponent } from "vue";
|
|
import NumberInput from "components/composer/common/NumberInput.vue";
|
|
import { parseFunction } from "js/composer/formatString";
|
|
|
|
// 检测操作系统
|
|
const isMac = window.utools.isMacOs();
|
|
|
|
// 常用按键列表
|
|
const 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" },
|
|
];
|
|
|
|
// 常用快捷键列表
|
|
const commonShortcuts = [
|
|
// Windows 快捷键
|
|
{
|
|
label: "Windows 快捷键",
|
|
header: true,
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "任务管理器",
|
|
mainKey: "delete",
|
|
modifiers: { control: true, shift: true, alt: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "运行",
|
|
mainKey: "r",
|
|
modifiers: { command: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "切换到地址栏",
|
|
mainKey: "l",
|
|
modifiers: { control: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "切换窗口",
|
|
mainKey: "tab",
|
|
modifiers: { alt: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "锁定电脑",
|
|
mainKey: "l",
|
|
modifiers: { command: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "显示桌面",
|
|
mainKey: "d",
|
|
modifiers: { command: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "文件资源管理器",
|
|
mainKey: "e",
|
|
modifiers: { command: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "快速访问设置",
|
|
mainKey: "a",
|
|
modifiers: { command: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "打开开始菜单",
|
|
mainKey: "escape",
|
|
modifiers: { control: true },
|
|
show: !isMac,
|
|
},
|
|
{
|
|
label: "系统属性",
|
|
mainKey: "pause",
|
|
modifiers: { command: true },
|
|
show: !isMac,
|
|
},
|
|
|
|
// macOS 快捷键
|
|
{
|
|
label: "macOS 快捷键",
|
|
header: true,
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "强制退出",
|
|
mainKey: "escape",
|
|
modifiers: { command: true, alt: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "截图",
|
|
mainKey: "3",
|
|
modifiers: { command: true, shift: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "区域截图",
|
|
mainKey: "4",
|
|
modifiers: { command: true, shift: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "前往文件夹",
|
|
mainKey: "g",
|
|
modifiers: { command: true, shift: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "新建文件夹",
|
|
mainKey: "n",
|
|
modifiers: { command: true, shift: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "显示隐藏文件",
|
|
mainKey: ".",
|
|
modifiers: { command: true, shift: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "访达偏好设置",
|
|
mainKey: ",",
|
|
modifiers: { command: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "清空废纸篓",
|
|
mainKey: "backspace",
|
|
modifiers: { command: true, shift: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "排序方式",
|
|
mainKey: "j",
|
|
modifiers: { command: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "显示简介",
|
|
mainKey: "i",
|
|
modifiers: { command: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "最小化所有窗口",
|
|
mainKey: "m",
|
|
modifiers: { command: true, alt: true },
|
|
show: isMac,
|
|
},
|
|
{
|
|
label: "调度中心",
|
|
mainKey: "up",
|
|
modifiers: { control: true },
|
|
show: isMac,
|
|
},
|
|
|
|
// 通用快捷键(所有系统)
|
|
{
|
|
label: "通用操作",
|
|
header: true,
|
|
},
|
|
{
|
|
label: "复制",
|
|
mainKey: "c",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "粘贴",
|
|
mainKey: "v",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "剪切",
|
|
mainKey: "x",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "撤销",
|
|
mainKey: "z",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "重做",
|
|
mainKey: isMac ? "z" : "y",
|
|
modifiers: isMac ? { command: true, shift: true } : { control: true },
|
|
},
|
|
{
|
|
label: "全选",
|
|
mainKey: "a",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "保存",
|
|
mainKey: "s",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "查找",
|
|
mainKey: "f",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "关闭窗口",
|
|
mainKey: isMac ? "w" : "f4",
|
|
modifiers: isMac ? { command: true } : { alt: true },
|
|
},
|
|
{
|
|
label: "刷新",
|
|
mainKey: "r",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "新建",
|
|
mainKey: "n",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "打印",
|
|
mainKey: "p",
|
|
modifiers: isMac ? { command: true } : { control: true },
|
|
},
|
|
{
|
|
label: "删除",
|
|
mainKey: "delete",
|
|
modifiers: {},
|
|
},
|
|
];
|
|
|
|
export default defineComponent({
|
|
name: "KeyEditor",
|
|
components: {
|
|
NumberInput,
|
|
},
|
|
props: {
|
|
modelValue: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isRecording: false,
|
|
showKeySelect: false,
|
|
defaultArgvs: {
|
|
mainKey: "",
|
|
modifiers: {
|
|
control: false,
|
|
alt: false,
|
|
shift: false,
|
|
command: false,
|
|
},
|
|
repeatCount: 1,
|
|
repeatInterval: 0,
|
|
keyDelay: 0,
|
|
},
|
|
modifierLabels: isMac
|
|
? {
|
|
control: "⌃",
|
|
alt: "⌥",
|
|
shift: "⇧",
|
|
command: "⌘",
|
|
}
|
|
: {
|
|
control: "Ctrl",
|
|
alt: "Alt",
|
|
shift: "Shift",
|
|
command: "Win",
|
|
},
|
|
};
|
|
},
|
|
computed: {
|
|
argvs() {
|
|
return (
|
|
this.modelValue.argvs || this.parseCodeToArgvs(this.modelValue.code)
|
|
);
|
|
},
|
|
mainKeyDisplay() {
|
|
if (!this.argvs.mainKey) return "";
|
|
// 特殊按键映射表
|
|
const specialKeyMap = {
|
|
enter: "↵",
|
|
tab: "⇥",
|
|
space: "␣",
|
|
backspace: "⌫",
|
|
delete: "⌦",
|
|
escape: "⎋",
|
|
up: "↑",
|
|
down: "↓",
|
|
left: "←",
|
|
right: "→",
|
|
};
|
|
return (
|
|
specialKeyMap[this.argvs.mainKey] ||
|
|
(this.argvs.mainKey.length === 1
|
|
? this.argvs.mainKey.toUpperCase()
|
|
: this.argvs.mainKey.charAt(0).toUpperCase() +
|
|
this.argvs.mainKey.slice(1))
|
|
);
|
|
},
|
|
commonKeys() {
|
|
return commonKeys;
|
|
},
|
|
commonShortcuts() {
|
|
return commonShortcuts;
|
|
},
|
|
},
|
|
methods: {
|
|
toggleModifier(key) {
|
|
const newModifier = !this.argvs.modifiers[key];
|
|
this.argvs.modifiers[key] = newModifier;
|
|
this.updateValue({
|
|
modifiers: {
|
|
...this.argvs.modifiers,
|
|
[key]: newModifier,
|
|
},
|
|
});
|
|
},
|
|
toggleRecording() {
|
|
if (!this.isRecording) {
|
|
this.startRecording();
|
|
} else {
|
|
this.stopRecording();
|
|
}
|
|
},
|
|
startRecording() {
|
|
this.isRecording = true;
|
|
let lastKeyTime = 0;
|
|
let lastKey = null;
|
|
|
|
this.recordEvent = (event) => {
|
|
event.preventDefault();
|
|
const currentTime = Date.now();
|
|
|
|
// 重置所有修饰键状态
|
|
Object.keys(this.argvs.modifiers).forEach((key) => {
|
|
this.argvs.modifiers[key] = false;
|
|
});
|
|
|
|
// 根据操作系统设置修饰键
|
|
if (isMac) {
|
|
if (event.metaKey) this.argvs.modifiers.command = true;
|
|
if (event.ctrlKey) this.argvs.modifiers.control = true;
|
|
} else {
|
|
if (event.ctrlKey) this.argvs.modifiers.control = true;
|
|
if (event.metaKey || event.winKey)
|
|
this.argvs.modifiers.command = true;
|
|
}
|
|
if (event.altKey) this.argvs.modifiers.alt = true;
|
|
if (event.shiftKey) this.argvs.modifiers.shift = true;
|
|
|
|
// 设置主按键
|
|
let key = null;
|
|
|
|
// 处理字母键
|
|
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("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();
|
|
}
|
|
|
|
// 处理双击修饰键
|
|
if (["control", "alt", "shift", "command"].includes(key)) {
|
|
if (key === lastKey && currentTime - lastKeyTime < 500) {
|
|
this.argvs.mainKey = key;
|
|
this.argvs.modifiers[key] = false; // 清除修饰键状态
|
|
this.stopRecording();
|
|
this.updateValue({
|
|
modifiers: {
|
|
...this.argvs.modifiers,
|
|
[key]: false,
|
|
},
|
|
mainKey: key,
|
|
});
|
|
return;
|
|
}
|
|
lastKey = key;
|
|
lastKeyTime = currentTime;
|
|
return;
|
|
}
|
|
|
|
// 处理空格键和其他按键
|
|
if (
|
|
key === "space" ||
|
|
!["meta", "control", "shift", "alt", "command"].includes(key)
|
|
) {
|
|
this.argvs.mainKey = key;
|
|
this.stopRecording();
|
|
this.updateValue({
|
|
mainKey: key,
|
|
});
|
|
}
|
|
};
|
|
document.addEventListener("keydown", this.recordEvent);
|
|
},
|
|
stopRecording() {
|
|
this.isRecording = false;
|
|
document.removeEventListener("keydown", this.recordEvent);
|
|
},
|
|
generateCode(argvs = this.argvs) {
|
|
if (!argvs.mainKey) return;
|
|
const activeModifiers = Object.entries(argvs.modifiers)
|
|
.filter(([_, active]) => active)
|
|
.map(([key]) => key)
|
|
// 在非 Mac 系统上,将 command 换为 meta
|
|
.map((key) => (!isMac && key === "command" ? "meta" : key));
|
|
|
|
const keys = [argvs.mainKey, ...activeModifiers];
|
|
|
|
// 添加重复次数、间隔和延迟参数
|
|
const options = {};
|
|
if (argvs.repeatCount > 1) options.repeatCount = argvs.repeatCount;
|
|
if (argvs.repeatInterval > 0)
|
|
options.repeatInterval = argvs.repeatInterval;
|
|
if (argvs.keyDelay > 0) options.keyDelay = argvs.keyDelay;
|
|
|
|
if (Object.keys(options).length > 0) {
|
|
return `${this.modelValue.value}(${JSON.stringify(
|
|
keys
|
|
)}, ${JSON.stringify(options)})`;
|
|
}
|
|
return `${this.modelValue.value}(${JSON.stringify(keys)})`;
|
|
},
|
|
updateValue(argv) {
|
|
const newArgvs = {
|
|
...this.argvs,
|
|
...argv,
|
|
};
|
|
this.$emit("update:modelValue", {
|
|
...this.modelValue,
|
|
argvs: newArgvs,
|
|
code: this.generateCode(newArgvs),
|
|
});
|
|
},
|
|
parseCodeToArgvs(code) {
|
|
const argvs = window.lodashM.cloneDeep(this.defaultArgvs);
|
|
if (!code) return argvs;
|
|
try {
|
|
const result = parseFunction(code);
|
|
if (!result || !result.argvs || !result.argvs[0]) return argvs;
|
|
|
|
const keys = result.argvs[0];
|
|
const options = result.argvs[1] || {};
|
|
|
|
if (keys.length > 0) {
|
|
argvs.mainKey = keys[0];
|
|
Object.keys(argvs.modifiers).forEach((key) => {
|
|
// 在非 Mac 系统上,将 meta 转换为 command
|
|
const modKey = !isMac && key === "command" ? "meta" : key;
|
|
argvs.modifiers[key] = keys.slice(1).includes(modKey);
|
|
});
|
|
}
|
|
|
|
// 解析选项对象
|
|
if (options) {
|
|
if (options.repeatCount) argvs.repeatCount = options.repeatCount;
|
|
if (options.repeatInterval)
|
|
argvs.repeatInterval = options.repeatInterval;
|
|
if (options.keyDelay) argvs.keyDelay = options.keyDelay;
|
|
}
|
|
|
|
return argvs;
|
|
} catch (e) {
|
|
console.error("Failed to parse key string:", e);
|
|
return argvs;
|
|
}
|
|
},
|
|
handleKeyInput(val) {
|
|
let newMainKey;
|
|
if (val === null) {
|
|
newMainKey = "";
|
|
} else if (typeof val === "string") {
|
|
// 检查是否是预设选项
|
|
const matchedOption = this.commonKeys.find(
|
|
(key) => key.value === val.toLowerCase()
|
|
);
|
|
if (matchedOption) {
|
|
newMainKey = matchedOption.value;
|
|
} else {
|
|
newMainKey = val.charAt(0).toLowerCase();
|
|
}
|
|
}
|
|
this.argvs.mainKey = newMainKey;
|
|
this.updateValue({ mainKey: newMainKey });
|
|
},
|
|
handleInput(val) {
|
|
// 直接输入时,取第一个字符并更新值
|
|
if (val) {
|
|
this.argvs.mainKey = val.data;
|
|
this.$refs.mainKeyInput.blur();
|
|
this.updateValue({ mainKey: val.data });
|
|
}
|
|
},
|
|
applyShortcut(shortcut) {
|
|
this.updateValue({
|
|
mainKey: shortcut.mainKey,
|
|
modifiers: {
|
|
...this.defaultArgvs.modifiers,
|
|
...shortcut.modifiers,
|
|
},
|
|
});
|
|
},
|
|
formatShortcut(shortcut) {
|
|
const modifiers = Object.entries(shortcut.modifiers)
|
|
.filter(([_, active]) => active)
|
|
.map(([key]) => this.modifierLabels[key])
|
|
.join(" + ");
|
|
return `${modifiers} + ${shortcut.mainKey}`;
|
|
},
|
|
},
|
|
mounted() {
|
|
const argvs = this.modelValue.argvs || this.defaultArgvs;
|
|
if (!this.modelValue.code) {
|
|
this.$emit("update:modelValue", {
|
|
...this.modelValue,
|
|
argvs,
|
|
code: this.generateCode(argvs),
|
|
});
|
|
}
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.modifier-chip {
|
|
height: 24px;
|
|
font-size: 13px;
|
|
margin: 0 2px;
|
|
border-radius: 8px;
|
|
background-color: rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.modifier-chip-active {
|
|
background-color: var(--q-primary) !important;
|
|
color: white;
|
|
}
|
|
|
|
.body--dark .modifier-chip {
|
|
background-color: rgba(44, 44, 44, 0.5);
|
|
}
|
|
|
|
.main-key {
|
|
height: 24px;
|
|
font-size: 13px;
|
|
margin: 0 2px;
|
|
}
|
|
</style>
|