重构代码结构、数据传递方式,方便数据存取

This commit is contained in:
fofolee
2025-01-05 00:22:53 +08:00
parent dcaa00823b
commit 827c702e50
42 changed files with 2713 additions and 2143 deletions

View File

@@ -42,7 +42,8 @@
<VariableInput
:model-value="item.value"
:label="item.key || '值'"
:command="{ icon: 'code' }"
icon="code"
class="col-grow"
@update:model-value="(val) => updateItemValue(val, index)"
/>
</div>
@@ -104,7 +105,7 @@ export default defineComponent({
props: {
modelValue: {
type: Object,
default: () => ({}),
required: true,
},
options: {
type: Object,
@@ -116,11 +117,17 @@ export default defineComponent({
const modelEntries = Object.entries(this.modelValue || {});
return {
localItems: modelEntries.length
? modelEntries.map(([key, value]) => ({
key,
value: typeof value === "string" ? value : JSON.stringify(value),
}))
: [{ key: "", value: "" }],
? modelEntries.map(([key, value]) => ({ key, value }))
: [
{
key: "",
value: {
value: "",
isString: true,
__varInputVal__: true,
},
},
],
filterOptions: this.options?.items || [],
inputValue: "",
};
@@ -134,7 +141,7 @@ export default defineComponent({
this.localItems = newItems;
const dict = {};
newItems.forEach((item) => {
if (item.key && item.value) {
if (item.key) {
dict[item.key] = item.value;
}
});
@@ -144,22 +151,24 @@ export default defineComponent({
},
methods: {
addItem() {
this.items = [...this.items, { key: "", value: "" }];
this.items = [
...this.items,
{
key: "",
value: { value: "", isString: true, __varInputVal__: true },
},
];
},
removeItem(index) {
const newItems = [...this.items];
newItems.splice(index, 1);
if (newItems.length === 0) {
newItems.push({ key: "", value: "" });
newItems.push({
key: "",
value: { value: "", isString: true, __varInputVal__: true },
});
}
this.items = newItems;
const dict = {};
newItems.forEach((item) => {
if (item.key && item.value) {
dict[item.key] = item.value;
}
});
this.$emit("update:modelValue", dict);
},
updateItemKey(val, index) {
const newItems = [...this.items];

View File

@@ -3,7 +3,8 @@
<div class="row items-center q-gutter-x-sm full-width">
<!-- 按键选择/输入区域 -->
<q-select
v-model="mainKey"
ref="mainKeyInput"
v-model="argvs.mainKey"
:options="commonKeys"
dense
filled
@@ -17,7 +18,6 @@
behavior="menu"
class="col q-px-sm"
placeholder="选择或输入按键"
@filter="filterFn"
@update:model-value="handleKeyInput"
@input="handleInput"
>
@@ -25,7 +25,7 @@
<!-- 修饰键 -->
<div class="row items-center q-gutter-x-xs no-wrap">
<q-chip
v-for="(active, key) in modifiers"
v-for="(active, key) in argvs.modifiers"
:key="key"
:color="active ? 'primary' : 'grey-4'"
:text-color="active ? 'white' : 'grey-7'"
@@ -41,7 +41,7 @@
<!-- 添加自定义选中值显示 -->
<template v-slot:selected>
<q-badge
v-if="mainKey"
v-if="argvs.mainKey"
color="primary"
text-color="white"
class="main-key"
@@ -75,20 +75,22 @@ export default defineComponent({
name: "KeyEditor",
props: {
modelValue: {
type: String,
default: "",
type: Object,
required: true,
},
},
data() {
return {
isRecording: false,
showKeySelect: false,
mainKey: "",
modifiers: {
control: false,
alt: false,
shift: false,
command: false,
defaultArgvs: {
mainKey: "",
modifiers: {
control: false,
alt: false,
shift: false,
command: false,
},
},
modifierLabels: isMac
? {
@@ -121,14 +123,14 @@ export default defineComponent({
],
};
},
props: {
command: {
type: Object,
},
},
computed: {
argvs() {
return (
this.modelValue.argvs || this.parseCodeToArgvs(this.modelValue.code)
);
},
mainKeyDisplay() {
if (!this.mainKey) return "";
if (!this.argvs.mainKey) return "";
// 特殊按键映射表
const specialKeyMap = {
enter: "↵",
@@ -143,27 +145,24 @@ export default defineComponent({
right: "→",
};
return (
specialKeyMap[this.mainKey] ||
(this.mainKey.length === 1
? this.mainKey.toUpperCase()
: this.mainKey.charAt(0).toUpperCase() + this.mainKey.slice(1))
specialKeyMap[this.argvs.mainKey] ||
(this.argvs.mainKey.length === 1
? this.argvs.mainKey.toUpperCase()
: this.argvs.mainKey.charAt(0).toUpperCase() +
this.argvs.mainKey.slice(1))
);
},
},
watch: {
modelValue: {
immediate: true,
handler(val) {
if (val) {
this.parseKeyString(val);
}
},
},
},
methods: {
toggleModifier(key) {
this.modifiers[key] = !this.modifiers[key];
this.updateValue();
const newModifier = !this.argvs.modifiers[key];
this.argvs.modifiers[key] = newModifier;
this.updateValue({
modifiers: {
...this.argvs.modifiers,
[key]: newModifier,
},
});
},
toggleRecording() {
if (!this.isRecording) {
@@ -182,20 +181,21 @@ export default defineComponent({
const currentTime = Date.now();
// 重置所有修饰键状态
Object.keys(this.modifiers).forEach((key) => {
this.modifiers[key] = false;
Object.keys(this.argvs.modifiers).forEach((key) => {
this.argvs.modifiers[key] = false;
});
// 根据操作系统设置修饰键
if (isMac) {
if (event.metaKey) this.modifiers.command = true;
if (event.ctrlKey) this.modifiers.control = true;
if (event.metaKey) this.argvs.modifiers.command = true;
if (event.ctrlKey) this.argvs.modifiers.control = true;
} else {
if (event.ctrlKey) this.modifiers.control = true;
if (event.metaKey || event.winKey) this.modifiers.command = true;
if (event.ctrlKey) this.argvs.modifiers.control = true;
if (event.metaKey || event.winKey)
this.argvs.modifiers.command = true;
}
if (event.altKey) this.modifiers.alt = true;
if (event.shiftKey) this.modifiers.shift = true;
if (event.altKey) this.argvs.modifiers.alt = true;
if (event.shiftKey) this.argvs.modifiers.shift = true;
// 设置主按键
let key = null;
@@ -240,10 +240,16 @@ export default defineComponent({
// 处理双击修饰键
if (["control", "alt", "shift", "command"].includes(key)) {
if (key === lastKey && currentTime - lastKeyTime < 500) {
this.mainKey = key;
this.modifiers[key] = false; // 清除修饰键状态
this.argvs.mainKey = key;
this.argvs.modifiers[key] = false; // 清除修饰键状态
this.stopRecording();
this.updateValue();
this.updateValue({
modifiers: {
...this.argvs.modifiers,
[key]: false,
},
mainKey: key,
});
return;
}
lastKey = key;
@@ -256,9 +262,11 @@ export default defineComponent({
key === "space" ||
!["meta", "control", "shift", "alt", "command"].includes(key)
) {
this.mainKey = key;
this.argvs.mainKey = key;
this.stopRecording();
this.updateValue();
this.updateValue({
mainKey: key,
});
}
};
document.addEventListener("keydown", this.recordEvent);
@@ -267,19 +275,31 @@ export default defineComponent({
this.isRecording = false;
document.removeEventListener("keydown", this.recordEvent);
},
updateValue() {
if (!this.mainKey) return;
const activeModifiers = Object.entries(this.modifiers)
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 = [this.mainKey, ...activeModifiers];
// 为每个参数添加引号
this.$emit("update:modelValue", `${this.command.value}("${args.join('","')}")`);
const args = [argvs.mainKey, ...activeModifiers];
return `keyTap("${args.join('","')}")`;
},
parseKeyString(val) {
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(/"\)$/, "");
@@ -288,57 +308,54 @@ export default defineComponent({
.split('","')
.map((arg) => arg.replace(/^"|"$/g, ""));
if (args.length > 0) {
this.mainKey = args[0];
Object.keys(this.modifiers).forEach((key) => {
argvs.mainKey = args[0];
Object.keys(argvs.modifiers).forEach((key) => {
// 在非 Mac 系统上,将 meta 转换为 command
const modKey = !isMac && args.includes("meta") ? "command" : key;
this.modifiers[key] = args.includes(modKey);
argvs.modifiers[key] = args.includes(modKey);
});
return argvs;
}
} catch (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) {
let newMainKey;
if (val === null) {
this.mainKey = "";
newMainKey = "";
} else if (typeof val === "string") {
// 检查是否是预设选项
const matchedOption = this.commonKeys.find(
(key) => key.value === val.toLowerCase()
);
if (matchedOption) {
this.mainKey = matchedOption.value;
newMainKey = matchedOption.value;
} else {
this.mainKey = val.charAt(0).toLowerCase();
newMainKey = val.charAt(0).toLowerCase();
}
}
this.updateValue();
this.argvs.mainKey = newMainKey;
this.updateValue({ mainKey: newMainKey });
},
handleInput(val) {
// 直接输入时,取第一个字符并更新值
if (val) {
this.mainKey = val.charAt(0).toLowerCase();
this.updateValue();
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>

View File

@@ -3,35 +3,44 @@
<div
v-if="hasFunctionSelector"
class="flex-item"
:style="{ flex: command.functionSelector.width || 3 }"
:style="{ flex: localCommand.functionSelector.width || 3 }"
>
<q-select
v-model="selectedFunction"
:options="command.functionSelector.options"
:label="command.functionSelector.selectLabel"
v-model="functionName"
:options="localCommand.functionSelector.options"
:label="localCommand.functionSelector.selectLabel"
dense
filled
emit-value
map-options
@update:model-value="handleFunctionChange"
>
<template v-slot:prepend>
<q-icon :name="command.icon || 'functions'" />
<q-icon :name="localCommand.icon || 'functions'" />
</template>
</q-select>
</div>
<div
v-for="item in config"
:key="item.key"
v-for="(item, index) in localConfig"
:key="index"
class="flex-item"
:style="{ flex: item.width || 12 }"
>
<VariableInput
v-model="item.value"
:label="item.label"
:command="item"
@update:model-value="handleArgvChange(item.key, $event)"
/>
<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>
</template>
@@ -39,74 +48,149 @@
<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: String,
default: "",
},
command: {
type: Object,
default: () => ({}),
required: true,
},
},
emits: ["update:modelValue"],
data() {
return {
selectedFunction: this.command.functionSelector?.options[0]?.value || "",
localConfig: (this.command.config || []).map((item) => ({
...item,
value: item.defaultValue ?? "",
})),
};
},
computed: {
config() {
return this.localConfig;
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.$emit("update:modelValue", {
...this.modelValue,
functionName: value,
code: this.generateCode(value, this.argvs),
});
},
},
argvs() {
return (
this.modelValue.argvs || this.parseCodeToArgvs(this.modelValue.code)
);
},
hasFunctionSelector() {
return !!this.command.functionSelector;
return !!this.localCommand.functionSelector;
},
},
methods: {
generateCode() {
const functionName = this.hasFunctionSelector
? this.selectedFunction
: this.command.value;
const args = this.config
.map((item) => item.value)
.filter((val) => val !== undefined && val !== "")
.join(",");
return `${functionName}(${args})`;
},
handleArgvChange(key, value) {
const item = this.localConfig.find((item) => item.key === key);
if (item) {
item.value = value;
}
updateArgv(index, value) {
const newArgvs = [...this.argvs];
newArgvs[index] = value;
this.$emit("update:modelValue", this.generateCode());
const newCode = this.generateCode(this.functionName, newArgvs);
this.$emit("update:modelValue", {
...this.modelValue,
argvs: newArgvs,
code: newCode,
});
},
handleFunctionChange(value) {
this.selectedFunction = value;
this.$emit("update:modelValue", this.generateCode());
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;
},
},
mounted() {
if (this.command.allowEmptyArgv) {
this.$emit("update:modelValue", this.generateCode());
} else {
const hasDefaultValues = this.localConfig.some(
(item) => item.defaultValue !== undefined && item.defaultValue !== ""
);
if (hasDefaultValues) {
this.$emit("update:modelValue", this.generateCode());
}
if (
!this.modelValue.argvs &&
!this.modelValue.code &&
!this.modelValue.functionName
) {
this.$emit("update:modelValue", {
...this.modelValue,
functionName: this.functionName,
argvs: this.defaultArgvs,
code: this.generateCode(this.functionName, this.defaultArgvs),
});
}
},
});

View File

@@ -0,0 +1,118 @@
<template>
<q-input
type="number"
v-model.number="localValue"
dense
filled
:label="label"
class="number-input"
>
<template v-slot:prepend>
<q-icon v-if="icon" :name="icon" />
</template>
<template v-slot:append>
<!-- <q-icon name="pin" size="xs" /> -->
<div class="column items-center number-controls">
<q-btn
flat
dense
icon="keyboard_arrow_up"
size="xs"
class="number-btn"
@click="updateNumber(100)"
/>
<q-btn
flat
dense
icon="keyboard_arrow_down"
size="xs"
class="number-btn"
@click="updateNumber(-100)"
/>
</div>
</template>
</q-input>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "NumberInput",
props: {
modelValue: {
type: Number,
},
label: {
type: String,
default: "",
},
icon: {
type: String,
default: "pin",
},
},
emits: ["update:modelValue"],
computed: {
localValue: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
},
},
},
methods: {
updateNumber(delta) {
this.$emit("update:modelValue", this.localValue + delta);
},
},
});
</script>
<style scoped>
/* 数字输入框样式 */
.number-input {
width: 100%;
}
/* 隐藏默认的数字输入框箭头 - Chrome, Safari, Edge, Opera */
.number-input :deep(input[type="number"]::-webkit-outer-spin-button),
.number-input :deep(input[type="number"]::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
.number-input :deep(.q-field__control) {
padding-left: 8px;
padding-right: 4px;
}
.number-controls {
height: 100%;
display: flex;
width: 32px;
flex-direction: column;
justify-content: center;
}
.number-btn {
opacity: 0.7;
font-size: 12px;
padding: 0;
margin: 0;
min-height: 16px;
height: 16px;
width: 20px;
}
.number-btn :deep(.q-icon) {
font-size: 12px;
}
.number-btn:hover {
opacity: 1;
color: var(--q-primary);
}
</style>

View File

@@ -1,7 +1,6 @@
<template>
<q-input
v-if="!isNumber"
v-model="localValue"
v-model="inputValue"
dense
filled
:label="label"
@@ -27,7 +26,7 @@
:icon="isString ? 'format_quote' : 'data_object'"
size="sm"
class="string-toggle"
@click="toggleStringType"
@click="isString = !isString"
v-if="!hasSelectedVariable"
>
<q-tooltip>{{
@@ -79,42 +78,7 @@
</q-btn-dropdown>
</template>
<template v-slot:prepend>
<q-icon :name="command.icon || 'code'" />
</template>
</q-input>
<!-- 强制为数字类型时不支持切换类型 -->
<q-input
v-else
type="number"
v-model.number="localValue"
dense
filled
:label="label"
class="number-input"
>
<template v-slot:prepend>
<q-icon v-if="command.icon" :name="command.icon" />
</template>
<template v-slot:append>
<!-- <q-icon name="pin" size="xs" /> -->
<div class="column items-center number-controls">
<q-btn
flat
dense
icon="keyboard_arrow_up"
size="xs"
class="number-btn"
@click="updateNumber(100)"
/>
<q-btn
flat
dense
icon="keyboard_arrow_down"
size="xs"
class="number-btn"
@click="updateNumber(-100)"
/>
</div>
<q-icon :name="icon || 'code'" />
</template>
</q-input>
</template>
@@ -126,16 +90,20 @@ export default defineComponent({
name: "VariableInput",
props: {
modelValue: [String, Number],
label: String,
command: {
modelValue: {
type: Object,
required: true,
default: () => ({
value: "",
isString: true,
__varInputVal__: true,
}),
},
label: String,
icon: String,
},
emits: ["update:modelValue"],
setup() {
const variables = inject("composerVariables", []);
return { variables };
@@ -144,103 +112,60 @@ export default defineComponent({
data() {
return {
selectedVariable: null,
// 根据输入类型初始化字符串状态
isString: this.command.inputType !== "number",
};
},
computed: {
localValue: {
get() {
// 数字类型直接返回原值
if (this.isNumber) return this.modelValue;
// 非数字类型时根据isString状态决定是否显示引号
const val = this.modelValue || "";
return this.isString ? val.replace(/^"|"$/g, "") : val;
},
set(value) {
this.$emit("update:modelValue", this.formatValue(value));
},
},
// 判断是否有选中的变量用于控制UI显示和行为
hasSelectedVariable() {
return this.selectedVariable !== null;
},
isNumber() {
return this.command.inputType === "number";
inputValue: {
get() {
return this.modelValue.value;
},
set(value) {
this.$emit("update:modelValue", {
...this.modelValue,
value,
});
},
},
isString: {
get() {
return this.modelValue.isString;
},
set(value) {
this.$emit("update:modelValue", {
...this.modelValue,
isString: value,
});
},
},
},
methods: {
// 格式化值,处理引号的添加和移除
formatValue(value) {
// 空值、变量模式或数字类型时不处理
if (!value || this.hasSelectedVariable || this.isNumber) return value;
// 根据isString状态添加或移除引号
return this.isString
? `"${value.replace(/^"|"$/g, "")}"`
: value.replace(/^"|"$/g, "");
},
// 切换字符串/非字符串模式
toggleStringType() {
if (!this.hasSelectedVariable) {
this.isString = !this.isString;
// 有值时需要重新格式化
if (this.modelValue) {
this.$emit("update:modelValue", this.formatValue(this.modelValue));
}
}
},
// 外部调用的方法,用于设置字符串状态
setIsString(value) {
if (!this.hasSelectedVariable && this.isString !== value) {
this.isString = value;
// 有值时需要重新格式化
if (this.modelValue) {
this.$emit("update:modelValue", this.formatValue(this.modelValue));
}
}
},
// 插入变量时的处理
insertVariable(variable) {
this.selectedVariable = variable;
this.isString = false; // 变量模式下不需要字符串处理
this.$emit("update:modelValue", variable.name);
this.$emit("update:modelValue", {
isString: false,
value: variable.name,
__varInputVal__: true,
});
},
// 清除变量时的处理
clearVariable() {
this.selectedVariable = null;
this.isString = true; // 恢复到字符串模式
this.$emit("update:modelValue", "");
},
// 数字类型特有的增减处理
updateNumber(delta) {
const current = Number(this.localValue) || 0;
this.$emit("update:modelValue", current + delta);
},
},
watch: {
// 解决通过外部传入值时,无法触发字符串处理的问题
modelValue: {
immediate: true,
handler(newVal) {
// 只在有值且非变量模式且非数字类型时处理
if (newVal && !this.hasSelectedVariable && !this.isNumber) {
const formattedValue = this.formatValue(newVal);
// 只在值真正需要更新时才发更新
if (formattedValue !== newVal) {
this.$emit("update:modelValue", formattedValue);
}
}
},
this.$emit("update:modelValue", {
isString: true,
value: "",
__varInputVal__: true,
});
},
},
});
@@ -325,48 +250,4 @@ export default defineComponent({
transform: scale(1.1);
color: var(--q-negative);
}
/* 数字输入框样式 */
.number-input {
width: 100%;
}
/* 隐藏默认的数字输入框箭头 - Chrome, Safari, Edge, Opera */
.number-input :deep(input[type="number"]::-webkit-outer-spin-button),
.number-input :deep(input[type="number"]::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
.number-input :deep(.q-field__control) {
padding-left: 8px;
padding-right: 4px;
}
.number-controls {
height: 100%;
display: flex;
width: 32px;
flex-direction: column;
justify-content: center;
}
.number-btn {
opacity: 0.7;
font-size: 12px;
padding: 0;
margin: 0;
min-height: 16px;
height: 16px;
width: 20px;
}
.number-btn :deep(.q-icon) {
font-size: 12px;
}
.number-btn:hover {
opacity: 1;
color: var(--q-primary);
}
</style>