编排分类调整

This commit is contained in:
fofolee
2025-01-05 23:14:52 +08:00
parent a6cc1c8737
commit 296c231c44
31 changed files with 59 additions and 147 deletions

View File

@@ -1,378 +0,0 @@
<template>
<div class="key-editor">
<div class="row items-center q-gutter-x-sm full-width">
<!-- 按键选择/输入区域 -->
<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 q-px-sm"
placeholder="选择或输入按键"
@update:model-value="handleKeyInput"
@input="handleInput"
>
<template v-slot:prepend>
<!-- 修饰键 -->
<div class="row items-center q-gutter-x-xs no-wrap">
<q-chip
v-for="(active, key) in argvs.modifiers"
:key="key"
:color="active ? 'primary' : 'grey-4'"
:text-color="active ? 'white' : 'grey-7'"
dense
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>
</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>
</template>
<script>
import { defineComponent } from "vue";
// 检测操作系统
const isMac = window.utools.isMacOs();
export default defineComponent({
name: "KeyEditor",
props: {
modelValue: {
type: Object,
required: true,
},
},
data() {
return {
isRecording: false,
showKeySelect: false,
defaultArgvs: {
mainKey: "",
modifiers: {
control: false,
alt: false,
shift: false,
command: false,
},
},
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" },
],
};
},
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))
);
},
},
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 args = [argvs.mainKey, ...activeModifiers];
return `${this.modelValue.value}("${args.join('","')}")`;
},
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 {
// 移除 keyTap 和引号
const cleanVal = val.replace(/^keyTap\("/, "").replace(/"\)$/, "");
// 分割并移除每个参数的引号
const args = cleanVal
.split('","')
.map((arg) => arg.replace(/^"|"$/g, ""));
if (args.length > 0) {
argvs.mainKey = args[0];
Object.keys(argvs.modifiers).forEach((key) => {
// 在非 Mac 系统上,将 meta 转换为 command
const modKey = !isMac && args.includes("meta") ? "command" : key;
argvs.modifiers[key] = args.includes(modKey);
});
return argvs;
}
} catch (e) {
console.error("Failed to parse key string:", e);
}
},
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 });
}
},
},
mounted() {
if (!this.modelValue.code && !this.modelValue.argvs) {
this.$emit("update:modelValue", {
...this.modelValue,
argvs: this.defaultArgvs,
code: this.generateCode(this.defaultArgvs),
});
}
},
});
</script>
<style scoped>
.key-editor {
padding: 4px 0;
}
.modifier-chip {
height: 24px;
font-size: 13px;
margin: 0 2px;
}
.main-key {
height: 24px;
font-size: 13px;
margin: 0 2px;
}
</style>

View File

