新增子流程功能,可以添加子流程,方便后续作为函数调用

This commit is contained in:
fofolee 2025-01-21 18:14:46 +08:00
parent 2728c5783f
commit e0208eb119
5 changed files with 416 additions and 161 deletions

View File

@ -9,12 +9,9 @@
<!-- 右侧命令流程 -->
<div class="col command-section">
<ComposerFlow
v-model="commandFlow"
:generate-code="generateFlowCode"
:show-close-button="showCloseButton"
@add-command="addCommand"
<FlowTabs
@action="handleComposer"
:show-close-button="showCloseButton"
/>
</div>
</div>
@ -27,19 +24,15 @@
<script>
import { defineComponent, provide, ref } from "vue";
import ComposerList from "./ComposerList.vue";
import ComposerFlow from "./ComposerFlow.vue";
import {
availableCommands,
findCommandByValue,
} from "js/composer/composerConfig";
import { generateCode } from "js/composer/generateCode";
import FlowTabs from "./FlowTabs.vue";
import { availableCommands } from "js/composer/composerConfig";
import { parseVariables } from "js/composer/variableManager";
export default defineComponent({
name: "CommandComposer",
components: {
ComposerList,
ComposerFlow,
FlowTabs,
},
setup() {
const commandFlow = ref([]);
@ -94,65 +87,9 @@ export default defineComponent({
outputVariable: null,
});
},
generateFlowCode() {
return generateCode(this.commandFlow);
},
handleComposer(type, flow) {
switch (type) {
case "save":
return this.saveFlow();
case "load":
return this.loadFlow();
case "run":
return this.runFlow(flow);
default:
return this.$emit("use-composer", {
type,
code: this.generateFlowCode(),
});
}
},
// flow
runFlow(flow) {
this.hasCommandNeedLoading = this.findCommandNeedLoading(flow);
const code = generateCode(flow || this.commandFlow);
this.$emit("use-composer", { type: "run", code });
if (!code.includes("console.log")) quickcommand.showMessageBox("已运行");
},
saveFlow() {
const flow = window.lodashM.cloneDeep(this.commandFlow);
const uselessProps = [
"config",
"code",
"label",
"component",
"subCommands",
"options",
"defaultValue",
"icon",
"width",
"placeholder",
];
//
flow.forEach((cmd) => {
for (const props of uselessProps) {
delete cmd[props];
}
});
localStorage.setItem("quickcomposer.flow", JSON.stringify(flow));
quickcommand.showMessageBox("保存成功");
},
loadFlow() {
const savedFlow = localStorage.getItem("quickcomposer.flow");
if (!savedFlow) return;
this.commandFlow = JSON.parse(savedFlow).map((cmd) => {
//
const command = findCommandByValue(cmd.value);
return {
...command,
...cmd,
};
});
handleComposer(type, code) {
//
this.$emit("use-composer", { type, code });
},
findCommandNeedLoading(flow) {
//

View File

@ -1,19 +1,6 @@
<template>
<div class="composer-flow">
<ChainStyles ref="chainStyles" :commands="commands" />
<div class="section-header flow-header">
<div class="flow-title">
<q-icon name="timeline" size="20px" class="q-mx-sm text-primary" />
<span class="text-subtitle1">命令流程</span>
</div>
<ComposerButtons
:generate-code="generateCode"
:is-all-collapsed="isAllCollapsed"
:show-close-button="showCloseButton"
@action="handleAction"
class="flex-grow"
/>
</div>
<q-scroll-area class="command-scroll">
<div
@ -73,7 +60,6 @@
import { defineComponent, inject } from "vue";
import draggable from "vuedraggable";
import ComposerCard from "./ComposerCard.vue";
import ComposerButtons from "./flow/ComposerButtons.vue";
import ChainStyles from "./flow/ChainStyles.vue";
import EmptyFlow from "./flow/EmptyFlow.vue";
import DropArea from "./flow/DropArea.vue";
@ -88,7 +74,6 @@ export default defineComponent({
components: {
draggable,
ComposerCard,
ComposerButtons,
ChainStyles,
EmptyFlow,
DropArea,
@ -118,7 +103,6 @@ export default defineComponent({
dragIndex: -1,
isDragging: false,
draggedCommand: null,
isAllCollapsed: false,
};
},
computed: {
@ -363,13 +347,16 @@ export default defineComponent({
},
handleRunCommand(command) {
//
const tempFlow = [
command,
{
//
code: `if(${command.outputVariable}!==undefined){console.log(${command.outputVariable})}`,
},
];
const tempFlow = {
name: "main",
commands: [
command,
{
//
code: `if(${command.outputVariable}!==undefined){console.log(${command.outputVariable})}`,
},
],
};
//
this.$emit("action", "run", tempFlow);
},
@ -497,46 +484,20 @@ export default defineComponent({
this.$emit("update:modelValue", newCommands);
},
collapseAll() {
const newCommands = [...this.commands];
let i = 0;
while (i < newCommands.length) {
const cmd = newCommands[i];
if (cmd.chainId && this.isFirstCommandInChain(cmd)) {
//
const { endIndex } = this.getChainIndex(cmd.chainId);
//
newCommands[i] = {
...cmd,
isCollapsed: true,
};
//
i = endIndex + 1;
} else if (!cmd.chainId) {
//
newCommands[i] = {
...cmd,
isCollapsed: true,
};
i++;
} else {
//
i++;
}
}
this.$emit("update:modelValue", newCommands);
this.isAllCollapsed = true;
},
expandAll() {
//
const newCommands = this.commands.map((cmd) => ({
...cmd,
isCollapsed: false, //
isCollapsed: true,
}));
this.$emit("update:modelValue", newCommands);
},
expandAll() {
//
const newCommands = this.commands.map((cmd) => ({
...cmd,
isCollapsed: false,
}));
this.$emit("update:modelValue", newCommands);
this.isAllCollapsed = false;
},
//
checkChainOrders(commands, chainId) {
@ -618,22 +579,6 @@ export default defineComponent({
border-radius: 10px;
}
.section-header {
flex-shrink: 0;
padding: 0 8px;
height: 30px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 8px;
}
.flow-title {
display: flex;
align-items: center;
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}

View File

@ -0,0 +1,367 @@
<template>
<div class="flow-tabs">
<div class="tabs-header">
<div class="header-content">
<div class="tabs-container">
<!-- main 作为固定按钮 -->
<q-btn
flat
dense
:color="activeTab === 'main' ? 'primary' : 'grey'"
label="main"
class="main-btn"
@click="activeTab = 'main'"
/>
<!-- 其他流程标签可滚动 -->
<q-tabs
v-model="activeTab"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="left"
narrow-indicator
>
<template v-for="flow in nonMainFlows" :key="flow.id">
<q-tab :name="flow.id" class="flow-tab">
<div class="flow-tab-content">
<q-input
v-model="flow.name"
dense
borderless
class="flow-name-input"
@keydown.space.prevent
@blur="validateFlowName(flow)"
/>
<q-btn
flat
dense
round
icon="close"
size="xs"
@click.stop="removeFlow(flow)"
/>
</div>
</q-tab>
</template>
</q-tabs>
<q-btn
flat
dense
round
icon="add"
size="sm"
class="q-ml-sm add-btn"
@click="addFlow"
/>
</div>
<ComposerButtons
:generate-code="generateAllFlowCode"
:is-all-collapsed="isAllCollapsed"
:show-close-button="showCloseButton"
@action="handleAction"
/>
</div>
</div>
<div class="flow-container">
<ComposerFlow
v-for="flow in flows"
v-show="activeTab === flow.id"
:key="flow.id"
v-model="flow.commands"
:generate-code="() => generateFlowCode(flow)"
:show-close-button="flows.length > 1"
@action="(type, payload) => handleFlowAction(type, payload, flow)"
ref="flowRefs"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import ComposerFlow from "./ComposerFlow.vue";
import ComposerButtons from "./flow/ComposerButtons.vue";
import { generateCode } from "js/composer/generateCode";
import { findCommandByValue } from "js/composer/composerConfig";
export default defineComponent({
name: "FlowTabs",
components: {
ComposerFlow,
ComposerButtons,
},
props: {
showCloseButton: {
type: Boolean,
default: true,
},
},
data() {
return {
flows: [
{
id: "main",
name: "main",
commands: [],
},
],
activeTab: "main",
isAllCollapsed: false,
};
},
computed: {
nonMainFlows() {
return this.flows.filter((f) => f.id !== "main");
},
},
methods: {
addFlow() {
const id = `flow_${this.$root.getUniqueId()}`;
const name = `flow${this.flows.length}`;
this.flows.push({
id,
name,
commands: [],
});
this.activeTab = id;
this.$nextTick(this.updateWidths);
},
removeFlow(flow) {
const index = this.flows.findIndex((f) => f.id === flow.id);
if (index > -1 && flow.id !== "main") {
this.flows.splice(index, 1);
this.activeTab = this.flows[0].id;
this.$nextTick(this.updateWidths);
}
},
validateFlowName(flow) {
if (flow.id === "main") return;
//
let newName = flow.name.replace(/\s+/g, "_");
let counter = 1;
const baseName = newName;
while (this.flows.some((f) => f.id !== flow.id && f.name === newName)) {
newName = `${baseName}_${counter++}`;
}
flow.name = newName;
},
generateFlowCode(flow) {
return generateCode(flow.commands, flow.name);
},
generateAllFlowCode() {
// flow
return this.flows.map((flow) => this.generateFlowCode(flow)).join("\n\n");
},
handleFlowAction(type, payload, flow) {
if (type === "close") {
const index = this.flows.findIndex((f) => f.id === flow.id);
if (index > -1 && this.flows.length > 1) {
this.flows.splice(index, 1);
this.activeTab = this.flows[0].id;
}
} else {
this.handleAction(type, payload);
}
},
handleAction(type, payload) {
switch (type) {
case "save":
this.saveFlows();
break;
case "load":
this.loadFlows();
break;
case "run":
this.runFlows(payload);
break;
case "collapseAll":
this.collapseAll();
break;
case "expandAll":
this.expandAll();
break;
default:
this.$emit("action", type, this.generateAllFlowCode());
}
},
saveFlows() {
const flowsData = this.flows.map((flow) => ({
id: flow.id,
name: flow.name,
commands: flow.commands.map((cmd) => {
const cmdCopy = { ...cmd };
//
const uselessProps = [
"config",
"code",
"label",
"component",
"subCommands",
"options",
"defaultValue",
"icon",
"width",
"placeholder",
];
uselessProps.forEach((prop) => delete cmdCopy[prop]);
return cmdCopy;
}),
}));
localStorage.setItem("quickcomposer.flows", JSON.stringify(flowsData));
quickcommand.showMessageBox("保存成功");
},
loadFlows() {
const savedFlows = localStorage.getItem("quickcomposer.flows");
if (!savedFlows) return;
const flowsData = JSON.parse(savedFlows);
this.flows = flowsData.map((flow) => ({
...flow,
commands: flow.commands.map((cmd) => {
const command = findCommandByValue(cmd.value);
return {
...command,
...cmd,
};
}),
}));
this.activeTab = this.flows[0].id;
},
runFlows(flow) {
const code = flow
? this.generateFlowCode(flow)
: this.generateAllFlowCode();
this.$emit("action", "run", code);
if (!code.includes("console.log")) {
quickcommand.showMessageBox("已运行");
}
},
collapseAll() {
this.$refs.flowRefs.forEach((flow) => {
if (flow.collapseAll) flow.collapseAll();
});
this.isAllCollapsed = true;
},
expandAll() {
this.$refs.flowRefs.forEach((flow) => {
if (flow.expandAll) flow.expandAll();
});
this.isAllCollapsed = false;
},
},
});
</script>
<style scoped>
.flow-tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.tabs-header {
flex-shrink: 0;
height: 28px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.tabs-container {
flex: 1;
display: flex;
align-items: center;
height: 100%;
min-width: 0;
margin-right: 8px;
}
/* 限制在当前组件内的tabs样式 */
.tabs-container :deep(.q-tabs) {
flex: 1;
height: 28px;
min-height: 28px;
overflow-x: auto;
overflow-y: hidden;
margin-left: 4px; /* 与 main 按钮保持间距 */
}
/* 隐藏滚动条 */
.tabs-container :deep(.q-tabs)::-webkit-scrollbar {
display: none;
}
.tabs-container :deep(.q-tab) {
min-height: 28px;
height: 28px;
padding: 0 8px;
}
.tabs-container :deep(.q-tab__content) {
min-width: 0;
}
.flow-tab {
min-width: 80px;
}
.flow-tab-content {
display: flex;
align-items: center;
gap: 4px;
}
.flow-name-input {
max-width: 100px;
}
.flow-name-input :deep(.q-field__native) {
padding: 0;
font-size: 12px;
}
/* 添加按钮样式 */
.tabs-container .q-btn {
height: 28px;
min-height: 28px;
}
.add-btn {
flex-shrink: 0;
height: 28px;
min-height: 28px;
}
.flow-container {
flex: 1;
position: relative;
overflow: hidden;
}
.body--dark .tabs-header {
border-bottom-color: rgba(255, 255, 255, 0.05);
}
.main-btn {
height: 28px;
min-height: 28px;
padding: 0 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin-right: 4px;
flex-shrink: 0;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="composer-buttons">
<div class="left-buttons">
<div class="right-buttons">
<q-btn
:icon="isAllCollapsed ? 'unfold_more' : 'unfold_less'"
dense
@ -11,9 +11,7 @@
>
<q-tooltip>{{ isAllCollapsed ? "展开所有" : "折叠所有" }}</q-tooltip>
</q-btn>
</div>
<div class="right-buttons">
<q-separator vertical />
<q-btn
@click="$q.dark.toggle()"
:icon="$q.dark.isActive ? 'dark_mode' : 'light_mode'"
@ -135,24 +133,25 @@ export default defineComponent({
.composer-buttons {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 28px;
}
.left-buttons,
.right-buttons {
display: flex;
align-items: center;
height: 100%;
}
.composer-buttons > div > .q-btn {
.right-buttons .q-btn {
height: 28px;
min-height: 28px;
opacity: 0.6;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 12px;
}
.composer-buttons > div > .q-btn:hover {
.right-buttons .q-btn:hover {
opacity: 1;
transform: translateY(-1px);
color: var(--q-primary);

View File

@ -1,9 +1,14 @@
export function generateCode(commandFlow) {
export function generateCode(commandFlow, functionName = null) {
// 检查是否包含异步函数
const hasAsyncFunction = commandFlow.some((cmd) => cmd.isAsync);
let code = hasAsyncFunction ? ["async function run() {"] : [];
const indent = hasAsyncFunction ? " " : "";
let code = [];
const funcName = functionName || "run";
// 生成函数声明
code.push(`${hasAsyncFunction ? "async " : ""}function ${funcName}() {`);
const indent = " ";
commandFlow.forEach((cmd) => {
// 跳过禁用的命令
@ -20,9 +25,11 @@ export function generateCode(commandFlow) {
code.push(line);
});
if (hasAsyncFunction) {
code.push("}"); // Close the async function
code.push("run();"); // Call the function
code.push("}"); // Close the function
// 如果是主函数,则自动执行
if (functionName === "main") {
code.push("\nmain();"); // Call the main function
}
return code.join("\n");