重构 OutputEditor:提取可重用组件并改进变量验证

This commit is contained in:
fofolee 2025-01-26 13:24:04 +08:00
parent 365964fc02
commit 40f1e1d7f7
6 changed files with 357 additions and 251 deletions

View File

@ -4,174 +4,82 @@
<div class="row justify-center q-px-sm q-pt-md">
{{ commandName }}
</div>
<div class="simple-output q-px-sm">
<q-badge color="primary" class="q-mb-sm q-pa-xs">完整结果</q-badge>
<q-input v-model="simpleOutputVar" filled dense autofocus>
<template v-slot:prepend>
<div class="variable-label">
{{ currentOutputs?.label || "输出变量名" }}
</div>
</template>
</q-input>
</div>
<div v-if="currentOutputs?.structure">
<div class="row items-center">
<q-badge color="primary" class="q-ma-sm q-pa-xs">详细输出</q-badge>
<div
v-if="Array.isArray(currentOutputs.structure)"
class="text-caption text-grey-5"
>
数组中第一个元素
</div>
</div>
<q-scroll-area
style="height: 200px"
:thumb-style="{
width: '2px',
}"
>
<div class="detail-output column q-col-gutter-sm q-px-sm">
<!-- 处理数组类型的structure -->
<template v-if="Array.isArray(currentOutputs.structure)">
<div
v-for="(output, key) in currentOutputs.structure[0]"
:key="key"
>
<!-- 如果是嵌套对象 -->
<div v-if="hasNestedFields(output)">
<BorderLabel
:label="output.label || key"
:model-value="false"
>
<div class="column q-col-gutter-sm">
<div
v-for="(subOutput, subKey) in getNestedFields(output)"
:key="subKey"
>
<div class="output-item">
<q-input
v-model="outputVars[`[0].${key}.${subKey}`]"
filled
dense
autofocus
class="col"
:placeholder="subOutput.placeholder"
>
<template v-slot:prepend>
<div class="variable-label">
{{ subOutput.label }}
</div>
</template>
</q-input>
</div>
</div>
</div>
</BorderLabel>
</div>
<!-- 如果是普通字段 -->
<div v-else class="output-item">
<q-input
v-model="outputVars[`[0].${key}`]"
filled
dense
class="col"
:placeholder="output.placeholder"
autofocus
>
<template v-slot:prepend>
<div class="variable-label">{{ output.label }}</div>
</template>
</q-input>
</div>
</div>
</template>
<!-- 处理对象类型的structure -->
<template v-else>
<div
v-for="(output, key) in currentOutputs?.structure"
:key="key"
>
<!-- 如果是嵌套对象 -->
<div v-if="hasNestedFields(output)">
<BorderLabel
:label="output.label || key"
:model-value="false"
>
<div class="column q-col-gutter-sm">
<div
v-for="(subOutput, subKey) in getNestedFields(output)"
:key="subKey"
>
<div class="output-item">
<q-input
v-model="outputVars[`${key}.${subKey}`]"
filled
dense
autofocus
class="col"
:placeholder="subOutput.placeholder"
>
<template v-slot:prepend>
<div class="variable-label">
{{ subOutput.label }}
</div>
</template>
</q-input>
</div>
</div>
</div>
</BorderLabel>
</div>
<!-- 如果是普通字段 -->
<div v-else class="output-item">
<q-input
v-model="outputVars[key]"
filled
dense
class="col"
:placeholder="output.placeholder"
autofocus
>
<template v-slot:prepend>
<div class="variable-label">{{ output.label }}</div>
</template>
</q-input>
</div>
</div>
</template>
</div>
</q-scroll-area>
</div>
<div v-if="!!asyncMode">
<q-badge color="primary" class="q-ma-sm q-pa-xs">运行模式</q-badge>
<div class="row q-col-gutter-sm q-px-sm">
<q-select
v-model="asyncMode"
:options="asyncModeOptions"
filled
dense
autofocus
emit-value
map-options
class="col"
<!-- 完整结果部分 -->
<SectionBlock title="完整结果">
<OutputField
v-model="simpleOutputVar"
:label="currentOutputs?.label || '输出变量名'"
autofocus
class="q-px-sm"
/>
</SectionBlock>
<!-- 详细输出部分 -->
<template v-if="currentOutputs?.structure">
<SectionBlock
title="详细输出"
:subtitle="
Array.isArray(currentOutputs.structure) ? '数组中第一个元素' : ''
"
>
<q-scroll-area
style="height: 200px"
:thumb-style="{
width: '2px',
}"
>
</q-select>
<q-input
v-model="callbackFunc"
filled
dense
autofocus
class="col-8"
v-if="asyncMode === 'then'"
placeholder="新函数名则自动创建"
>
<template v-slot:prepend>
<div class="variable-label">回调函数</div>
<div class="detail-output column q-col-gutter-sm q-px-sm">
<template v-if="Array.isArray(currentOutputs.structure)">
<template
v-for="(output, key) in currentOutputs.structure[0]"
:key="key"
>
<OutputStructure
:output="output"
:output-key="key"
:is-array="true"
v-model="outputVars"
/>
</template>
</template>
<template v-else>
<template
v-for="(output, key) in currentOutputs?.structure"
:key="key"
>
<OutputStructure
:output="output"
:output-key="key"
v-model="outputVars"
/>
</template>
</template>
</div>
</q-scroll-area>
</SectionBlock>
</template>
<!-- 运行模式部分 -->
<template v-if="!!asyncMode">
<SectionBlock title="运行模式">
<div class="row q-col-gutter-sm q-px-sm">
<OutputField
v-model="asyncMode"
class="col"
:options="asyncModeOptions"
/>
<template v-if="asyncMode === 'then'">
<OutputField
v-model="callbackFunc"
label="回调函数"
placeholder="新函数名则自动创建"
class="col-8"
/>
</template>
</q-input>
</div>
</div>
</div>
</SectionBlock>
</template>
<div class="row justify-end q-px-sm q-py-sm">
<q-btn flat label="取消" color="primary" v-close-popup />
@ -183,12 +91,17 @@
<script>
import { defineComponent } from "vue";
import BorderLabel from "components/composer/common/BorderLabel.vue";
import { validateVariableName } from "js/common/variableValidator";
import OutputField from "./output/OutputField.vue";
import SectionBlock from "./output/SectionBlock.vue";
import OutputStructure from "./output/OutputStructure.vue";
export default defineComponent({
name: "OutputEditor",
components: {
BorderLabel,
OutputField,
SectionBlock,
OutputStructure,
},
props: {
modelValue: {
@ -263,39 +176,6 @@ export default defineComponent({
},
},
methods: {
hasNestedFields(output) {
if (!output) return false;
return Object.keys(output).some(
(key) => key !== "label" && key !== "placeholder"
);
},
/**
* 只处理一层嵌套手动在配置文件中控制outputs结构不要太复杂
* 第二层嵌套只嵌套对象不嵌套数组
* 最复杂的情况
* outputs: {
* label: "测试",
* structure: [
* {
* position: { label: "位置", {
* x: { label: "X坐标" },
* y: { label: "Y坐标" }
* }
* }
* ]
* }
*
*
*/
getNestedFields(output) {
const fields = {};
Object.entries(output).forEach(([key, value]) => {
if (key !== "label" && key !== "placeholder") {
fields[key] = value;
}
});
return fields;
},
initOutputVars(outputVariable) {
//
if (!outputVariable) return;
@ -338,6 +218,25 @@ export default defineComponent({
}
}
//
const varNames = [
outputVariable.name,
...Object.keys(outputVariable.details || {}),
result.callbackFunc,
].filter(Boolean);
const invalidVars = varNames.filter((name) => {
return !validateVariableName(name).isValid;
});
if (invalidVars.length > 0) {
quickcommand.showMessageBox(
`变量名/函数名 ${invalidVars.join(", ")} 包含无效字符,请修改`,
"error"
);
return;
}
this.$emit("confirm", result);
this.isOpen = false;
},
@ -349,44 +248,4 @@ export default defineComponent({
.output-editor {
width: 450px;
}
.output-item {
border-radius: 8px;
transition: all 0.3s ease;
}
.output-item:hover {
background: rgba(0, 0, 0, 0.02);
}
.variable-label {
font-size: 12px;
border-radius: 4px;
padding-right: 10px;
text-align: center;
}
.body--dark .output-item:hover {
background: rgba(255, 255, 255, 0.02);
}
.output-editor :deep(.q-field--filled .q-field__control),
.output-editor :deep(.q-field--filled .q-field__control > *),
.output-editor :deep(.q-field--filled .q-field__native) {
max-height: 36px;
min-height: 36px;
border-radius: 5px;
font-size: 12px;
}
/* 去除filled输入框边框 */
.output-editor :deep(.q-field__control:before) {
border: none;
}
/* 去除filled输入框下划线 */
.output-editor :deep(.q-field__control:after) {
height: 0;
border-bottom: none;
}
</style>

View File

@ -0,0 +1,86 @@
<template>
<q-input
v-if="!options"
:model-value="modelValue"
@update:model-value="$emit('update:modelValue', $event)"
filled
dense
class="output-field"
:placeholder="placeholder"
:autofocus="autofocus"
>
<template v-slot:prepend>
<div class="variable-label">{{ label }}</div>
</template>
</q-input>
<q-select
v-else
:model-value="modelValue"
:options="options"
filled
dense
emit-value
map-options
class="output-field"
@update:model-value="$emit('update:modelValue', $event)"
/>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "OutputField",
props: {
modelValue: {
type: String,
default: "",
},
label: {
type: String,
},
placeholder: {
type: String,
default: "",
},
autofocus: {
type: Boolean,
default: false,
},
options: {
type: null,
default: null,
},
},
emits: ["update:modelValue"],
});
</script>
<style scoped>
.variable-label {
font-size: 12px;
border-radius: 4px;
padding-right: 10px;
text-align: center;
}
.output-field :deep(.q-field__control),
.output-field :deep(.q-field__control > *),
.output-field :deep(.q-field__native) {
max-height: 36px;
min-height: 36px;
border-radius: 5px;
font-size: 12px;
}
/* 去除filled输入框边框 */
.output-field :deep(.q-field__control:before) {
border: none;
}
/* 去除filled输入框下划线 */
.output-field :deep(.q-field__control:after) {
height: 0;
border-bottom: none;
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<div>
<!-- 如果是嵌套对象 -->
<div v-if="hasNestedFields">
<BorderLabel :label="output.label || outputKey" :model-value="false">
<div class="column q-col-gutter-sm">
<div v-for="(subOutput, subKey) in getNestedFields" :key="subKey">
<div class="output-item">
<OutputField
:model-value="modelValue[getFieldPath(subKey)]"
@update:model-value="updateField(subKey, $event)"
:label="subOutput.label"
:placeholder="subOutput.placeholder"
autofocus
/>
</div>
</div>
</div>
</BorderLabel>
</div>
<!-- 如果是普通字段 -->
<div v-else class="output-item">
<OutputField
:model-value="modelValue[getFieldPath()]"
@update:model-value="updateField('', $event)"
:label="output.label"
:placeholder="output.placeholder"
autofocus
/>
</div>
</div>
</template>
<script>
/**
* 只处理一层嵌套手动在配置文件中控制outputs结构不要太复杂
* 第二层嵌套只嵌套对象不嵌套数组
* 最复杂的情况
* outputs: {
* label: "测试",
* structure: [
* {
* position: { label: "位置", {
* x: { label: "X坐标" },
* y: { label: "Y坐标" }
* }
* }
* ]
* }
*
*
*/
import { defineComponent } from "vue";
import BorderLabel from "components/composer/common/BorderLabel.vue";
import OutputField from "./OutputField.vue";
export default defineComponent({
name: "OutputStructure",
components: {
BorderLabel,
OutputField,
},
props: {
output: {
type: Object,
required: true,
},
outputKey: {
type: String,
required: true,
},
isArray: {
type: Boolean,
default: false,
},
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ["update:modelValue"],
computed: {
hasNestedFields() {
if (!this.output) return false;
return Object.keys(this.output).some(
(key) => key !== "label" && key !== "placeholder"
);
},
getNestedFields() {
const fields = {};
Object.entries(this.output).forEach(([key, value]) => {
if (key !== "label" && key !== "placeholder") {
fields[key] = value;
}
});
return fields;
},
},
methods: {
getFieldPath(subKey = "") {
const base = this.isArray ? `[0].${this.outputKey}` : this.outputKey;
return subKey ? `${base}.${subKey}` : base;
},
updateField(subKey, value) {
const path = this.getFieldPath(subKey);
const newValue = { ...this.modelValue };
newValue[path] = value;
this.$emit("update:modelValue", newValue);
},
},
});
</script>
<style scoped>
.output-item {
border-radius: 8px;
transition: all 0.3s ease;
}
.output-item:hover {
background: rgba(0, 0, 0, 0.02);
}
.body--dark .output-item:hover {
background: rgba(255, 255, 255, 0.02);
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<div>
<div class="row items-center">
<q-badge color="primary" class="q-ma-sm q-pa-xs">{{ title }}</q-badge>
<div v-if="subtitle" class="text-caption text-grey-5">
{{ subtitle }}
</div>
</div>
<slot></slot>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "SectionBlock",
props: {
title: {
type: String,
required: true,
},
subtitle: {
type: String,
default: "",
},
},
});
</script>

View File

@ -35,6 +35,7 @@
dense
borderless
class="var-input"
@blur="validateVariable(localFlow)"
>
<template #prepend>
<div class="var-label">标识</div>
@ -76,8 +77,8 @@
dense
borderless
class="var-input"
@blur="validateVariable(variable, 'param')"
@keydown.enter="validateVariable(variable, 'param')"
@blur="validateVariable(variable)"
@keydown.enter="validateVariable(variable)"
/>
<q-btn
flat
@ -125,8 +126,8 @@
borderless
class="var-input"
placeholder="变量名"
@blur="validateVariable(variable, 'var')"
@keydown.enter="validateVariable(variable, 'var')"
@blur="validateVariable(variable)"
@keydown.enter="validateVariable(variable)"
/>
<q-separator vertical />
<q-input
@ -174,6 +175,7 @@
<script>
import { defineComponent } from "vue";
import { validateVariableName } from "js/common/variableValidator";
export default defineComponent({
name: "FlowManager",
@ -242,14 +244,14 @@ export default defineComponent({
this.localFlow.customVariables = newVars;
}
},
validateVariable(variable, type) {
if (!variable.name) {
const prefix = type === "param" ? "param_" : "var_";
const count = this.localFlow.customVariables.filter(
(v) => v.type === type
).length;
variable.name = prefix + count;
validateVariable(variable) {
if (validateVariableName(variable.name).isValid) {
return;
}
quickcommand.showMessageBox(
`变量/函数名 ${variable.name} 包含无效字符,请修改`,
"error"
);
},
},
});

View File

@ -10,6 +10,9 @@ export function generateCode(flow) {
return `${varName} = ${varValue};`;
}
usedVarNames[funcName].push(varName);
if (!varValue) {
return `let ${varName};`;
}
return `let ${varName} = ${varValue};`;
};