mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-28 11:52:46 +08:00
完善变量输入输出,新增预览窗口
This commit is contained in:
parent
e0eb337b1b
commit
cdfb2b502f
159
src/components/editor/composer/CodePreview.vue
Normal file
159
src/components/editor/composer/CodePreview.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
448
src/components/editor/composer/LogicFlowCard.vue
Normal file
448
src/components/editor/composer/LogicFlowCard.vue
Normal 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>
|
255
src/components/editor/composer/VariableInput.vue
Normal file
255
src/components/editor/composer/VariableInput.vue
Normal 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>
|
@ -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",
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
};
|
@ -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";
|
Loading…
x
Reference in New Issue
Block a user