完善条件判断的删除添加逻辑,添加编排卡片折叠功能

This commit is contained in:
fofolee 2025-01-01 23:07:47 +08:00
parent f3a01a1ba9
commit 37ebebc5ac
7 changed files with 727 additions and 358 deletions

View File

@ -1,12 +1,26 @@
<template> <template>
<div class="composer-card" :class="{ 'can-drop': canDrop }" v-bind="$attrs"> <div
class="composer-card"
:class="{
collapsed: isCollapsed && !command.isControlFlow,
'drag-handle': !isLastCommandInChain,
'no-animation': isClickingControl,
}"
v-bind="$attrs"
@mousedown="handleMouseDown"
@mouseup="handleMouseUp"
>
<q-card class="command-item"> <q-card class="command-item">
<q-card-section class="q-pa-sm"> <q-card-section
class="card-section"
:class="{ collapsed: isCollapsed || command.isControlFlow }"
>
<CommandHead <CommandHead
:command="command" :command="command"
:is-control-flow="command.isControlFlow" :is-collapsed="isCollapsed"
@update:outputVariable="handleOutputVariableUpdate" @update:outputVariable="handleOutputVariableUpdate"
@toggle-output="handleToggleOutput" @toggle-output="handleToggleOutput"
@toggle-collapse="handleToggleCollapse"
@run="runCommand" @run="runCommand"
@remove="$emit('remove')" @remove="$emit('remove')"
> >
@ -29,21 +43,27 @@
</CommandHead> </CommandHead>
<!-- 非控制流程组件的参数输入 --> <!-- 非控制流程组件的参数输入 -->
<div v-if="!command.isControlFlow" class="row items-center q-mt-sm"> <div
<component v-if="!command.isControlFlow"
v-if="!!command.component" class="command-content-wrapper"
:is="command.component" :class="{ collapsed: isCollapsed }"
v-model="argvLocal" >
:command="command" <div class="command-content">
class="col" <component
v-bind="command.componentProps || {}" v-if="!!command.component"
/> :is="command.component"
<MultiParamInput v-model="argvLocal"
v-else :command="command"
v-model="argvLocal" class="col"
:command="command" v-bind="command.componentProps || {}"
class="col" />
/> <MultiParamInput
v-else
v-model="argvLocal"
:command="command"
class="col"
/>
</div>
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -101,7 +121,7 @@ export default defineComponent({
type: String, type: String,
default: "", default: "",
}, },
canDrop: { isDragging: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
@ -109,8 +129,20 @@ export default defineComponent({
data() { data() {
return { return {
showKeyRecorder: false, showKeyRecorder: false,
isCollapsed: false,
isClickingControl: false,
}; };
}, },
watch: {
"command.isCollapsed": {
immediate: true,
handler(val) {
if (val !== undefined) {
this.isCollapsed = val;
}
},
},
},
emits: [ emits: [
"remove", "remove",
"toggle-output", "toggle-output",
@ -118,6 +150,7 @@ export default defineComponent({
"update:command", "update:command",
"run", "run",
"addBranch", "addBranch",
"toggle-collapse",
], ],
computed: { computed: {
saveOutputLocal: { saveOutputLocal: {
@ -166,6 +199,12 @@ export default defineComponent({
showOutputBtn() { showOutputBtn() {
return !this.command.isControlFlow; return !this.command.isControlFlow;
}, },
isLastCommandInChain() {
if (!this.command.commandChain) return false;
return (
this.command.commandType === this.command.commandChain?.slice(-1)[0]
);
},
}, },
setup() { setup() {
const addVariable = inject("addVariable"); const addVariable = inject("addVariable");
@ -259,6 +298,31 @@ export default defineComponent({
}; };
this.$emit("run", tempCommand); this.$emit("run", tempCommand);
}, },
handleToggleCollapse() {
if (this.command.isControlFlow) {
//
this.$emit("toggle-collapse", {
isCollapsed: this.isCollapsed,
chainId: this.command.chainId,
});
} else {
//
this.isCollapsed = !this.isCollapsed;
}
},
handleMouseDown(event) {
//
const isControlElement = event.target.closest(
".q-btn, .q-field, .q-icon, button, input, .border-label"
);
this.isClickingControl = !!isControlElement;
},
handleMouseUp() {
//
setTimeout(() => {
this.isClickingControl = false;
}, 100);
},
}, },
}); });
</script> </script>
@ -269,28 +333,68 @@ export default defineComponent({
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center; transform-origin: center;
opacity: 1; opacity: 1;
padding: 2px 2px 2px 0;
transform: translateY(0) scale(1); transform: translateY(0) scale(1);
border-radius: inherit;
position: relative;
}
.composer-card.no-animation,
.composer-card.no-animation::before,
.composer-card.no-animation .command-item {
transition: none !important;
transform: none !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
}
.composer-card.drag-handle {
cursor: grab;
}
.composer-card.no-animation.drag-handle {
cursor: default;
}
.composer-card.drag-handle:active {
cursor: grabbing;
}
.composer-card.drag-handle:hover::before {
content: "";
position: absolute;
inset: 0;
background: var(--q-primary);
opacity: 0.03;
border-radius: inherit;
transition: all 0.3s ease;
pointer-events: none;
}
.composer-card.drag-handle:active::before {
opacity: 0.06;
transform: scale(0.99);
} }
.command-item { .command-item {
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: inherit;
user-select: none;
transform: translateZ(0);
will-change: transform;
} }
.command-item:hover { .drag-handle .command-item {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); transition: transform 0.2s ease, box-shadow 0.3s ease;
} }
/* 拖拽和放置样式 */ .drag-handle:hover .command-item {
.can-drop { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.can-drop .command-item { .drag-handle:active .command-item {
border: 2px dashed var(--q-primary); transform: translateY(0) scale(0.98);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
} }
/* 暗色模式适配 */ /* 暗色模式适配 */
@ -298,12 +402,61 @@ export default defineComponent({
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
} }
.body--dark .command-item:hover { .body--dark .drag-handle:hover .command-item {
box-shadow: 0 4px 8px rgba(58, 58, 58, 0.3); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
} }
.body--dark .can-drop { .body--dark .drag-handle:active .command-item {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.body--dark .composer-card.drag-handle:hover::before {
background: rgba(255, 255, 255, 0.1);
}
.body--dark .composer-card.drag-handle:active::before {
background: rgba(255, 255, 255, 0.15);
}
/* 收起状态样式 */
.composer-card.collapsed {
transform-origin: top;
}
.composer-card.collapsed .command-item {
min-height: 20px;
}
/* 卡片内容区域动画 */
.card-section {
transition: padding 0.2s cubic-bezier(0.4, 0, 0.2, 1);
padding: 8px;
}
.card-section.collapsed {
padding: 2px 8px;
}
/* 命令内容动画 */
.command-content-wrapper {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 0.2s cubic-bezier(0.4, 0, 0.2, 1);
margin-top: 8px;
}
.command-content {
min-height: 0;
overflow: hidden;
}
.command-content-wrapper.collapsed {
grid-template-rows: 0fr;
margin-top: 0;
}
.command-content-wrapper.collapsed .command-content {
opacity: 0;
} }
/* 调整控制流程组件的样式 */ /* 调整控制流程组件的样式 */

View File

@ -1,13 +1,16 @@
<template> <template>
<div class="composer-flow"> <div class="composer-flow">
<ChainStyles ref="chainStyles" :commands="commands" /> <ChainStyles ref="chainStyles" :commands="commands" />
<div class="section-header"> <div class="section-header flow-header">
<q-icon name="timeline" size="20px" class="q-mx-sm text-primary" /> <div class="flow-title">
<span class="text-subtitle1">命令流程</span> <q-icon name="timeline" size="20px" class="q-mx-sm text-primary" />
<q-space /> <span class="text-subtitle1">命令流程</span>
</div>
<ComposerButtons <ComposerButtons
:generate-code="generateCode" :generate-code="generateCode"
@action="$emit('action', $event)" :is-all-collapsed="isAllCollapsed"
@action="handleAction"
class="flex-grow"
/> />
</div> </div>
@ -23,8 +26,8 @@
group="commands" group="commands"
item-key="id" item-key="id"
class="flow-list" class="flow-list"
handle=".drag-handle"
:animation="200" :animation="200"
handle=".drag-handle"
@start="onDragStart" @start="onDragStart"
@end="onDragEnd" @end="onDragEnd"
@change="onDragChange" @change="onDragChange"
@ -39,6 +42,7 @@
'insert-after': 'insert-after':
dragIndex === commands.length && dragIndex === commands.length &&
index === commands.length - 1, index === commands.length - 1,
...getCollapsedChainClass(index),
...getChainGroupClass(index), ...getChainGroupClass(index),
}" }"
> >
@ -51,6 +55,7 @@
@update:command="(val) => updateCommand(index, val)" @update:command="(val) => updateCommand(index, val)"
@run="handleRunCommand" @run="handleRunCommand"
@add-branch="(chainInfo) => addBranch(index, chainInfo)" @add-branch="(chainInfo) => addBranch(index, chainInfo)"
@toggle-collapse="(event) => handleControlFlowCollapse(event)"
/> />
</div> </div>
</transition> </transition>
@ -67,7 +72,7 @@
import { defineComponent, inject } from "vue"; import { defineComponent, inject } from "vue";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import ComposerCard from "./ComposerCard.vue"; import ComposerCard from "./ComposerCard.vue";
import ComposerButtons from "./ComposerButtons.vue"; import ComposerButtons from "./flow/ComposerButtons.vue";
import ChainStyles from "./flow/ChainStyles.vue"; import ChainStyles from "./flow/ChainStyles.vue";
import EmptyFlow from "./flow/EmptyFlow.vue"; import EmptyFlow from "./flow/EmptyFlow.vue";
import DropArea from "./flow/DropArea.vue"; import DropArea from "./flow/DropArea.vue";
@ -102,6 +107,8 @@ export default defineComponent({
dragIndex: -1, dragIndex: -1,
isDragging: false, isDragging: false,
draggedCommand: null, draggedCommand: null,
collapsedRanges: [],
isAllCollapsed: false,
}; };
}, },
computed: { computed: {
@ -284,15 +291,53 @@ export default defineComponent({
getUniqueId() { getUniqueId() {
return this.$root.getUniqueId(); return this.$root.getUniqueId();
}, },
isFirstCommandInChain(command) {
if (!command.commandChain) return false;
return command.commandType === command.commandChain?.[0];
},
removeRangeCommand(startIndex, endIndex, chainId) {
if (!endIndex) endIndex = startIndex;
const newCommands = [...this.commands];
//
for (let i = endIndex; i >= startIndex; i--) {
const cmd = newCommands[i];
// chainIdchainId
if (chainId && cmd.chainId !== chainId) continue;
if (cmd.outputVariable) {
this.removeVariable(cmd.outputVariable);
}
newCommands.splice(i, 1);
}
this.$emit("update:modelValue", newCommands);
},
removeCommand(index) { removeCommand(index) {
const command = this.commands[index]; const command = this.commands[index];
//
if (command.outputVariable) { //
this.removeVariable(command.outputVariable); if (this.isFirstCommandInChain(command)) {
//
quickcommand
.showButtonBox(["全部删除", "保留内部命令", "手抖👋🏻"])
.then(({ id }) => {
if (id !== 0 && id !== 1) return;
const newCommands = [...this.commands];
const chainId = command.chainId;
const lastIndex = newCommands.findLastIndex(
(cmd) => cmd.chainId === chainId
);
const startIndex = newCommands.findIndex(
(cmd) => cmd.chainId === chainId
);
this.removeRangeCommand(
startIndex,
lastIndex,
id === 0 ? null : chainId
);
});
} else {
//
this.removeRangeCommand(index);
} }
const newCommands = [...this.commands];
newCommands.splice(index, 1);
this.$emit("update:modelValue", newCommands);
}, },
toggleSaveOutput(index) { toggleSaveOutput(index) {
const newCommands = [...this.commands]; const newCommands = [...this.commands];
@ -358,6 +403,87 @@ export default defineComponent({
this.$emit("update:modelValue", newCommands); this.$emit("update:modelValue", newCommands);
} }
}, },
handleControlFlowCollapse(event) {
const chainId = event.chainId;
const isCollapsed = !event.isCollapsed; //
if (!chainId) return;
// commandschainIdindex
const startIndex = this.commands.findIndex(
(cmd) => cmd.chainId === chainId
);
const endIndex = this.commands.findLastIndex(
(cmd) => cmd.chainId === chainId
);
if (startIndex === -1 || endIndex === -1) return;
//
const newCommands = [...this.commands];
newCommands[startIndex] = {
...newCommands[startIndex],
isCollapsed,
};
this.$emit("update:modelValue", newCommands);
if (isCollapsed) {
//
this.collapsedRanges.push({
chainId,
start: startIndex,
end: endIndex,
});
} else {
//
const existingRangeIndex = this.collapsedRanges.findIndex(
(range) => range.chainId === chainId
);
if (existingRangeIndex !== -1) {
this.collapsedRanges.splice(existingRangeIndex, 1);
}
}
},
getCollapsedChainClass(index) {
// index
const matchingRanges = this.collapsedRanges.filter(
(range) => index >= range.start && index <= range.end
);
if (!matchingRanges.length) return {};
//
const isAnyMiddleEnd = matchingRanges.some(
(range) => index > range.start && index <= range.end
);
// hidden
return isAnyMiddleEnd
? { "collapsed-chain-hidden": true }
: { "collapsed-chain-start": true };
},
handleAction(action, payload) {
if (action === "collapseAll") {
this.collapseAll();
} else if (action === "expandAll") {
this.expandAll();
} else {
this.$emit("action", action, payload);
}
},
collapseAll() {
const newCommands = this.commands.map((cmd) => ({
...cmd,
isCollapsed: true,
}));
this.$emit("update:modelValue", newCommands);
this.isAllCollapsed = true;
},
expandAll() {
const newCommands = this.commands.map((cmd) => ({
...cmd,
isCollapsed: false,
}));
this.$emit("update:modelValue", newCommands);
this.isAllCollapsed = false;
},
}, },
}); });
</script> </script>
@ -377,6 +503,17 @@ export default defineComponent({
border-bottom: 1px solid rgba(0, 0, 0, 0.05); border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
}
.flow-title {
display: flex;
align-items: center;
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
} }
.command-scroll { .command-scroll {
@ -461,10 +598,49 @@ export default defineComponent({
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 { .flow-item {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
margin: 3px 0;
border-radius: 5px;
display: grid;
grid-template-rows: 1fr;
}
/* 隐藏的链式命令 */
.collapsed-chain-hidden {
grid-template-rows: 0fr !important;
margin: 0 !important;
padding: 0 !important;
opacity: 0 !important;
pointer-events: none !important;
overflow: hidden !important;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.collapsed-chain-hidden > * {
min-height: 0;
overflow: hidden;
}
.flow-item.chain-start {
border-radius: 5px 5px 0 0;
margin: 0;
}
.flow-item.chain-start.collapsed-chain-start {
border-radius: 5px;
}
.flow-item.chain-middle {
border-radius: 0;
margin: 0;
}
.flow-item.chain-end {
border-radius: 0 0 5px 5px;
margin: 0;
} }
.flow-item.insert-before { .flow-item.insert-before {

View File

@ -4,14 +4,13 @@
<!-- 输出变量设置和按钮 --> <!-- 输出变量设置和按钮 -->
<div <div
class="output-section row items-center no-wrap" class="output-section row items-center no-wrap"
v-if="!showDeleteOnly" v-if="!isControlFlow"
> >
<!-- 变量输入框 --> <!-- 变量输入框 -->
<q-input <q-input
v-if="command.saveOutput" v-if="command.saveOutput"
:model-value="command.outputVariable" :model-value="command.outputVariable"
@update:model-value="$emit('update:outputVariable', $event)" @update:model-value="$emit('update:outputVariable', $event)"
dense
outlined outlined
placeholder="变量名" placeholder="变量名"
class="variable-input" class="variable-input"
@ -19,13 +18,9 @@
> >
</q-input> </q-input>
<!-- 保存变量按钮 --> <!-- 保存变量按钮 -->
<q-btn <q-icon
:icon="command.saveOutput ? 'data_object' : 'output'" :name="command.saveOutput ? 'data_object' : 'output'"
:label="command.saveOutput ? '保存到变量' : '获取输出'"
flat
dense
class="output-btn" class="output-btn"
size="sm"
@click="$emit('toggle-output')" @click="$emit('toggle-output')"
> >
<q-tooltip> <q-tooltip>
@ -44,35 +39,28 @@
}} }}
</div> </div>
</q-tooltip> </q-tooltip>
</q-btn> </q-icon>
</div> </div>
<!-- 操作按钮组 --> <!-- 操作按钮组 -->
<div class="action-buttons row items-center no-wrap"> <div class="action-buttons row items-center no-wrap">
<q-btn <q-icon
flat v-if="!isControlFlow"
dense name="play_arrow"
v-if="!showDeleteOnly" class="run-btn"
round
icon="play_arrow"
class="run-btn q-mr-xs"
size="sm"
@click="$emit('run')" @click="$emit('run')"
> >
<q-tooltip>单独运行此命令并打印输出</q-tooltip> <q-tooltip>单独运行此命令并打印输出</q-tooltip>
</q-btn> </q-icon>
<q-btn <q-icon
flat name="close"
round
dense
icon="close"
@click="$emit('remove')" @click="$emit('remove')"
size="sm"
class="remove-btn" class="remove-btn"
v-if="!isLastCommandInChain"
> >
<q-tooltip>移除此命令</q-tooltip> <q-tooltip>移除此命令</q-tooltip>
</q-btn> </q-icon>
</div> </div>
</div> </div>
</div> </div>
@ -86,12 +74,30 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
showDeleteOnly: { isCollapsed: {
type: Boolean,
default: false,
},
isControlFlow: {
type: Boolean,
default: false,
},
isFirstCommandInChain: {
type: Boolean,
default: false,
},
isLastCommandInChain: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
emits: ["update:outputVariable", "toggle-output", "run", "remove"], emits: [
"update:outputVariable",
"toggle-output",
"run",
"remove",
"toggle-collapse",
],
}; };
</script> </script>
@ -99,11 +105,12 @@ export default {
.command-buttons { .command-buttons {
display: flex; display: flex;
align-items: center; align-items: center;
height: 20px;
} }
/* 输出部分样式 */ /* 输出部分样式 */
.output-section { .output-section {
margin-right: 8px; /* margin-right: 8px; */
gap: 8px; gap: 8px;
} }
@ -116,13 +123,13 @@ export default {
} }
.output-section :deep(.q-field__control) { .output-section :deep(.q-field__control) {
height: 28px; height: 20px;
min-height: 28px; min-height: 20px;
padding: 0 4px; padding: 0 4px;
} }
.output-section :deep(.q-field__marginal) { .output-section :deep(.q-field__marginal) {
height: 28px; height: 20px;
width: 24px; width: 24px;
min-width: 24px; min-width: 24px;
} }
@ -130,7 +137,7 @@ export default {
.output-section :deep(.q-field__native) { .output-section :deep(.q-field__native) {
padding: 0; padding: 0;
font-size: 12px; font-size: 12px;
min-height: 28px; min-height: 20px;
text-align: center; text-align: center;
} }
@ -138,18 +145,20 @@ export default {
.output-btn, .output-btn,
.run-btn, .run-btn,
.remove-btn { .remove-btn {
font-size: 12px; font-size: 18px;
border-radius: 4px; min-height: 25px;
min-height: 28px; cursor: pointer;
opacity: 0.6; opacity: 0.6;
transition: all 0.3s ease; transition: all 0.3s ease;
padding: 0 4px;
} }
.output-btn:hover, .output-btn:hover,
.run-btn:hover, .run-btn:hover,
.remove-btn:hover { .remove-btn:hover {
opacity: 1; opacity: 1;
transform: scale(1.05); transform: scale(1.1) translateY(-1px);
transition: all 0.3s ease;
} }
.run-btn:hover { .run-btn:hover {

View File

@ -1,13 +1,19 @@
<template> <template>
<div <div class="row items-center">
class="row items-center" <!-- 折叠按钮 -->
:class="{ <div class="collapse-btn" v-if="!isLastCommandInChain">
'q-pb-sm': !isControlFlow, <q-btn
}" :icon="isCollapsed ? 'expand_more' : 'expand_less'"
> dense
<!-- 拖拽手柄 --> flat
<div class="drag-handle q-mr-sm" draggable="true"> size="sm"
<q-icon name="drag_indicator" size="18px" class="text-grey-6" /> @click="$emit('toggle-collapse')"
>
<q-tooltip>折叠/展开此{{ isControlFlow ? "流程" : "命令" }}</q-tooltip>
</q-btn>
</div>
<div v-else class="end-icon">
<q-icon name="last_page" size="xs" />
</div> </div>
<!-- 标题 --> <!-- 标题 -->
@ -23,8 +29,11 @@
<!-- 按钮组 --> <!-- 按钮组 -->
<CommandButtons <CommandButtons
:command="command" :command="command"
:show-delete-only="isControlFlow"
v-bind="$attrs" v-bind="$attrs"
:isCollapsed="isCollapsed"
:isControlFlow="isControlFlow"
:isFirstCommandInChain="isFirstCommandInChain"
:isLastCommandInChain="isLastCommandInChain"
@update:outputVariable="$emit('update:outputVariable', $event)" @update:outputVariable="$emit('update:outputVariable', $event)"
@toggle-output="$emit('toggle-output')" @toggle-output="$emit('toggle-output')"
@run="$emit('run')" @run="$emit('run')"
@ -46,50 +55,66 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isControlFlow: { isCollapsed: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
emits: ["update:outputVariable", "toggle-output", "run", "remove"], emits: [
"update:outputVariable",
"toggle-output",
"run",
"remove",
"toggle-collapse",
],
computed: { computed: {
contentClass() { contentClass() {
return { return {
col: true, col: true,
"q-ml-md": !this.isControlFlow, "q-ml-md": !this.command.isControlFlow,
}; };
}, },
isControlFlow() {
return this.command.isControlFlow;
},
isFirstCommandInChain() {
if (!this.command.commandChain) return false;
return this.command.commandType === this.command.commandChain?.[0];
},
isLastCommandInChain() {
if (!this.command.commandChain) return false;
return (
this.command.commandType === this.command.commandChain?.slice(-1)[0]
);
},
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.drag-handle { .collapse-btn,
.end-icon {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 4px; padding-right: 4px;
cursor: grab; transition: all 0.2s ease;
}
.collapse-btn :deep(.q-btn),
.end-icon {
opacity: 0.6; opacity: 0.6;
transition: all 0.2s ease; min-height: 20px;
padding: 0 4px;
} }
.row:hover .drag-handle { .collapse-btn :deep(.q-btn:hover) {
opacity: 0.8;
}
.drag-handle:hover {
opacity: 1; opacity: 1;
transform: scale(1.1) translateY(-1px);
color: var(--q-primary); color: var(--q-primary);
transform: scale(1.2);
transition: all 0.2s ease;
}
.drag-handle:active {
cursor: grabbing;
transform: scale(0.95);
} }
.command-label { .command-label {
user-select: none; user-select: none;
pointer-events: none;
} }
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="conditional-judgment"> <div class="conditional-judgment">
<div class="row items-center no-wrap"> <div class="row items-end no-wrap">
<!-- 类型标签 --> <!-- 类型标签 -->
<div class="text-subtitle2 type-label"> <div class="text-subtitle2 type-label">
<template v-if="type === 'if'">如果满足</template> <template v-if="type === 'if'">如果满足</template>
@ -14,7 +14,6 @@
<q-btn <q-btn
v-if="type === 'if'" v-if="type === 'if'"
flat flat
round
dense dense
size="sm" size="sm"
icon="add" icon="add"
@ -33,10 +32,9 @@
<q-btn <q-btn
v-if="type === 'else'" v-if="type === 'else'"
flat flat
round
dense dense
size="sm" size="sm"
:icon="showCondition ? 'unfold_less' : 'unfold_more'" :icon="showCondition ? 'remove' : 'add'"
class="control-btn q-mx-xs" class="control-btn q-mx-xs"
@click="toggleCondition" @click="toggleCondition"
> >
@ -49,7 +47,6 @@
v-model="conditionLocal" v-model="conditionLocal"
dense dense
borderless borderless
:bg-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
placeholder="输入条件表达式" placeholder="输入条件表达式"
class="condition-input" class="condition-input"
/> />
@ -161,10 +158,6 @@ export default defineComponent({
</script> </script>
<style scoped> <style scoped>
.conditional-judgment {
padding: 4px 0;
}
.type-label { .type-label {
font-size: 14px; font-size: 14px;
color: var(--q-primary); color: var(--q-primary);
@ -178,17 +171,19 @@ export default defineComponent({
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.condition-input :deep(.q-field__control) { .condition-input :deep(.q-field__control),
padding: 0 16px; .condition-input :deep(.q-field__native) {
height: 24px !important; padding: 1px;
min-height: 24px; height: 21px !important;
min-height: 21px;
border-radius: 4px; border-radius: 4px;
font-size: 13px;
} }
.control-btn { .control-btn {
width: 24px; width: 21px;
height: 24px; height: 21px;
min-height: 24px; min-height: 21px;
opacity: 0.7; opacity: 0.7;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@ -198,11 +193,6 @@ export default defineComponent({
transform: scale(1.1); transform: scale(1.1);
} }
/* 暗色模式适配 */
.body--dark .condition-input {
background: rgba(255, 255, 255, 0.05) !important;
}
.body--dark .type-label { .body--dark .type-label {
color: var(--q-primary); color: var(--q-primary);
opacity: 0.8; opacity: 0.8;

View File

@ -1,9 +1,83 @@
<template> <template>
<component :is="'style'" v-if="chainStyles">{{ chainStyles }}</component> <component :is="'style'" v-if="styles">{{ styles }}</component>
</template> </template>
<script> <script>
import { defineComponent } from "vue"; import { defineComponent, ref, watchEffect, computed } from "vue";
//
const STYLE_CONSTANTS = {
goldenRatio: 0.618033988749895,
hueStep: 360 * 0.618033988749895,
indent: 5,
lightSl: "60%, 60%",
darkSl: "60%, 40%",
};
//
const generateStyleString = (selector, rules) => {
return `${selector} {
${Object.entries(rules)
.map(
([prop, value]) =>
` ${prop.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${value} !important;`
)
.join("\n")}
}`;
};
const generateShadows = (
parentChainIds,
hue,
indent,
lightSl,
darkSl,
uniqueChainIds
) => {
return parentChainIds.reduce(
(acc, parentChainId, i) => {
const parentIndex = uniqueChainIds.indexOf(parentChainId);
const parentHue = (parentIndex * STYLE_CONSTANTS.hueStep) % 360;
const start = -((i + 2) * indent);
acc.light.push(`${start}px 0 0 0 hsl(${parentHue}, ${lightSl})`);
acc.dark.push(`${start}px 0 0 0 hsl(${parentHue}, ${darkSl})`);
return acc;
},
{
light: [`-${indent}px 0 0 0 hsl(${hue}, ${lightSl})`],
dark: [`-${indent}px 0 0 0 hsl(${hue}, ${darkSl})`],
}
);
};
//
const getChainGroups = (commands) => {
const groups = [];
const activeGroups = new Map();
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
if (!cmd.chainId) continue;
if (cmd.commandType === cmd.commandChain?.[0]) {
activeGroups.set(cmd.chainId, {
chainId: cmd.chainId,
startIndex: i,
});
} else if (
cmd.commandType === cmd.commandChain?.[cmd.commandChain.length - 1]
) {
const group = activeGroups.get(cmd.chainId);
if (group) {
group.endIndex = i;
groups.push({ ...group });
activeGroups.delete(cmd.chainId);
}
}
}
return groups;
};
export default defineComponent({ export default defineComponent({
name: "ChainStyles", name: "ChainStyles",
@ -13,204 +87,119 @@ export default defineComponent({
required: true, required: true,
}, },
}, },
computed: { setup(props) {
uniqueChainIds() { const styles = ref("");
return this.commands const chainGroups = computed(() => getChainGroups(props.commands));
.filter(
(cmd) => cmd.chainId && cmd.commandType === cmd.commandChain?.[0]
)
.map((cmd) => cmd.chainId)
.filter((chainId, index, self) => self.indexOf(chainId) === index);
},
chainGroups() {
const groups = [];
const activeGroups = new Map(); //
this.commands.forEach((cmd, index) => { // 使 watchEffect commands
if (cmd.chainId) { watchEffect(() => {
if (cmd.commandType === cmd.commandChain?.[0]) { // commands
// if (!props.commands?.length) {
activeGroups.set(cmd.chainId, { styles.value = "";
chainId: cmd.chainId, return;
startIndex: index, }
});
} else if ( try {
cmd.commandType === cmd.commandChain?.[cmd.commandChain.length - 1] // 1. ID
) { const uniqueChainIds = [
// ...new Set(
const group = activeGroups.get(cmd.chainId); props.commands
if (group) { .filter(
group.endIndex = index; (cmd) =>
groups.push({ ...group }); cmd.chainId && cmd.commandType === cmd.commandChain?.[0]
activeGroups.delete(cmd.chainId); )
.map((cmd) => cmd.chainId)
),
];
// 2. 使
const groups = chainGroups.value;
// 3.
const styleRules = {};
const { hueStep, indent, lightSl, darkSl } = STYLE_CONSTANTS;
uniqueChainIds.forEach((chainId, index) => {
const hue = (index * hueStep) % 360;
const className = "chain-group-" + chainId;
//
const currentGroup = groups.find((g) => g.chainId === chainId);
if (!currentGroup) return;
let depth = 1;
const parents = [];
for (const group of groups) {
if (
group.startIndex < currentGroup.startIndex &&
group.endIndex > currentGroup.endIndex
) {
depth++;
parents.push(group.chainId);
}
}
const shadows = generateShadows(
parents,
hue,
indent,
lightSl,
darkSl,
uniqueChainIds
);
const commonStyle = {
marginLeft: `calc(${indent}px * ${depth - 1})`,
};
styleRules["." + className] = {
...commonStyle,
boxShadow: shadows.light.join(", "),
};
styleRules[".body--dark ." + className] = {
...commonStyle,
boxShadow: shadows.dark.join(", "),
};
});
// 4.
styles.value = Object.entries(styleRules)
.map(([selector, rules]) => generateStyleString(selector, rules))
.join("\n\n");
} catch (error) {
console.error("Error generating chain styles:", error);
styles.value = "";
}
});
return {
styles,
getChainGroupClass(index) {
const classes = {};
// 使
for (const group of chainGroups.value) {
if (index >= group.startIndex && index <= group.endIndex) {
classes[`chain-group-${group.chainId}`] = true;
if (index === group.startIndex) {
classes["chain-start"] = true;
} else if (index === group.endIndex) {
classes["chain-end"] = true;
} else {
classes["chain-middle"] = true;
//
classes["chain-start"] = false;
classes["chain-end"] = false;
} }
} }
} }
});
return groups; return classes;
}, },
styleConstants() { };
return {
goldenRatio: 0.618033988749895,
hueStep: 360 * 0.618033988749895,
indent: 5,
lightSl: "60%, 60%",
darkSl: "60%, 40%",
};
},
chainStylesMap() {
const styles = {};
const { hueStep, indent, lightSl, darkSl } = this.styleConstants;
//
styles[".chain-start"] = { borderRadius: "4px 4px 0 0" };
styles[".chain-end"] = { borderRadius: "0 4px 0 4px" };
styles[".chain-middle"] = { borderRadius: "0 4px 0 0" };
this.uniqueChainIds.forEach((chainId, index) => {
const hue = (index * hueStep) % 360;
const className = "chain-group-" + chainId;
const depth = this.getChainDepth(chainId);
const parentChainIds = this.getParentChainIds(chainId);
//
const shadows = parentChainIds.reduce(
(acc, parentChainId, i) => {
const parentIndex = this.uniqueChainIds.indexOf(parentChainId);
const parentHue = (parentIndex * hueStep) % 360;
const start = -((i + 2) * indent);
acc.light.push(
start + "px 0 0 0 hsl(" + parentHue + ", " + lightSl + ")"
);
acc.dark.push(
start + "px 0 0 0 hsl(" + parentHue + ", " + darkSl + ")"
);
return acc;
},
{
light: [-indent + "px 0 0 0 hsl(" + hue + ", " + lightSl + ")"],
dark: [-indent + "px 0 0 0 hsl(" + hue + ", " + darkSl + ")"],
}
);
//
const commonStyle = {
marginLeft: "calc(" + indent + "px * " + (depth - 1) + ")",
};
//
styles["." + className] = {
...commonStyle,
background: "hsla(" + hue + ", " + lightSl + ", 0.15)",
boxShadow: shadows.light.join(", "),
};
//
styles[".body--dark ." + className] = {
...commonStyle,
background: "hsla(" + hue + ", " + darkSl + ", 0.2)",
boxShadow: shadows.dark.join(", "),
};
});
return styles;
},
chainStyles() {
return Object.entries(this.chainStylesMap)
.map(
([selector, rules]) =>
selector +
" {\n" +
Object.entries(rules)
.map(
([prop, value]) =>
" " +
prop.replace(/([A-Z])/g, "-$1").toLowerCase() +
": " +
value +
" !important;"
)
.join("\n") +
"\n}"
)
.join("\n\n");
},
},
methods: {
getChainDepth(chainId) {
let depth = 1;
let currentIndex = this.commands.findIndex(
(cmd) =>
cmd.chainId === chainId && cmd.commandType === cmd.commandChain?.[0]
);
if (currentIndex === -1) return depth;
for (let i = 0; i < currentIndex; i++) {
const cmd = this.commands[i];
if (cmd.chainId && cmd.commandType === cmd.commandChain?.[0]) {
const endIndex = this.commands.findIndex(
(c, idx) =>
idx > i &&
c.chainId === cmd.chainId &&
c.commandType === cmd.commandChain?.[cmd.commandChain.length - 1]
);
if (endIndex > currentIndex) {
depth++;
}
}
}
return depth;
},
getParentChainIds(chainId) {
const parents = [];
let currentIndex = this.commands.findIndex(
(cmd) =>
cmd.chainId === chainId && cmd.commandType === cmd.commandChain?.[0]
);
if (currentIndex === -1) return parents;
for (let i = currentIndex - 1; i >= 0; i--) {
const cmd = this.commands[i];
if (cmd.chainId && cmd.commandType === cmd.commandChain?.[0]) {
const endIndex = this.commands.findIndex(
(c, idx) =>
idx > i &&
c.chainId === cmd.chainId &&
c.commandType === cmd.commandChain?.[cmd.commandChain.length - 1]
);
if (endIndex > currentIndex) {
parents.push(cmd.chainId);
}
}
}
return parents;
},
getChainGroupClass(index) {
//
const matchingGroups = this.chainGroups.filter(
(g) => index >= g.startIndex && index <= g.endIndex
);
//
const classes = {};
matchingGroups.forEach((group) => {
classes["chain-group-" + group.chainId] = true;
if (index === group.startIndex) {
classes["chain-start"] = true;
}
if (index === group.endIndex) {
classes["chain-end"] = true;
}
if (index > group.startIndex && index < group.endIndex) {
classes["chain-middle"] = true;
}
});
return classes;
},
}, },
}); });
</script> </script>

View File

@ -1,33 +1,46 @@
<template> <template>
<div class="composer-buttons"> <div class="composer-buttons">
<q-btn <div class="left-buttons">
@click="$q.dark.toggle()" <q-btn
:icon="$q.dark.isActive ? 'dark_mode' : 'light_mode'" :icon="isAllCollapsed ? 'unfold_more' : 'unfold_less'"
flat dense
dense flat
v-if="isDev" @click="$emit('action', isAllCollapsed ? 'expandAll' : 'collapseAll')"
> >
</q-btn> <q-tooltip>{{ isAllCollapsed ? "展开所有" : "折叠所有" }}</q-tooltip>
<q-btn icon="logout" dense flat v-close-popup> </q-btn>
<q-tooltip>退出可视化编排</q-tooltip> </div>
</q-btn>
<q-btn dense icon="publish" flat @click="$emit('action', 'insert')"> <div class="right-buttons">
<q-tooltip>插入到编辑器光标处</q-tooltip> <q-btn
</q-btn> @click="$q.dark.toggle()"
<q-btn dense flat icon="done_all" @click="$emit('action', 'apply')"> :icon="$q.dark.isActive ? 'dark_mode' : 'light_mode'"
<q-tooltip>清空编辑器内容并插入</q-tooltip> flat
</q-btn> dense
<q-btn v-if="isDev"
flat >
dense </q-btn>
icon="preview" <q-btn icon="logout" dense flat v-close-popup>
@mouseenter="handleMouseEnter" <q-tooltip>退出可视化编排</q-tooltip>
@mouseleave="handleMouseLeave" </q-btn>
> <q-btn dense icon="publish" flat @click="$emit('action', 'insert')">
</q-btn> <q-tooltip>插入到编辑器光标处</q-tooltip>
<q-btn dense flat icon="play_circle" @click="$emit('action', 'run')"> </q-btn>
<q-tooltip>运行</q-tooltip> <q-btn dense flat icon="done_all" @click="$emit('action', 'apply')">
</q-btn> <q-tooltip>清空编辑器内容并插入</q-tooltip>
</q-btn>
<q-btn
flat
dense
icon="preview"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
</q-btn>
<q-btn dense flat icon="play_circle" @click="$emit('action', 'run')">
<q-tooltip>运行</q-tooltip>
</q-btn>
</div>
<transition name="preview-fade"> <transition name="preview-fade">
<div v-if="isVisible" class="preview-popup"> <div v-if="isVisible" class="preview-popup">
@ -52,6 +65,10 @@ export default defineComponent({
type: Function, type: Function,
required: true, required: true,
}, },
isAllCollapsed: {
type: Boolean,
default: false,
},
}, },
emits: ["action"], emits: ["action"],
@ -88,15 +105,25 @@ export default defineComponent({
<style scoped> <style scoped>
.composer-buttons { .composer-buttons {
position: relative; position: relative;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
} }
.composer-buttons > .q-btn { .left-buttons,
.right-buttons {
display: flex;
align-items: center;
}
.composer-buttons > div > .q-btn {
opacity: 0.6; opacity: 0.6;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 12px; font-size: 12px;
} }
.composer-buttons > .q-btn:hover { .composer-buttons > div > .q-btn:hover {
opacity: 1; opacity: 1;
transform: translateY(-1px); transform: translateY(-1px);
color: var(--q-primary); color: var(--q-primary);