条件判断添加作用范围样式及拖拽校验

This commit is contained in:
fofolee 2025-01-01 14:04:37 +08:00
parent 9ea10f8033
commit f3a01a1ba9
10 changed files with 474 additions and 166 deletions

View File

@ -251,6 +251,13 @@ export default defineComponent({
.padStart(2, "0")
);
},
getUniqueId() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
},
},
watch: {
// glassEffect

View File

@ -1,9 +1,5 @@
<template>
<div
class="composer-card q-pa-xs"
:class="{ 'can-drop': canDrop }"
v-bind="$attrs"
>
<div class="composer-card" :class="{ 'can-drop': canDrop }" v-bind="$attrs">
<q-card class="command-item">
<q-card-section class="q-pa-sm">
<CommandHead
@ -21,8 +17,8 @@
v-model="argvLocal"
:command="command"
v-bind="command.componentProps || {}"
:type="command.controlFlowType"
@addBranch="$emit('addBranch')"
:type="command.commandType"
@addBranch="(chainInfo) => $emit('addBranch', chainInfo)"
/>
</template>
@ -273,6 +269,7 @@ export default defineComponent({
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center;
opacity: 1;
padding: 2px 2px 2px 0;
transform: translateY(0) scale(1);
}

View File

@ -1,5 +1,6 @@
<template>
<div class="composer-flow">
<ChainStyles ref="chainStyles" :commands="commands" />
<div class="section-header">
<q-icon name="timeline" size="20px" class="q-mx-sm text-primary" />
<span class="text-subtitle1">命令流程</span>
@ -18,7 +19,7 @@
@dragleave.prevent="onDragLeave"
>
<draggable
v-model="commands"
:list="commands"
group="commands"
item-key="id"
class="flow-list"
@ -26,6 +27,7 @@
:animation="200"
@start="onDragStart"
@end="onDragEnd"
@change="onDragChange"
>
<template #item="{ element, index }">
<transition name="slide-fade" mode="out-in" appear>
@ -37,6 +39,7 @@
'insert-after':
dragIndex === commands.length &&
index === commands.length - 1,
...getChainGroupClass(index),
}"
>
<ComposerCard
@ -47,21 +50,14 @@
@update:argv="(val) => handleArgvChange(index, val)"
@update:command="(val) => updateCommand(index, val)"
@run="handleRunCommand"
@add-branch="() => addBranch(index)"
@add-branch="(chainInfo) => addBranch(index, chainInfo)"
/>
</div>
</transition>
</template>
</draggable>
<div v-if="commands.length === 0" class="empty-flow">
<div class="text-center text-grey-6">
<q-icon name="drag_indicator" size="32px" />
<div class="text-body2 q-mt-sm">从左侧拖拽命令到这里开始编排</div>
</div>
</div>
<div v-else class="drop-area">
<q-icon name="add" size="32px" />
</div>
<EmptyFlow v-if="commands.length === 0" />
<DropArea v-else />
</div>
</q-scroll-area>
</div>
@ -72,6 +68,9 @@ import { defineComponent, inject } from "vue";
import draggable from "vuedraggable";
import ComposerCard from "./ComposerCard.vue";
import ComposerButtons from "./ComposerButtons.vue";
import ChainStyles from "./flow/ChainStyles.vue";
import EmptyFlow from "./flow/EmptyFlow.vue";
import DropArea from "./flow/DropArea.vue";
export default defineComponent({
name: "ComposerFlow",
@ -79,6 +78,9 @@ export default defineComponent({
draggable,
ComposerCard,
ComposerButtons,
ChainStyles,
EmptyFlow,
DropArea,
},
props: {
modelValue: {
@ -91,6 +93,17 @@ export default defineComponent({
},
},
emits: ["update:modelValue", "add-command", "action"],
setup() {
const removeVariable = inject("removeVariable");
return { removeVariable };
},
data() {
return {
dragIndex: -1,
isDragging: false,
draggedCommand: null,
};
},
computed: {
commands: {
get() {
@ -101,32 +114,46 @@ export default defineComponent({
},
},
},
setup() {
const removeVariable = inject("removeVariable");
return {
removeVariable,
};
},
data() {
return {
dragIndex: -1,
isDragging: false,
};
},
methods: {
onDragStart() {
this.isDragging = true;
getChainGroupClass(index) {
return this.$refs.chainStyles?.getChainGroupClass(index) || {};
},
getPlaceholder(element, index) {
return element.desc;
},
onDragStart(event) {
this.isDragging = true;
this.draggedCommand = this.commands[event.oldIndex];
},
onDragEnd() {
this.isDragging = false;
this.dragIndex = -1;
this.draggedCommand = null;
},
onDragChange(event) {
let newCommands = [...this.commands];
if (event.moved || event.added) {
//
const isValidOrder = this.checkAllChainOrders(newCommands);
if (!isValidOrder) {
//
if (event.moved) {
const { oldIndex, newIndex } = event.moved;
const [item] = newCommands.splice(newIndex, 1);
newCommands.splice(oldIndex, 0, item);
} else if (event.added) {
const { newIndex } = event.added;
newCommands.splice(newIndex, 1);
}
}
}
this.$emit("update:modelValue", newCommands);
},
onDragOver(event) {
if (!this.isDragging) {
const rect = event.currentTarget.getBoundingClientRect();
const items = this.$el.querySelectorAll(".flow-item");
const mouseY = event.clientY;
@ -154,59 +181,86 @@ export default defineComponent({
this.dragIndex = closestIndex;
}
},
onDragLeave() {
if (!this.isDragging) {
this.dragIndex = -1;
}
},
checkAllChainOrders(commands) {
// chainId
const chainIds = new Set(
commands.filter((cmd) => cmd.chainId).map((cmd) => cmd.chainId)
);
//
for (const chainId of chainIds) {
//
const indices = commands
.map((cmd, index) => ({ cmd, index }))
.filter((item) => item.cmd.chainId === chainId)
.map((item) => item.index);
// ifelseend
const ifIndex = indices.find(
(index) => commands[index].commandType === "if"
);
const endIndex = indices.find(
(index) => commands[index].commandType === "end"
);
const elseIndices = indices.filter(
(index) =>
commands[index].commandType !== "if" &&
commands[index].commandType !== "end"
);
//
// 1. if end
if (ifIndex === undefined || endIndex === undefined) return false;
// 2. if
if (indices.some((index) => index < ifIndex)) return false;
// 3. end
if (indices.some((index) => index > endIndex)) return false;
// 4. else if end
if (elseIndices.some((index) => index < ifIndex || index > endIndex))
return false;
}
return true;
},
onDrop(event) {
try {
const actionData = event.dataTransfer.getData("action");
if (!actionData) return;
const parsedAction = JSON.parse(actionData);
const isControlFlow = parsedAction.isControlFlow;
const commandChain = parsedAction.commandChain;
const newCommand = this.createNewCommand(parsedAction);
let newCommands = [...this.commands];
const newCommand = {
...parsedAction,
id: Date.now(),
argv: "",
saveOutput: false,
useOutput: null,
outputVariable: null,
cmd: parsedAction.value || parsedAction.cmd,
value: parsedAction.value || parsedAction.cmd,
};
const newCommands = [...this.commands];
// startend
if (isControlFlow) {
const startCommand = {
...newCommand,
id: Date.now(),
controlFlowType: "start",
};
const endCommand = {
...newCommand,
id: Date.now() + 1,
controlFlowType: "end",
};
if (this.dragIndex >= 0) {
newCommands.splice(this.dragIndex, 0, startCommand, endCommand);
} else {
newCommands.push(startCommand, endCommand);
}
} else {
if (!commandChain) {
//
if (this.dragIndex >= 0) {
newCommands.splice(this.dragIndex, 0, newCommand);
} else {
newCommands.push(newCommand);
}
} else {
//
const chainId = this.getUniqueId();
let insertIndex =
this.dragIndex >= 0 ? this.dragIndex : newCommands.length;
//
for (const commandType of commandChain) {
const commandItem = {
...newCommand,
id: this.getUniqueId(),
commandType,
chainId,
};
newCommands.splice(insertIndex, 0, commandItem);
insertIndex++; //
}
}
this.$emit("update:modelValue", newCommands);
@ -215,6 +269,21 @@ export default defineComponent({
console.debug("Internal drag & drop reorder", error);
}
},
createNewCommand(parsedAction) {
return {
...parsedAction,
id: this.getUniqueId(),
argv: "",
saveOutput: false,
useOutput: null,
outputVariable: null,
cmd: parsedAction.value || parsedAction.cmd,
value: parsedAction.value || parsedAction.cmd,
};
},
getUniqueId() {
return this.$root.getUniqueId();
},
removeCommand(index) {
const command = this.commands[index];
//
@ -225,9 +294,6 @@ export default defineComponent({
newCommands.splice(index, 1);
this.$emit("update:modelValue", newCommands);
},
getPlaceholder(element, index) {
return element.desc;
},
toggleSaveOutput(index) {
const newCommands = [...this.commands];
newCommands[index].saveOutput = !newCommands[index].saveOutput;
@ -268,27 +334,27 @@ export default defineComponent({
//
this.$emit("action", "run", tempFlow);
},
addBranch(index) {
addBranch(index, chainInfo) {
const newCommands = [...this.commands];
const midCommand = {
const branchCommand = {
...newCommands[index],
id: Date.now(),
controlFlowType: "mid",
id: this.getUniqueId(),
chainId: chainInfo.chainId,
commandType: chainInfo.commandType,
argv: "",
};
// end
let endIndex = index + 1;
let depth = 1;
while (endIndex < newCommands.length && depth > 0) {
if (newCommands[endIndex].controlFlowType === "start") depth++;
if (newCommands[endIndex].controlFlowType === "end") depth--;
endIndex++;
// chainId
let lastIndex = -1;
for (let i = index + 1; i < newCommands.length; i++) {
if (newCommands[i].chainId === chainInfo.chainId) {
lastIndex = i;
}
}
// end
if (endIndex > index + 1) {
newCommands.splice(endIndex - 1, 0, midCommand);
//
if (lastIndex !== -1) {
newCommands.splice(lastIndex, 0, branchCommand);
this.$emit("update:modelValue", newCommands);
}
},
@ -333,53 +399,6 @@ export default defineComponent({
background-color: rgba(32, 32, 32, 0.8);
}
/* .flow-list {
min-height: 50px;
} */
.drop-area {
flex: 1;
min-height: 50px;
border-radius: 8px;
margin: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #9994;
border: 2px dashed #9994;
}
.body--dark .drop-area {
color: #6664;
border: 2px dashed #6664;
}
.empty-flow {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed #e0e0e0;
border-radius: 4px;
margin: 8px 0;
transition: all 0.3s ease;
flex: 1;
}
.body--dark .empty-flow {
border: 2px dashed #676666;
}
.empty-flow:hover {
border-color: #bdbdbd;
background-color: #fafafa;
}
.body--dark .empty-flow:hover {
border-color: #676666;
background-color: #303132;
}
/* 滑动淡出动画 */
.slide-fade-enter-active,
.slide-fade-leave-active {
@ -460,23 +479,4 @@ export default defineComponent({
.flow-item.insert-before + .flow-item {
transform: translateY(3px);
}
/* 暗色模式适配 */
.body--dark .flow-item::before,
.body--dark .flow-item::after {
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.08) 10%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.08) 90%,
transparent
);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.03),
0 0 4px rgba(255, 255, 255, 0.05);
}
.body--dark .section-header {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
</style>

View File

@ -179,6 +179,7 @@ export default defineComponent({
flex-direction: column;
height: 100%;
border-radius: 10px;
user-select: none;
}
.section-header {
@ -218,6 +219,10 @@ export default defineComponent({
border: none;
}
.composer-list :deep(.q-expansion-item .q-item) {
justify-content: space-between;
}
.body--dark .composer-list {
background-color: rgba(32, 32, 32, 0.8);
}

View File

@ -3,30 +3,35 @@
<div class="row items-center no-wrap">
<!-- 类型标签 -->
<div class="text-subtitle2 type-label">
<template v-if="type === 'start'">如果满足</template>
<template v-else-if="type === 'mid'">
<template v-if="type === 'if'">如果满足</template>
<template v-else-if="type === 'else'">
{{ showCondition ? "否则满足" : "否则" }}
</template>
<template v-else>结束条件判断</template>
</div>
<!-- start类型显示添加按钮 -->
<!-- if类型显示添加按钮 -->
<q-btn
v-if="type === 'start'"
v-if="type === 'if'"
flat
round
dense
size="sm"
icon="add"
class="control-btn q-mx-xs"
@click="$emit('addBranch')"
@click="
$emit('addBranch', {
chainId: command.chainId,
commandType: 'else',
})
"
>
<q-tooltip>添加条件分支</q-tooltip>
</q-btn>
<!-- mid类型显示切换按钮 -->
<q-btn
v-if="type === 'mid'"
v-if="type === 'else'"
flat
round
dense
@ -63,7 +68,7 @@ export default defineComponent({
type: {
type: String,
required: true,
validator: (value) => ["start", "mid", "end"].includes(value),
validator: (value) => ["if", "else", "end"].includes(value),
},
},
emits: ["update:modelValue", "addBranch"],
@ -80,7 +85,7 @@ export default defineComponent({
computed: {
showCondition() {
return (
this.type === "start" || (this.type === "mid" && this.showMidCondition)
this.type === "if" || (this.type === "else" && this.showMidCondition)
);
},
conditionLocal: {
@ -94,9 +99,9 @@ export default defineComponent({
},
generatedCode() {
switch (this.type) {
case "start":
case "if":
return `if(${this.condition || "true"}){`;
case "mid":
case "else":
return this.showMidCondition && this.condition
? `}else if(${this.condition}){`
: "}else{";
@ -130,12 +135,12 @@ export default defineComponent({
},
parseCodeString(val) {
try {
if (this.type === "start") {
if (this.type === "if") {
const match = val.match(/^if\((.*)\){$/);
if (match) {
this.condition = match[1] === "true" ? "" : match[1];
}
} else if (this.type === "mid") {
} else if (this.type === "else") {
if (val === "}else{") {
this.showMidCondition = false;
this.condition = "";
@ -165,6 +170,7 @@ export default defineComponent({
color: var(--q-primary);
white-space: nowrap;
opacity: 0.9;
user-select: none;
}
.condition-input {

View File

@ -0,0 +1,216 @@
<template>
<component :is="'style'" v-if="chainStyles">{{ chainStyles }}</component>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "ChainStyles",
props: {
commands: {
type: Array,
required: true,
},
},
computed: {
uniqueChainIds() {
return this.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) => {
if (cmd.chainId) {
if (cmd.commandType === cmd.commandChain?.[0]) {
//
activeGroups.set(cmd.chainId, {
chainId: cmd.chainId,
startIndex: index,
});
} else if (
cmd.commandType === cmd.commandChain?.[cmd.commandChain.length - 1]
) {
//
const group = activeGroups.get(cmd.chainId);
if (group) {
group.endIndex = index;
groups.push({ ...group });
activeGroups.delete(cmd.chainId);
}
}
}
});
return groups;
},
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>

View File

@ -0,0 +1,32 @@
<template>
<div class="drop-area">
<q-icon name="add" size="32px" />
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "DropArea",
});
</script>
<style scoped>
.drop-area {
flex: 1;
min-height: 50px;
border-radius: 8px;
margin: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #9994;
border: 2px dashed #9994;
}
.body--dark .drop-area {
color: #6664;
border: 2px dashed #6664;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div class="empty-flow">
<div class="text-center text-grey-6">
<q-icon name="drag_indicator" size="32px" />
<div class="text-body2 q-mt-sm">从左侧拖拽命令到这里开始编排</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "EmptyFlow",
});
</script>
<style scoped>
.empty-flow {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed #e0e0e0;
border-radius: 4px;
margin: 8px 0;
transition: all 0.3s ease;
flex: 1;
}
.body--dark .empty-flow {
border: 2px dashed #676666;
}
.empty-flow:hover {
border-color: #bdbdbd;
background-color: #fafafa;
}
.body--dark .empty-flow:hover {
border-color: #676666;
background-color: #303132;
}
</style>

View File

@ -133,7 +133,7 @@ export default defineComponent({
...this.selectedActions,
{
...action,
id: Date.now(),
id: this.$root.getUniqueId(),
argv: "",
saveOutput: false,
useOutput: null,

View File

@ -7,6 +7,7 @@ export const controlCommands = {
label: "条件判断",
component: "ConditionalJudgment",
isControlFlow: true,
commandChain: ["if", "end"],
},
],
};