完善变量输入输出,新增预览窗口

This commit is contained in:
fofolee 2024-12-24 00:29:52 +08:00
parent e0eb337b1b
commit cdfb2b502f
12 changed files with 1224 additions and 148 deletions

View File

@ -0,0 +1,159 @@
<template>
<div class="code-preview">
<q-btn
flat
round
dense
icon="preview"
class="preview-btn"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
</q-btn>
<transition name="preview-fade">
<div v-if="isVisible" class="preview-popup">
<div class="preview-header">
<q-icon name="code" size="16px" class="q-mr-xs" />
<span>预览代码</span>
</div>
<pre class="preview-code"><code>{{ code }}</code></pre>
</div>
</transition>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "CodePreview",
props: {
generateCode: {
type: Function,
required: true,
},
},
data() {
return {
isVisible: false,
code: "",
previewTimer: null,
};
},
methods: {
handleMouseEnter() {
this.previewTimer = setTimeout(() => {
this.code = this.generateCode();
this.isVisible = true;
}, 200);
},
handleMouseLeave() {
clearTimeout(this.previewTimer);
this.isVisible = false;
},
},
beforeUnmount() {
clearTimeout(this.previewTimer);
},
});
</script>
<style scoped>
.code-preview {
position: relative;
}
.preview-btn {
color: var(--q-primary);
opacity: 0.7;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.preview-btn:hover {
opacity: 1;
transform: scale(1.1);
}
.preview-popup {
position: absolute;
top: 0;
right: calc(100% + 12px);
min-width: 300px;
max-width: 600px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 1000;
transform-origin: center right;
}
.preview-header {
padding: 10px 14px;
background: rgba(var(--q-primary-rgb), 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 8px 8px 0 0;
font-size: 13px;
font-weight: 500;
color: var(--q-primary);
display: flex;
align-items: center;
}
.preview-code {
margin: 0;
padding: 14px;
font-family: consolas, monaco, monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
/* 自定义滚动条 */
.preview-code::-webkit-scrollbar {
width: 6px;
}
.preview-code::-webkit-scrollbar-thumb {
background: var(--q-primary-opacity-20);
border-radius: 3px;
transition: background 0.3s;
}
.preview-code::-webkit-scrollbar-thumb:hover {
background: var(--q-primary-opacity-30);
}
/* 过渡动画 */
.preview-fade-enter-active,
.preview-fade-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.preview-fade-enter-from,
.preview-fade-leave-to {
opacity: 0;
transform: translateX(20px) scale(0.95);
}
/* 暗色模式适配 */
.body--dark .preview-popup {
background: #1d1d1d;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.body--dark .preview-header {
background: rgba(255, 255, 255, 0.03);
}
.body--dark .preview-code {
color: #e0e0e0;
}
</style>

View File

@ -1,10 +1,13 @@
<template>
<div class="command-composer">
<!-- 主体内容 -->
<div class="composer-body row no-wrap q-pa-sm">
<div class="composer-body row no-wrap q-pa-sm q-gutter-sm">
<!-- 左侧命令列表 -->
<div class="col-3 command-section">
<div class="text-subtitle1 q-pb-sm">可用命令</div>
<div class="section-header">
<q-icon name="list" size="20px" class="q-mr-sm text-primary" />
<span class="text-subtitle1">可用命令</span>
</div>
<q-scroll-area class="command-scroll">
<ComposerList
:commands="availableCommands"
@ -15,7 +18,12 @@
<!-- 右侧命令流程 -->
<div class="col q-pl-md command-section">
<div class="text-subtitle1 q-pb-sm">命令流程</div>
<div class="section-header">
<q-icon name="timeline" size="20px" class="q-mr-sm text-primary" />
<span class="text-subtitle1">命令流程</span>
<q-space />
<CodePreview :generate-code="generateCode" />
</div>
<q-scroll-area class="command-scroll">
<ComposerFlow v-model="commandFlow" @add-command="addCommand" />
</q-scroll-area>
@ -23,20 +31,23 @@
</div>
<!-- 固定底部 -->
<div class="composer-footer q-pa-sm q-gutter-sm row justify-end">
<q-btn label="取消" v-close-popup />
<q-btn color="primary" label="插入" @click="handleComposer('insert')" />
<q-btn color="primary" label="应用" @click="handleComposer('apply')" />
<q-btn color="positive" label="运行" @click="handleComposer('run')" />
<div class="composer-footer q-pa-sm row justify-end">
<div class="action-buttons q-gutter-sm">
<q-btn label="取消" v-close-popup />
<q-btn color="primary" label="插入" @click="handleComposer('insert')" />
<q-btn color="primary" label="应用" @click="handleComposer('apply')" />
<q-btn color="positive" label="运行" @click="handleComposer('run')" />
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { defineComponent, provide, ref } from "vue";
import ComposerList from "./ComposerList.vue";
import ComposerFlow from "./ComposerFlow.vue";
import { commandCategories } from "./composerConfig";
import CodePreview from "./CodePreview.vue";
import { commandCategories } from "js/composer/composerConfig";
// commandCategories
const availableCommands = commandCategories.reduce((commands, category) => {
@ -53,6 +64,36 @@ export default defineComponent({
components: {
ComposerList,
ComposerFlow,
CodePreview,
},
setup() {
const variables = ref([]);
const addVariable = (name, command) => {
if (!variables.value.find((v) => v.name === name)) {
variables.value.push({
name,
sourceCommand: command,
});
}
};
const removeVariable = (name) => {
const index = variables.value.findIndex((v) => v.name === name);
if (index !== -1) {
variables.value.splice(index, 1);
}
};
provide("composerVariables", variables);
provide("addVariable", addVariable);
provide("removeVariable", removeVariable);
return {
variables,
addVariable,
removeVariable,
};
},
data() {
return {
@ -68,31 +109,35 @@ export default defineComponent({
...action,
id: this.nextId++,
argv: "",
argvType: "string",
saveOutput: false,
useOutput: null,
outputVariable: null,
cmd: action.value || action.cmd,
value: action.value || action.cmd,
});
},
generateCode() {
let code = [];
let outputVars = new Map();
this.commandFlow.forEach((cmd, index) => {
this.commandFlow.forEach((cmd) => {
let line = "";
if (cmd.saveOutput) {
const varName = `output${index}`;
outputVars.set(index, varName);
line += `let ${varName} = `;
// TODO: string
console.log("Generating code for command:", cmd);
if (cmd.outputVariable) {
line += `let ${cmd.outputVariable} = `;
}
if (cmd.value === "ubrowser") {
line += cmd.argv;
} else if (cmd.useOutput !== null) {
const inputVar = outputVars.get(cmd.useOutput);
line += `${cmd.value}(${inputVar})`;
const outputVar = this.commandFlow[cmd.useOutput].outputVariable;
line += `${cmd.value}(${outputVar})`;
} else {
const argv =
cmd.value !== "quickcommand.sleep" ? `"${cmd.argv}"` : cmd.argv;
const needQuotes =
cmd.argvType === "string" && cmd.argvType !== "variable";
const argv = needQuotes ? `"${cmd.argv}"` : cmd.argv;
line += `${cmd.value}(${argv})`;
}
@ -131,6 +176,25 @@ export default defineComponent({
display: flex;
flex-direction: column;
height: 100%;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.body--dark .command-section {
background: #1d1d1d;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.section-header {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.body--dark .section-header {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.command-scroll {
@ -139,16 +203,18 @@ export default defineComponent({
}
.composer-footer {
border-top: 1px solid #e0e0e0;
border-top: 1px solid rgba(0, 0, 0, 0.05);
background: white;
}
.body--dark .composer-footer {
border-top: 1px solid #676666;
border-top-color: rgba(255, 255, 255, 0.1);
background: #1d1d1d;
}
/* 滚动美化 */
:deep(.q-scrollarea__thumb) {
width: 6px;
width: 2px;
opacity: 0.4;
transition: opacity 0.3s ease;
}
@ -157,7 +223,16 @@ export default defineComponent({
opacity: 0.8;
}
:deep(.q-scrollarea__content) {
padding-right: 8px;
/* 动画效果 */
.command-section {
transition: all 0.3s ease;
}
.command-section:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.body--dark .command-section:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
</style>

View File

@ -15,13 +15,52 @@
</div>
<div class="text-subtitle1">{{ command.label }}</div>
<q-space />
<!-- 输出开关 -->
<q-toggle
v-if="hasOutput"
v-model="saveOutputLocal"
label="保存输出"
<!-- 输出变量设置 -->
<div
class="output-section row items-center no-wrap"
v-if="command.saveOutput"
>
<q-input
:model-value="command.outputVariable"
@update:model-value="handleOutputVariableUpdate"
dense
outlined
placeholder="变量名"
class="variable-input"
style="width: 100px"
align="center"
>
</q-input>
</div>
<q-btn
:icon="saveOutputLocal ? 'data_object' : 'output'"
:label="saveOutputLocal ? '保存到变量' : '获取输出'"
flat
dense
/>
class="output-btn q-px-sm q-mr-sm"
size="sm"
@click="handleToggleOutput"
>
<q-tooltip>
<div class="text-body2">
{{
saveOutputLocal
? "当前命令的输出将保存到变量中"
: "点击将此命令的输出保存为变量以供后续使用"
}}
</div>
<div class="text-caption text-grey-5">
{{
saveOutputLocal
? "点击取消输出到变量"
: "保存后可在其他命令中使用此变量"
}}
</div>
</q-tooltip>
</q-btn>
<q-btn
flat
round
@ -29,65 +68,31 @@
icon="close"
@click="$emit('remove')"
size="sm"
/>
class="remove-btn"
>
<q-tooltip>移除此命令</q-tooltip>
</q-btn>
</div>
<!-- 参数输入 -->
<div class="row items-center">
<!-- 使用上一个命令的输出 -->
<template v-if="canUseOutput && availableOutputs.length > 0">
<q-select
v-model="useOutputLocal"
:options="availableOutputs"
dense
outlined
class="col"
emit-value
map-options
clearable
:label="placeholder"
@clear="handleClearOutput"
>
<template v-slot:prepend>
<q-icon name="input" />
</template>
<template v-slot:selected-item="scope">
<div class="row items-center">
<q-icon
name="output"
color="primary"
size="xs"
class="q-mr-xs"
/>
{{ scope.opt.label }}
</div>
</template>
</q-select>
</template>
<!-- 按键编辑器 -->
<template v-else-if="command.hasKeyRecorder">
<template v-if="command.hasKeyRecorder">
<KeyEditor v-model="argvLocal" class="col" />
</template>
<!-- UBrowser编辑器 -->
<template v-else-if="command.hasUBrowserEditor">
<UBrowserEditor
v-model="argvLocal"
class="col"
/>
<UBrowserEditor v-model="argvLocal" class="col" />
</template>
<!-- 普通参数输入 -->
<template v-else>
<q-input
<VariableInput
v-model="argvLocal"
dense
outlined
class="col"
:label="placeholder"
>
<template v-slot:prepend>
<q-icon name="text_fields" size="18px" />
</template>
</q-input>
class="col"
ref="variableInput"
@update:type="handleArgvTypeUpdate"
/>
</template>
</div>
</div>
@ -97,29 +102,23 @@
</template>
<script>
import { defineComponent } from "vue";
import { defineComponent, inject } from "vue";
import KeyEditor from "./KeyEditor.vue";
import UBrowserEditor from './ubrowser/UBrowserEditor.vue';
import UBrowserEditor from "./ubrowser/UBrowserEditor.vue";
import VariableInput from "./VariableInput.vue";
export default defineComponent({
name: "ComposerCard",
components: {
KeyEditor,
UBrowserEditor
UBrowserEditor,
VariableInput,
},
props: {
command: {
type: Object,
required: true,
},
hasOutput: {
type: Boolean,
default: false,
},
canUseOutput: {
type: Boolean,
default: false,
},
availableOutputs: {
type: Array,
default: () => [],
@ -138,7 +137,13 @@ export default defineComponent({
showKeyRecorder: false,
};
},
emits: ["remove", "toggle-output", "update:argv", "update:use-output"],
emits: [
"remove",
"toggle-output",
"update:argv",
"update:use-output",
"update:command",
],
computed: {
saveOutputLocal: {
get() {
@ -165,6 +170,15 @@ export default defineComponent({
},
},
},
setup() {
const addVariable = inject("addVariable");
const removeVariable = inject("removeVariable");
return {
addVariable,
removeVariable,
};
},
methods: {
handleClearOutput() {
this.$emit("update:use-output", null);
@ -177,6 +191,49 @@ export default defineComponent({
this.$emit("update:argv", matches[1]);
}
},
handleOutputVariableChange(value) {
if (this.command.outputVariable) {
this.removeVariable(this.command.outputVariable);
}
if (value) {
this.addVariable(value, this.command);
}
},
handleOutputVariableUpdate(value) {
//
const updatedCommand = {
...this.command,
outputVariable: value,
};
//
this.$emit("update:command", updatedCommand);
//
this.handleOutputVariableChange(value);
},
handleArgvTypeUpdate(type) {
console.log("Type updated in card:", type);
const updatedCommand = {
...this.command,
argvType: type,
};
this.$emit("update:command", updatedCommand);
},
handleToggleOutput() {
//
const updatedCommand = {
...this.command,
saveOutput: !this.command.saveOutput,
};
//
if (!updatedCommand.saveOutput && updatedCommand.outputVariable) {
this.removeVariable(updatedCommand.outputVariable);
updatedCommand.outputVariable = null;
}
//
this.$emit("update:command", updatedCommand);
},
},
mounted() {
this.$el.classList.add("composer-card-enter-from");
@ -218,8 +275,8 @@ export default defineComponent({
/* 拖拽动画 */
/* .composer-card:active { */
/* transform: scale(1.02); */
/* transition: transform 0.2s; */
/* transform: scale(1.02); */
/* transition: transform 0.2s; */
/* } */
.command-item {
@ -271,4 +328,105 @@ export default defineComponent({
.drag-handle:hover {
color: var(--q-primary);
}
/* 添加新的样式 */
.output-section {
max-width: 120px;
margin-right: 4px;
}
.output-section :deep(.q-field) {
background: rgba(var(--q-primary-rgb), 0.03);
border-radius: 4px;
}
/* 输出按钮样式优化 */
.output-btn {
font-size: 12px;
border-radius: 4px;
min-height: 28px;
padding: 0 8px;
border: 1px solid rgba(var(--q-primary-rgb), 0.1);
transition: all 0.3s ease;
}
.output-btn:hover {
background: rgba(var(--q-primary-rgb), 0.05);
}
.output-btn .q-icon {
font-size: 14px;
margin-right: 4px;
}
.output-btn.q-btn--active {
background: rgba(var(--q-primary-rgb), 0.1);
color: var(--q-primary);
}
/* 移除按钮样式 */
.remove-btn {
opacity: 0.5;
transition: all 0.3s ease;
font-size: 18px;
}
.remove-btn:hover {
opacity: 1;
color: var(--q-negative);
transform: scale(1.05);
}
/* 暗色模式适配 */
.body--dark .output-section :deep(.q-field) {
background: rgba(255, 255, 255, 0.03);
}
.body--dark .output-section :deep(.q-field--focused) {
background: #1d1d1d;
}
.body--dark .output-btn {
border-color: rgba(255, 255, 255, 0.1);
}
.body--dark .output-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
/* 输入框内部样式优化 */
.output-section :deep(.q-field__control) {
height: 28px;
min-height: 28px;
padding: 0 4px;
}
.output-section :deep(.q-field__marginal) {
height: 28px;
width: 24px;
min-width: 24px;
}
.output-section :deep(.q-field__native) {
padding: 0;
font-size: 12px;
min-height: 28px;
text-align: center;
}
/* Tooltip 样式优化 */
:deep(.q-tooltip) {
max-width: 300px;
padding: 8px 12px;
}
/* 优化图标样式 */
.output-section :deep(.q-icon) {
opacity: 0.8;
transition: opacity 0.3s ease;
}
.output-section :deep(.q-field--focused .q-icon) {
opacity: 1;
}
</style>

View File

@ -23,19 +23,20 @@
class="flow-item"
:class="{
'insert-before': dragIndex === index,
'insert-after': dragIndex === commands.length && index === commands.length - 1
'insert-after':
dragIndex === commands.length &&
index === commands.length - 1,
}"
>
<ComposerCard
:command="element"
:has-output="hasOutput(element)"
:can-use-output="canUseOutput(element, index)"
:available-outputs="getAvailableOutputs(index)"
:placeholder="getPlaceholder(element, index)"
@remove="removeCommand(index)"
@toggle-output="toggleSaveOutput(index)"
@update:argv="(val) => handleArgvChange(index, val)"
@update:use-output="(val) => handleUseOutputChange(index, val)"
@update:command="(val) => updateCommand(index, val)"
/>
</div>
</transition>
@ -56,7 +57,6 @@
import { defineComponent } from "vue";
import draggable from "vuedraggable";
import ComposerCard from "./ComposerCard.vue";
import { commandsWithOutput, commandsAcceptOutput } from "./composerConfig";
export default defineComponent({
name: "ComposerFlow",
@ -84,8 +84,8 @@ export default defineComponent({
data() {
return {
dragIndex: -1,
isDragging: false
}
isDragging: false,
};
},
methods: {
onDragStart() {
@ -100,7 +100,7 @@ export default defineComponent({
onDragOver(event) {
if (!this.isDragging) {
const rect = event.currentTarget.getBoundingClientRect();
const items = this.$el.querySelectorAll('.flow-item');
const items = this.$el.querySelectorAll(".flow-item");
const mouseY = event.clientY;
//
@ -135,11 +135,11 @@ export default defineComponent({
},
onDrop(event) {
const actionData = JSON.parse(event.dataTransfer.getData('action'));
const actionData = JSON.parse(event.dataTransfer.getData("action"));
const newCommand = {
...actionData,
id: Date.now(), // 使ID
argv: '',
argv: "",
saveOutput: false,
useOutput: null,
cmd: actionData.value || actionData.cmd,
@ -153,11 +153,11 @@ export default defineComponent({
newCommands.push(newCommand);
}
this.$emit('update:modelValue', newCommands);
this.$emit("update:modelValue", newCommands);
this.dragIndex = -1;
document.querySelectorAll('.dragging').forEach(el => {
el.classList.remove('dragging');
document.querySelectorAll(".dragging").forEach((el) => {
el.classList.remove("dragging");
});
},
removeCommand(index) {
@ -165,15 +165,6 @@ export default defineComponent({
newCommands.splice(index, 1);
this.$emit("update:modelValue", newCommands);
},
hasOutput(command) {
return commandsWithOutput[command.value] || false;
},
canUseOutput(command, index) {
return (
commandsAcceptOutput[command.value] &&
this.getAvailableOutputs(index).length > 0
);
},
getAvailableOutputs(currentIndex) {
return this.commands
.slice(0, currentIndex)
@ -198,7 +189,10 @@ export default defineComponent({
},
handleArgvChange(index, value) {
const newCommands = [...this.commands];
newCommands[index].argv = value;
newCommands[index] = {
...newCommands[index],
argv: value,
};
this.$emit("update:modelValue", newCommands);
},
handleUseOutputChange(index, value) {
@ -215,6 +209,15 @@ export default defineComponent({
}
return element.desc;
},
updateCommand(index, updatedCommand) {
console.log("Command updated in flow:", updatedCommand);
const newCommands = [...this.commands];
newCommands[index] = {
...newCommands[index],
...updatedCommand,
};
this.$emit("update:modelValue", newCommands);
},
},
});
</script>
@ -227,7 +230,7 @@ export default defineComponent({
.command-flow-container {
padding: 8px;
background-color: #fafafa;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 4px;
transition: all 0.3s ease;
height: 100%;
@ -237,7 +240,7 @@ export default defineComponent({
}
.body--dark .command-flow-container {
background-color: #303132;
background-color: rgba(32, 32, 32, 0.8);
}
.flow-list {
@ -286,7 +289,7 @@ export default defineComponent({
border-color: #676666;
}
/* 滑动淡出画 */
/* 滑动淡出<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>画 */
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.3s ease;
@ -305,7 +308,7 @@ export default defineComponent({
/* 拖拽指示器基础样式 */
.flow-item::before,
.flow-item::after {
content: '';
content: "";
position: absolute;
left: 12px;
right: 12px;
@ -338,18 +341,14 @@ export default defineComponent({
.flow-item.insert-before::before {
opacity: 1;
transform: scaleX(1) translateY(0);
box-shadow:
0 0 10px rgba(0, 0, 0, 0.03),
0 0 4px rgba(0, 0, 0, 0.05);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.03), 0 0 4px rgba(0, 0, 0, 0.05);
}
/* 激活状态 - 插入到最后 */
.flow-item.insert-after::after {
opacity: 1;
transform: scaleX(1) translateY(0);
box-shadow:
0 0 10px rgba(0, 0, 0, 0.03),
0 0 4px rgba(0, 0, 0, 0.05);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.03), 0 0 4px rgba(0, 0, 0, 0.05);
}
/* 拖拽时的卡片效果 */
@ -382,8 +381,7 @@ export default defineComponent({
rgba(255, 255, 255, 0.08) 90%,
transparent
);
box-shadow:
0 0 10px rgba(255, 255, 255, 0.03),
box-shadow: 0 0 10px rgba(255, 255, 255, 0.03),
0 0 4px rgba(255, 255, 255, 0.05);
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="composer-list">
<q-list bordered separator class="rounded-borders">
<q-list separator class="rounded-borders">
<template v-for="category in commandCategories" :key="category.label">
<q-item-label header class="q-py-sm">
<div class="row items-center">
@ -37,7 +37,7 @@
<script>
import { defineComponent } from "vue";
import { commandCategories } from "./composerConfig";
import { commandCategories } from "js/composer/composerConfig";
export default defineComponent({
name: "ComposerList",

View File

@ -0,0 +1,448 @@
<template>
<div
class="logic-flow-card"
@dragover.stop.prevent
@drop.stop.prevent="onCardDrop"
>
<q-card class="logic-container">
<q-card-section class="q-pa-sm">
<!-- 标题栏 -->
<div class="row items-center q-mb-sm">
<div class="drag-handle cursor-move q-mr-sm">
<q-icon name="drag_indicator" size="18px" class="text-grey-6" />
</div>
<div class="text-subtitle1">{{ command.label }}</div>
<q-space />
<q-btn
flat
round
dense
icon="close"
@click="$emit('remove')"
size="sm"
/>
</div>
<!-- 条件输入 -->
<q-input
v-model="condition"
dense
outlined
label="条件表达式"
class="q-mb-md"
>
<template v-slot:prepend>
<q-icon name="code" size="18px" />
</template>
</q-input>
<!-- 分支流程 -->
<div class="branch-flows q-gutter-y-md">
<!-- IF 分支 -->
<div class="branch-container">
<div class="branch-header q-mb-sm">IF 分支</div>
<div
class="branch-drop-area"
:class="{ 'is-active': isIfBranchActive }"
@dragover.stop.prevent="onBranchDragOver('if', $event)"
@drop.stop.prevent="onBranchDrop('if', $event)"
@dragleave.prevent="onBranchDragLeave('if')"
>
<!-- 拖拽指示器 -->
<div v-if="isIfBranchActive" class="branch-indicator">
<q-icon name="add" size="24px" class="text-primary" />
<div class="text-caption text-primary q-mt-xs">
添加到 IF 分支
</div>
</div>
<draggable
v-model="ifCommands"
group="commands"
item-key="id"
handle=".drag-handle"
:animation="200"
>
<template #item="{ element, index }">
<ComposerCard
:command="element"
:available-outputs="getAvailableOutputs(index, 'if')"
:placeholder="getPlaceholder(element, index, 'if')"
@remove="removeCommand('if', index)"
@toggle-output="toggleSaveOutput('if', index)"
@update:argv="(val) => handleArgvChange('if', index, val)"
@update:use-output="
(val) => handleUseOutputChange('if', index, val)
"
/>
</template>
</draggable>
</div>
</div>
<!-- ELSE 分支 -->
<div class="branch-container">
<div class="branch-header q-mb-sm">ELSE 分支</div>
<div
class="branch-drop-area"
:class="{ 'is-active': isElseBranchActive }"
@dragover.stop.prevent="onBranchDragOver('else', $event)"
@drop.stop.prevent="onBranchDrop('else', $event)"
@dragleave.prevent="onBranchDragLeave('else')"
>
<!-- 拖拽指示器 -->
<div v-if="isElseBranchActive" class="branch-indicator">
<q-icon name="add" size="24px" class="text-primary" />
<div class="text-caption text-primary q-mt-xs">
添加到 ELSE 分支
</div>
</div>
<draggable
v-model="elseCommands"
group="commands"
item-key="id"
handle=".drag-handle"
:animation="200"
>
<template #item="{ element, index }">
<ComposerCard
:command="element"
:available-outputs="getAvailableOutputs(index, 'else')"
:placeholder="getPlaceholder(element, index, 'else')"
@remove="removeCommand('else', index)"
@toggle-output="toggleSaveOutput('else', index)"
@update:argv="(val) => handleArgvChange('else', index, val)"
@update:use-output="
(val) => handleUseOutputChange('else', index, val)
"
/>
</template>
</draggable>
</div>
</div>
</div>
</q-card-section>
</q-card>
</div>
</template>
<script>
import { defineComponent } from "vue";
import draggable from "vuedraggable";
import ComposerCard from "./ComposerCard.vue";
export default defineComponent({
name: "LogicFlowCard",
components: {
draggable,
ComposerCard,
},
props: {
command: {
type: Object,
required: true,
},
modelValue: {
type: Object,
default: () => ({
condition: "",
if: [],
else: [],
}),
},
},
emits: ["update:modelValue", "remove"],
data() {
return {
isIfBranchActive: false,
isElseBranchActive: false,
};
},
computed: {
condition: {
get() {
return this.modelValue.condition;
},
set(value) {
this.$emit("update:modelValue", {
...this.modelValue,
condition: value,
});
},
},
ifCommands: {
get() {
return this.modelValue.if;
},
set(value) {
this.$emit("update:modelValue", {
...this.modelValue,
if: value,
});
},
},
elseCommands: {
get() {
return this.modelValue.else;
},
set(value) {
this.$emit("update:modelValue", {
...this.modelValue,
else: value,
});
},
},
},
methods: {
//
onBranchDragOver(branch, event) {
//
try {
const types = event.dataTransfer.types;
if (!types.includes("application/json")) {
return;
}
} catch (e) {
return;
}
event.stopPropagation();
if (branch === "if") {
this.isIfBranchActive = true;
} else {
this.isElseBranchActive = true;
}
},
onBranchDragLeave(branch) {
if (branch === "if") {
this.isIfBranchActive = false;
} else {
this.isElseBranchActive = false;
}
},
onBranchDrop(branch, event) {
event.stopPropagation();
let actionData;
try {
const data = event.dataTransfer.getData("application/json");
if (!data) {
console.warn("No valid drag data found");
return;
}
actionData = JSON.parse(data);
} catch (e) {
console.error("Failed to parse drag data:", e);
return;
}
//
if (!actionData || !actionData.value) {
console.warn("Invalid command data");
return;
}
const newCommand = {
...actionData,
id: Date.now(),
argv: "",
saveOutput: false,
useOutput: null,
cmd: actionData.value || actionData.cmd,
value: actionData.value || actionData.cmd,
};
const commands =
branch === "if" ? [...this.ifCommands] : [...this.elseCommands];
commands.push(newCommand);
this.$emit("update:modelValue", {
...this.modelValue,
[branch]: commands,
});
//
if (branch === "if") {
this.isIfBranchActive = false;
} else {
this.isElseBranchActive = false;
}
//
document.querySelectorAll(".dragging").forEach((el) => {
el.classList.remove("dragging");
});
},
// -
onCardDrop(event) {
//
return;
},
//
removeCommand(branch, index) {
const commands =
branch === "if" ? [...this.ifCommands] : [...this.elseCommands];
commands.splice(index, 1);
this.$emit("update:modelValue", {
...this.modelValue,
[branch]: commands,
});
},
getAvailableOutputs(currentIndex, branch) {
//
const commands = branch === "if" ? this.ifCommands : this.elseCommands;
return commands
.slice(0, currentIndex)
.map((cmd, index) => ({
label: `${cmd.label} 的输出`,
value: index,
disable: !cmd.saveOutput,
}))
.filter((item) => !item.disable);
},
toggleSaveOutput(branch, index) {
const commands =
branch === "if" ? [...this.ifCommands] : [...this.elseCommands];
commands[index].saveOutput = !commands[index].saveOutput;
// 使
if (!commands[index].saveOutput) {
commands.forEach((cmd, i) => {
if (i > index && cmd.useOutput === index) {
cmd.useOutput = null;
}
});
}
this.$emit("update:modelValue", {
...this.modelValue,
[branch]: commands,
});
},
handleArgvChange(branch, index, value) {
const commands =
branch === "if" ? [...this.ifCommands] : [...this.elseCommands];
commands[index].argv = value;
this.$emit("update:modelValue", {
...this.modelValue,
[branch]: commands,
});
},
handleUseOutputChange(branch, index, value) {
const commands =
branch === "if" ? [...this.ifCommands] : [...this.elseCommands];
commands[index].useOutput = value;
if (value !== null) {
commands[index].argv = "";
}
this.$emit("update:modelValue", {
...this.modelValue,
[branch]: commands,
});
},
getPlaceholder(element, index, branch) {
if (element.useOutput !== null) {
const commands = branch === "if" ? this.ifCommands : this.elseCommands;
return `使用 ${commands[element.useOutput].label} 的输出`;
}
return element.desc;
},
},
});
</script>
<style scoped>
.logic-flow-card {
margin-bottom: 8px;
}
.logic-container {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.branch-container {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
padding: 12px;
}
.branch-header {
font-weight: 500;
color: var(--q-primary);
font-size: 14px;
}
.branch-drop-area {
min-height: 50px;
border: 1px dashed rgba(0, 0, 0, 0.1);
border-radius: 4px;
transition: all 0.3s ease;
padding: 4px;
position: relative;
}
.branch-drop-area.is-active {
border-color: var(--q-primary);
background: rgba(var(--q-primary-rgb), 0.03);
}
/* 拖拽指示器样式 */
.branch-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 1;
background: rgba(255, 255, 255, 0.9);
padding: 12px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
animation: indicator-fade-in 0.3s ease;
}
@keyframes indicator-fade-in {
from {
opacity: 0;
transform: translate(-50%, -40%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
.body--dark .logic-container {
background: rgba(34, 34, 34, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.body--dark .branch-container {
background: rgba(255, 255, 255, 0.03);
}
.body--dark .branch-drop-area {
border-color: rgba(255, 255, 255, 0.1);
}
.body--dark .branch-indicator {
background: rgba(0, 0, 0, 0.7);
}
</style>

View File

@ -0,0 +1,255 @@
<template>
<q-input
v-model="inputValue"
dense
outlined
:label="label"
class="variable-input"
>
<template v-slot:prepend>
<q-btn
flat
dense
round
:icon="isString ? 'format_quote' : 'format_quote'"
size="sm"
:class="{
'text-primary': isString,
'text-grey-6': !isString,
}"
class="string-toggle"
@click="toggleStringType"
>
<q-tooltip>{{
isString
? "当前类型是:字符串,点击切换"
: "当前类型是:变量、数字、表达式等,点击切换"
}}</q-tooltip>
</q-btn>
</template>
<template v-slot:append>
<q-btn
v-if="hasSelectedVariable"
flat
dense
round
icon="close"
size="sm"
class="clear-btn q-mr-xs"
@click="clearVariable"
>
<q-tooltip>清除选中的变量</q-tooltip>
</q-btn>
<q-btn-dropdown
flat
dense
:icon="hasSelectedVariable ? 'data_object' : 'functions'"
:class="{
'text-primary': hasSelectedVariable,
'text-grey-6': !hasSelectedVariable,
}"
class="variable-dropdown"
size="sm"
>
<q-list class="variable-list">
<q-item-label header class="text-subtitle2">
<q-icon name="functions" size="16px" class="q-mr-sm" />
选择变量
</q-item-label>
<q-separator v-if="variables.length > 0" />
<template v-if="variables.length > 0">
<q-item
v-for="variable in variables"
:key="variable.name"
clickable
v-close-popup
@click="insertVariable(variable)"
class="variable-item"
>
<q-item-section>
<q-item-label class="variable-name">
{{ variable.name }}
</q-item-label>
<q-item-label caption class="variable-source">
来自: {{ variable.sourceCommand.label }}
</q-item-label>
</q-item-section>
</q-item>
</template>
<q-item v-else class="text-grey-6">
<q-item-section class="text-center">
<q-item-label>暂无可用变量</q-item-label>
<q-item-label caption>
点击命令卡片的获取输出按钮输入保存的变量名
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</template>
</q-input>
</template>
<script>
import { defineComponent, inject, computed } from "vue";
export default defineComponent({
name: "VariableInput",
props: {
modelValue: String,
label: String,
},
emits: ["update:modelValue", "update:type"],
setup() {
const variables = inject("composerVariables", []);
return { variables };
},
data() {
return {
isString: true,
selectedVariable: null,
};
},
computed: {
inputValue: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
},
},
hasSelectedVariable() {
return this.selectedVariable !== null;
},
},
methods: {
toggleStringType() {
if (!this.hasSelectedVariable) {
this.isString = !this.isString;
this.$emit("update:type", this.isString ? "string" : "number");
}
},
insertVariable(variable) {
this.selectedVariable = variable;
this.isString = false;
this.$emit("update:type", "variable");
this.$emit("update:modelValue", variable.name);
},
clearVariable() {
this.selectedVariable = null;
this.isString = true;
this.$emit("update:type", "string");
this.$emit("update:modelValue", "");
},
},
watch: {
modelValue(newVal) {
if (this.selectedVariable && newVal !== this.selectedVariable.name) {
this.selectedVariable = null;
this.isString = true;
this.$emit("update:type", "string");
}
},
},
mounted() {
this.$emit("update:type", "string");
},
});
</script>
<style scoped>
.variable-input {
width: 100%;
}
.variable-input :deep(.q-field__control) {
padding-left: 8px;
padding-right: 8px;
}
/* 字符串切换按钮样式 */
.string-toggle {
min-width: 24px;
padding: 4px;
opacity: 0.8;
transition: all 0.3s ease;
}
.string-toggle:hover {
opacity: 1;
transform: scale(1.05);
}
/* 变量下拉框样式 */
.variable-dropdown {
min-width: 32px;
padding: 4px;
opacity: 0.8;
transition: all 0.3s ease;
margin-left: 4px;
}
.variable-dropdown:hover {
opacity: 1;
transform: scale(1.05);
}
/* 变量列表样式 */
.variable-list {
min-width: 200px;
padding: 4px;
}
.variable-item {
border-radius: 4px;
margin: 2px 0;
transition: all 0.3s ease;
}
.variable-item:hover {
background: rgba(var(--q-primary-rgb), 0.1);
}
.variable-name {
font-size: 13px;
font-weight: 500;
}
.variable-source {
font-size: 12px;
opacity: 0.7;
}
/* 暗色模式适配 */
.body--dark .variable-item:hover {
background: rgba(255, 255, 255, 0.1);
}
/* 清空按钮样式 */
.clear-btn {
opacity: 0.6;
transition: all 0.3s ease;
}
.clear-btn:hover {
opacity: 1;
transform: scale(1.1);
color: var(--q-negative);
}
</style>

View File

@ -66,8 +66,8 @@ import { defineComponent } from "vue";
import UBrowserBasic from "./UBrowserBasic.vue";
import UBrowserOperations from "./UBrowserOperations.vue";
import UBrowserRun from "./UBrowserRun.vue";
import { defaultUBrowserConfigs } from "./ubrowserConfig";
import { generateUBrowserCode } from "./generateUBrowserCode";
import { defaultUBrowserConfigs } from "js/composer/ubrowserConfig";
import { generateUBrowserCode } from "js/composer/generateUBrowserCode";
export default defineComponent({
name: "UBrowserEditor",

View File

@ -92,7 +92,7 @@
<script>
import { defineComponent } from "vue";
import { ubrowserOperationConfigs } from "../composerConfig";
import { ubrowserOperationConfigs } from "js/composer/composerConfig";
import UBrowserOperation from "./operations/UBrowserOperation.vue";
export default defineComponent({

View File

@ -1,7 +1,7 @@
export {
ubrowserOperationConfigs,
defaultUBrowserConfigs,
} from "./ubrowser/ubrowserConfig";
} from "./ubrowserConfig";
// 定义命令图标映射
export const commandIcons = {
@ -125,20 +125,3 @@ export const commandCategories = [
],
},
];
// 定义哪些命令可以产生输出
export const commandsWithOutput = {
system: true,
open: true,
locate: true,
copyTo: true,
ubrowser: true,
};
// 定义哪些命令可以接收输出
export const commandsAcceptOutput = {
message: true,
alert: true,
send: true,
copyTo: true,
};

View File

@ -4,7 +4,7 @@
* @param {Array} selectedActions 已选择的操作列表
* @returns {string} 生成的代码
*/
import { defaultUBrowserConfigs } from "./ubrowserConfig";
import { defaultUBrowserConfigs } from "js/composer/ubrowserConfig";
export function generateUBrowserCode(configs, selectedActions) {
let code = "utools.ubrowser";