587 lines
14 KiB
Vue

<template>
<div class="flow-tabs">
<div class="tabs-header">
<div class="tabs-container">
<!-- main 作为固定按钮 -->
<q-btn
flat
dense
label="主流程"
class="main-btn"
@dblclick="editFunction(mainFlow)"
:class="{ 'main-btn-active': activeTab === 'main' }"
@click="activeTab = 'main'"
>
<q-tooltip>双击管理</q-tooltip>
</q-btn>
<!-- 其他流程标签可滚动 -->
<q-tabs
v-model="activeTab"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="left"
narrow-indicator
outside-arrows
>
<draggable
v-model="subFlows"
:animation="150"
item-key="id"
tag="div"
class="row no-wrap"
handle=".flow-tab-content"
>
<template #item="{ element: flow }">
<q-tab :name="flow.id" class="flow-tab" no-caps>
<div class="flow-tab-content">
<span class="flow-name-text" @dblclick="editFunction(flow)">
{{ flow.label }}
</span>
<q-btn
flat
dense
round
icon="close"
size="xs"
@click.stop="removeFlow(flow)"
/>
</div>
<q-tooltip> 双击管理 </q-tooltip>
</q-tab>
</template>
</draggable>
</q-tabs>
<q-icon dense name="add" class="add-btn" @click="addFlow" />
</div>
<ComposerButtons
:is-all-collapsed="isAllCollapsed"
:disabled-buttons="disabledControlButtons"
:flows="flows"
@action="handleAction"
/>
</div>
<div
class="flow-container"
v-for="flow in flows"
:key="flow.id"
v-show="activeTab === flow.id"
>
<CommandConfig
class="command-config-panel"
v-if="flow.id === 'main' && showCommandConfig"
:model-value="commandConfig"
@update:model-value="updateCommandConfig"
/>
<ComposerFlow
class="flow-wrapper"
v-model="flow.commands"
:show-close-button="flows.length > 1"
@action="(type, payload) => handleAction(type, payload)"
ref="flowRefs"
/>
<FlowManager
v-model="showFlowManager"
:flow="flow"
:variables="flow.customVariables"
@update-flow="updateFlows(flow)"
:is-main-flow="flow.id === 'main'"
:output-variables="outputVariables"
class="variable-panel"
/>
</div>
</div>
</template>
<script>
import { defineComponent, provide, ref, computed } from "vue";
import draggable from "vuedraggable";
import ComposerFlow from "components/composer/ComposerFlow.vue";
import ComposerButtons from "components/composer/flow/ComposerButtons.vue";
import FlowManager from "components/composer/flow/FlowManager.vue";
import CommandConfig from "components/editor/CommandConfig.vue";
import { generateUniqSuffix } from "js/composer/variableManager";
import { getUniqueId } from "js/common/uuid";
export default defineComponent({
name: "FlowTabs",
components: {
ComposerFlow,
ComposerButtons,
draggable,
FlowManager,
CommandConfig,
},
emits: ["update:modelValue", "action"],
props: {
disabledControlButtons: {
type: Array,
default: () => [],
},
modelValue: {
type: Object,
default: () => ({}),
},
},
setup(props, { emit }) {
const updateFlows = (newFlows) => {
emit("update:modelValue", {
...props.modelValue,
flows: newFlows,
});
};
const defaultFlow = [
{
id: "main",
name: "main",
label: "主流程",
commands: [],
customVariables: [],
},
];
const clearFlows = () => {
updateFlows(window.lodashM.cloneDeep(defaultFlow));
activeTab.value = "main";
};
if (!props.modelValue.flows || props.modelValue.flows.length === 0) {
updateFlows(window.lodashM.cloneDeep(defaultFlow));
}
const flows = computed(() => props.modelValue.flows || []);
const commandConfig = computed(() => {
const { tags, output, features, program } = props.modelValue;
return { tags, output, features, program };
});
const mainFlow = computed({
get: () => flows.value[0],
set: (newVal) => {
const newFlows = [newVal, ...flows.value.slice(1)];
updateFlows(newFlows);
},
});
const subFlows = computed({
get: () => flows.value.slice(1),
set: (newVal) => {
const newFlows = [mainFlow.value, ...newVal];
updateFlows(newFlows);
},
});
// 获取所有函数
const getCurrentFunctions = () => {
return subFlows.value.map((flow) => {
return {
label: flow.label,
name: flow.name,
id: flow.id,
};
});
};
provide("getCurrentFunctions", getCurrentFunctions);
const activeTab = ref("main");
const getCurrentFlow = () => {
return flows.value.find((flow) => flow.id === activeTab.value);
};
// 获取当前函数所有输出变量
const getOutputVariables = (flow = getCurrentFlow()) => {
const variables = [];
for (const [index, cmd] of flow.commands.entries()) {
if (cmd.outputVariable && cmd.asyncMode !== "then") {
const { name, details = {} } = cmd.outputVariable;
variables.push(
...[name, ...Object.values(details)]
.filter((v) => v)
.map((variable) => ({
name: variable,
// 提供来源命令的标志信息
sourceCommand: {
label: cmd.label,
id: cmd.id,
index,
},
type: "output",
}))
);
}
}
return variables;
};
const getFunctionParams = (flowId) => {
const flow = flows.value.find((f) => f.id === flowId);
return flow.customVariables.filter((v) => v.type === "param");
};
provide("getFunctionParams", getFunctionParams);
/**
* 获取当前函数所有变量
* 返回格式:
* [
* { name: "变量名", type: "变量类型", sourceCommand: { label: "变量来源" , index?: 来源命令索引, id?: 来源命令id} }
* ]
*
* type:
* 1. output: 输出变量
* 2. param: 函数参数
* 3. var: 局部变量
*/
const getCurrentVariables = () => {
const currentFlow = getCurrentFlow();
const variables = getOutputVariables(currentFlow);
const customVariables = currentFlow.customVariables.map((v) => ({
name: v.name,
type: v.type,
sourceCommand: {
label: v.type === "param" ? "函数参数" : "局部变量",
},
}));
return [...customVariables, ...variables];
};
provide("getCurrentVariables", getCurrentVariables);
return {
flows,
mainFlow,
subFlows,
commandConfig,
activeTab,
getOutputVariables,
updateFlows,
clearFlows,
};
},
data() {
return {
isAllCollapsed: false,
showFlowManager: false,
outputVariables: [],
};
},
computed: {
isRunComposerPage() {
return this.$route.name === "composer";
},
showCommandConfig() {
return !this.isRunComposerPage && this.commandConfig.features;
},
},
methods: {
generateFlowName(baseName = "func_") {
return (
baseName +
generateUniqSuffix(
baseName,
this.flows.map((f) => f.name),
false
)
);
},
addFlow(options = {}) {
const id = getUniqueId();
const name = options.name || this.generateFlowName();
const newFlow = {
id,
name,
label: name.replace("func_", "函数"),
commands: [],
customVariables: [],
};
// 添加函数参数
if (options.params) {
options.params.forEach((param) => {
newFlow.customVariables.push({
name: param,
type: "param",
});
});
}
this.subFlows = [...this.subFlows, newFlow];
if (options.silent) {
return;
}
this.activeTab = id;
this.$nextTick(() => {
this.toggleFlowManager();
});
},
removeFlow(flow) {
const index = this.subFlows.findIndex((f) => f.id === flow.id);
if (index > -1) {
this.subFlows = [
...this.subFlows.slice(0, index),
...this.subFlows.slice(index + 1),
];
this.activeTab = this.flows[0].id;
}
},
updateSubFlow(index, payload) {
const { params } = payload;
const newParams = params.map((param) => ({
name: param,
type: "param",
}));
const localVars = this.subFlows[index].customVariables.filter(
(v) => v.type === "var"
);
// 完全更新参数
this.subFlows[index].customVariables = [...newParams, ...localVars];
},
handleAction(type, payload) {
switch (type) {
case "save":
this.saveFlows();
break;
case "run":
this.runCommand(payload);
break;
case "collapseAll":
this.collapseAll();
break;
case "expandAll":
this.expandAll();
break;
case "toggleFlowManager":
this.toggleFlowManager();
break;
case "close":
this.$emit("action", "close");
break;
case "apply":
this.$emit("action", "apply", payload);
break;
case "addFlow":
// 处理新函数创建
const index = this.subFlows.findIndex((f) => f.name === payload.name);
if (index > -1) {
// 如果函数已存在,则更新
this.updateSubFlow(index, payload);
} else {
this.addFlow(payload);
}
break;
case "clear":
this.clearFlows();
break;
default:
this.$emit("action", type, payload);
}
},
toggleFlowManager() {
this.showFlowManager = !this.showFlowManager;
this.outputVariables = this.getOutputVariables();
},
saveFlows() {
this.$emit("action", "save", {
...this.modelValue,
flows: this.flows,
});
},
runCommand(flows = this.flows) {
const command = {
...this.modelValue,
flows,
};
this.$emit("action", "run", command);
},
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;
},
editFunction(flow) {
this.activeTab = flow.id;
this.toggleFlowManager();
},
updateCommandConfig(newVal) {
const newModelValue = {
...this.modelValue,
...newVal,
};
this.$emit("update:modelValue", newModelValue);
},
},
});
</script>
<style scoped>
.flow-tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.tabs-header {
flex-shrink: 0;
height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.tabs-container {
flex: 1;
display: flex;
align-items: center;
height: 100%;
min-width: 0;
width: 0;
}
/* 限制在当前组件内的tabs样式 */
.tabs-container :deep(.q-tabs) {
height: 30px;
min-height: 30px;
min-width: 0;
width: auto;
max-width: 100%;
}
.body--dark .tabs-container :deep(.q-tabs) {
background-color: #232323 !important;
}
.tabs-container :deep(.q-tab) {
min-height: 30px;
height: 30px;
padding: 0;
text-align: center;
background-color: transparent;
}
.tabs-container
:deep(.q-tabs--scrollable.q-tabs__arrows--outside.q-tabs--horizontal) {
padding: 0 15px;
}
.tabs-container :deep(.q-tabs__arrow) {
min-width: 0;
width: 15px;
font-size: 15px;
}
.tabs-container :deep(.q-tab__content) {
min-width: 0;
width: 100%;
}
.flow-tab {
min-width: 80px;
max-width: 120px;
}
.flow-tab-content {
display: flex;
align-items: center;
user-select: none;
width: 100%;
}
.flow-name-input {
max-width: 100%;
min-width: 0;
}
.flow-name-input :deep(.q-field__control),
.flow-name-input :deep(.q-field__control *) {
font-size: 12px;
height: 30px;
text-align: center;
}
.flow-name-text {
font-size: 12px;
padding: 0 2px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
/* 添加按钮样式 */
.add-btn {
flex-shrink: 0;
padding: 0 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.add-btn:hover {
color: var(--q-primary);
transform: scale(1.2);
transition: all 0.2s ease-in-out;
}
.flow-container {
flex: 1;
position: relative;
overflow: hidden;
}
.flow-wrapper {
flex: 1;
position: relative;
overflow: hidden;
height: 100%;
}
.variable-panel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
z-index: 10;
}
.main-btn {
height: 30px;
min-height: 30px;
padding: 0 12px;
border-radius: 8px 0 0 0;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
color: var(--q-text-color);
}
.main-btn-active {
color: var(--q-primary);
border-bottom: 2px solid var(--q-primary);
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.command-config-panel {
padding: 8px;
}
</style>