@@ -1,294 +0,0 @@
<template>
<div class="flex-container">
<div
v-if="hasFunctionSelector"
class="flex-item"
:style="{ flex: localCommand.functionSelector.width || 3 }"
>
<div class="operation-cards">
<div
v-for="option in localCommand.functionSelector?.options"
:key="option.value"
:class="['operation-card', { active: functionName === option.value }]"
:data-value="option.value"
@click="functionName = option.value"
>
<q-icon
:name="option.icon || localCommand.icon || 'functions'"
size="16px"
:color="functionName === option.value ? 'primary' : 'grey'"
/>
<div class="text-caption">{{ option.label }}</div>
</div>
</div>
</div>
<div class="flex-container">
<div
v-for="(item, index) in localConfig"
:key="index"
class="flex-item"
:style="{ flex: item.width || 12 }"
>
<div v-if="item.type === 'varInput'">
<VariableInput
:model-value="argvs[index]"
@update:model-value="updateArgv(index, $event)"
:label="item.label"
:icon="item.icon"
/>
</div>
<div v-else-if="item.type === 'numInput'">
<NumberInput
:model-value="argvs[index]"
@update:model-value="updateArgv(index, $event)"
:label="item.label"
:icon="item.icon"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "./VariableInput.vue";
import NumberInput from "./NumberInput.vue";
import { stringifyWithType, parseToHasType } from "js/composer/formatString";
export default defineComponent({
name: "MultiParams",
components: {
VariableInput,
NumberInput,
},
props: {
modelValue: {
type: Object,
default: () => ({}),
required: true,
},
},
emits: ["update:modelValue"],
computed: {
localCommand() {
return this.modelValue;
},
localConfig() {
return (this.modelValue.config || []).map((item) => {
return {
...item,
value: item.defaultValue,
};
});
},
defaultArgvs() {
return this.localConfig.map((item) => item.value);
},
functionName: {
get() {
return (
this.modelValue.functionName ||
this.modelValue.functionSelector?.options[0]?.value ||
this.modelValue.value
);
},
set(value) {
this.updateModelValue(value, this.defaultArgvs);
},
},
argvs() {
return (
this.modelValue.argvs || this.parseCodeToArgvs(this.modelValue.code)
);
},
hasFunctionSelector() {
return !!this.localCommand.functionSelector;
},
},
methods: {
updateArgv(index, value) {
const newArgvs = [...this.argvs];
newArgvs[index] = value;
this.updateModelValue(this.functionName, newArgvs);
},
generateCode(functionName, argvs) {
const newArgvs = argvs.map((argv) => stringifyWithType(argv));
return `${functionName}(${newArgvs.join(",")})`;
},
parseCodeToArgvs(code) {
const argvs = window.lodashM.cloneDeep(this.defaultArgvs);
if (!code) return argvs;
// 匹配函数名和参数
const pattern = new RegExp(`^${this.functionName}\\((.*?)\\)$`);
const match = code.match(pattern);
if (match) {
try {
const paramStr = match[1].trim();
if (!paramStr) return argvs;
// 分割参数,考虑括号嵌套
let params = [];
let bracketCount = 0;
let currentParam = "";
for (let i = 0; i < paramStr.length; i++) {
const char = paramStr[i];
if (char === "," && bracketCount === 0) {
params.push(currentParam.trim());
currentParam = "";
continue;
}
if (char === "{") bracketCount++;
if (char === "}") bracketCount--;
currentParam += char;
}
if (currentParam) {
params.push(currentParam.trim());
}
// 根据配置处理每个参数
params.forEach((param, index) => {
if (index >= this.localConfig.length) return;
const config = this.localConfig[index];
if (config.type === "varInput") {
// 对于 VariableInput 类型,解析为带有 __varInputVal__ 标记的对象
argvs[index] = parseToHasType(param);
} else if (config.type === "numInput") {
// 对于 NumberInput 类型,转换为数字
argvs[index] = Number(param) || 0;
} else {
// 其他类型直接使用值
argvs[index] = param;
}
});
return argvs;
} catch (e) {
console.error("解析参数失败:", e);
}
}
return argvs;
},
getSummary(argvs) {
// 虽然header里对溢出做了处理但是这里截断主要是为了节省存储空间
const funcNameLabel = this.localCommand.functionSelector?.options.find(
(option) => option.value === this.functionName
)?.label;
const subFeature = funcNameLabel ? `${funcNameLabel} ` : "";
const allArgvs = argvs
.map((item) =>
item?.hasOwnProperty("__varInputVal__")
? window.lodashM.truncate(item.value, {
length: 30,
omission: "...",
})
: item
)
.filter((item) => item != null && item != "");
return `${subFeature}${allArgvs.join(",")}`;
},
updateModelValue(functionName, argvs) {
this.$emit("update:modelValue", {
...this.modelValue,
functionName,
summary: this.getSummary(argvs),
argvs,
code: this.generateCode(functionName, argvs),
});
},
},
mounted() {
if (
!this.modelValue.argvs &&
!this.modelValue.code &&
!this.modelValue.functionName
) {
this.updateModelValue(this.functionName, this.defaultArgvs);
}
},
watch: {
functionName: {
immediate: true,
handler(newVal) {
// 当操作卡片改变时,确保它在视图中可见
this.$nextTick(() => {
document
.querySelector(`.operation-card[data-value="${newVal}"]`)
?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
});
});
},
},
},
});
</script>
<style scoped>
.flex-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
.flex-item {
min-width: 100px;
}
@media (max-width: 600px) {
.flex-item {
flex: 1 1 100% !important;
}
}
.operation-cards {
display: flex;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding: 1px;
gap: 8px;
border-radius: 8px;
}
.operation-cards::-webkit-scrollbar {
display: none;
}
.operation-card {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
border-radius: 6px;
min-width: 72px;
padding: 2px 0;
background: rgba(0, 0, 0, 0.05);
}
.body--dark .operation-card {
background: rgba(0, 0, 0, 0.05);
}
.operation-card:hover {
background: var(--q-primary-opacity-5);
transform: translateY(-1px);
border: 1px solid var(--q-primary-opacity-10);
}
.operation-card.active {
border-color: var(--q-primary);
background: var(--q-primary-opacity-5);
}
.body--dark .operation-card.active {
border-color: var(--q-primary-opacity-50);
}
</style>