510 lines
9.8 KiB
Vue
510 lines
9.8 KiB
Vue
<template>
|
|
<div
|
|
class="tools-card"
|
|
:class="[
|
|
`is-${result.status}`,
|
|
{
|
|
'is-active': isActive,
|
|
'is-moving': node.isMoving
|
|
}
|
|
]"
|
|
>
|
|
<!-- 源选择点 -->
|
|
<tools-handle
|
|
type="source"
|
|
id="source"
|
|
:node-id="nodeId"
|
|
:position="{
|
|
right: '-9px',
|
|
top: '14px'
|
|
}"
|
|
v-if="node?.handle?.source ?? true"
|
|
/>
|
|
|
|
<!-- 目标选择点 -->
|
|
<tools-handle
|
|
type="target"
|
|
id="target"
|
|
:node-id="nodeId"
|
|
:position="{
|
|
left: '-9px',
|
|
top: '14px'
|
|
}"
|
|
v-if="node?.handle?.target ?? true"
|
|
/>
|
|
|
|
<div class="head">
|
|
<tools-icon :name="node?.icon" :color="node?.color" />
|
|
|
|
<el-input
|
|
v-if="isActive"
|
|
class="label"
|
|
v-model="flow.node!.label"
|
|
placeholder="标题"
|
|
/>
|
|
<span v-else>{{ node?.label }}</span>
|
|
|
|
<div class="btns" @click.stop>
|
|
<template v-if="!isStart">
|
|
<!-- <tools-nodes :node="node" is-change>
|
|
<cl-svg name="change" />
|
|
</tools-nodes> -->
|
|
|
|
<el-tooltip content="调试" placement="top" v-if="isRun && isActive">
|
|
<cl-svg class="btn-icon is-bg" name="run" @click="run" />
|
|
</el-tooltip>
|
|
|
|
<el-tooltip content="删除" placement="top">
|
|
<cl-svg
|
|
name="delete"
|
|
:style="{
|
|
marginLeft: 'auto'
|
|
}"
|
|
@click="remove"
|
|
/>
|
|
</el-tooltip>
|
|
|
|
<!-- <el-tooltip content="关闭" placement="top">
|
|
<cl-svg
|
|
v-if="isActive"
|
|
class="btn-icon is-bg"
|
|
name="close"
|
|
@click.stop="close"
|
|
/>
|
|
</el-tooltip> -->
|
|
</template>
|
|
|
|
<!-- <el-tooltip :content="node?.description" placement="top">
|
|
<cl-svg name="info" />
|
|
</el-tooltip> -->
|
|
|
|
<tools-more :node="node">
|
|
<cl-svg name="more" />
|
|
</tools-more>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="desc">
|
|
<el-input
|
|
v-if="isActive"
|
|
v-model="flow.node!.desc"
|
|
:placeholder="node?.description"
|
|
clearable
|
|
/>
|
|
<span class="text" v-else>{{ node?.desc || node?.description }}</span>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<component :is="component()" :node="node" :focus="isActive" />
|
|
<card-form :ref="setRefs('form')" v-if="isActive" />
|
|
</div>
|
|
|
|
<div class="tips" v-if="result.status !== 'none'">
|
|
<el-icon class="is-loading" v-if="result.status == 'running'">
|
|
<loading />
|
|
</el-icon>
|
|
<span>{{ result.message }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts" name="tools-card">
|
|
import CardForm from "./card/form.vue";
|
|
import { useFlow } from "/$/flow/hooks";
|
|
import { computed, onMounted, onUnmounted, reactive, watch } from "vue";
|
|
import ToolsIcon from "./icon.vue";
|
|
import ToolsHandle from "./handle.vue";
|
|
import ToolsMore from "./more.vue";
|
|
import { useCool } from "/@/cool";
|
|
import { Loading } from "@element-plus/icons-vue";
|
|
import type { FlowNode, FlowNodeResult } from "/$/flow/types";
|
|
import { nextTick } from "vue";
|
|
|
|
const props = defineProps({
|
|
nodeId: String
|
|
});
|
|
|
|
const { mitt, refs, setRefs } = useCool();
|
|
const flow = useFlow();
|
|
|
|
// 是否可调试
|
|
const isRun = computed(() => {
|
|
return flow.node ? !["start", "end"].includes(flow.node.type!) : false;
|
|
});
|
|
|
|
// 最小宽
|
|
const width = computed(() => {
|
|
return flow.node?.form?.width || "400px";
|
|
});
|
|
|
|
// 节点数据
|
|
const node = computed(() => {
|
|
return flow.findNode(props.nodeId!);
|
|
});
|
|
|
|
// 是否开始节点
|
|
const isStart = computed(() => {
|
|
return node.value?.type == "start";
|
|
});
|
|
|
|
// 是否激活
|
|
const isActive = computed(() => {
|
|
return node.value?.id == flow.node?.id;
|
|
});
|
|
|
|
// 节点组件
|
|
function component() {
|
|
return flow.CustomNodes.find((e) => e.type == node.value?.type)?.component;
|
|
}
|
|
|
|
// 调试
|
|
function run() {
|
|
mitt.emit("flow.runOpen", flow.node);
|
|
}
|
|
|
|
// 移除节点
|
|
function remove() {
|
|
flow.removeNodes(node.value);
|
|
}
|
|
|
|
// 关闭表单
|
|
function close() {
|
|
flow.enableDrag();
|
|
flow.clearNode();
|
|
}
|
|
|
|
// 获取颜色值
|
|
function getColor(color: string) {
|
|
return getComputedStyle(document.documentElement).getPropertyValue(`--el-color-${color}`);
|
|
}
|
|
|
|
// 节点运行结果
|
|
const result = reactive({
|
|
status: "none" as "none" | "running" | "waiting" | "success" | "fail",
|
|
message: "",
|
|
clear() {
|
|
result.status = "none";
|
|
result.message = "";
|
|
result.updateEdge({ animated: false, style: {} });
|
|
},
|
|
check() {
|
|
result.clear();
|
|
},
|
|
ready(_, node?: FlowNode) {
|
|
function done() {
|
|
result.status = "waiting";
|
|
result.message = "等待中...";
|
|
}
|
|
|
|
if (node) {
|
|
if (props.nodeId == node.id) {
|
|
done();
|
|
}
|
|
} else {
|
|
const startNode = flow.findNodeByType("start");
|
|
|
|
if (startNode) {
|
|
const childrens = flow.childrenAllNodes(startNode.id!);
|
|
|
|
if (childrens.find((e) => e.id == props.nodeId) || startNode.id == props.nodeId) {
|
|
done();
|
|
|
|
result.updateEdge({
|
|
animated: true,
|
|
style: {
|
|
stroke: getColor("info")
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
start(nodeId: string) {
|
|
if (nodeId == props.nodeId) {
|
|
flow.setViewportByNode(flow.findNode(nodeId));
|
|
|
|
result.status = "running";
|
|
result.message = "运行中...";
|
|
|
|
result.updateEdge({
|
|
style: {
|
|
stroke: getColor("primary")
|
|
}
|
|
});
|
|
}
|
|
},
|
|
result(res: FlowNodeResult, node?: FlowNode) {
|
|
const nodeId = node?.id || res.nodeId;
|
|
|
|
if (nodeId == props.nodeId) {
|
|
// 设置节点状态
|
|
if (res.result.success) {
|
|
const duration = parseFloat(((res.duration || 1) / 1000).toFixed(3));
|
|
|
|
result.status = "success";
|
|
result.message = `执行成功,耗时:${duration}s`;
|
|
|
|
// 下一个准备
|
|
if (!node) {
|
|
if (res.nextNodeId != nodeId) {
|
|
mitt.emit("flow.run", { action: "start", data: res.nextNodeId });
|
|
}
|
|
}
|
|
} else {
|
|
result.status = "fail";
|
|
result.message = res.result.error.message;
|
|
|
|
// 设置视角
|
|
flow.setViewportByNode(flow.findNode(res.nodeId!));
|
|
}
|
|
|
|
// 非单节点需要控制线的样式
|
|
if (!node) {
|
|
// 更新线颜色、动画
|
|
result.updateEdge({
|
|
animated: true,
|
|
style: {
|
|
stroke: res.result.success ? getColor("success") : getColor("danger")
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
fail({ nodeId, message }: { nodeId: string; message: string }) {
|
|
if (props.nodeId == nodeId) {
|
|
result.status = "fail";
|
|
result.message = message;
|
|
flow.setViewportByNode(flow.findNode(nodeId));
|
|
}
|
|
},
|
|
end() {
|
|
if (["waiting", "running"].includes(result.status)) {
|
|
result.close();
|
|
}
|
|
},
|
|
close() {
|
|
result.clear();
|
|
},
|
|
updateEdge(data: any) {
|
|
const edge = flow.edges.find((e) => e.target == props.nodeId);
|
|
|
|
if (edge) {
|
|
flow.updateEdge(edge.id, data);
|
|
}
|
|
},
|
|
onRun({ action, data, node }: { action: string; data: any; node?: FlowNode }) {
|
|
result[action](data, node);
|
|
}
|
|
});
|
|
|
|
// 打开表单
|
|
async function openForm(data: FlowNode) {
|
|
nextTick(() => {
|
|
if (data) {
|
|
if (data.id == props.nodeId) {
|
|
refs.form?.open();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 关闭表单
|
|
function closeForm(data: FlowNode) {
|
|
nextTick(() => {
|
|
if (data?.id == props.nodeId) {
|
|
refs.form?.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
onMounted(() => {
|
|
mitt.on("flow.openForm", openForm);
|
|
mitt.on("flow.closeForm", closeForm);
|
|
mitt.on("flow.run", result.onRun);
|
|
mitt.on("flow.runClose", result.close);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
mitt.off("flow.openForm", openForm);
|
|
mitt.off("flow.closeForm", closeForm);
|
|
mitt.off("flow.run", result.onRun);
|
|
mitt.off("flow.runClose", result.close);
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.tools-card {
|
|
border-radius: 12px;
|
|
background-color: var(--el-bg-color);
|
|
width: 300px;
|
|
position: relative;
|
|
border: 2px solid var(--el-fill-color-light);
|
|
box-sizing: border-box;
|
|
transition: all 0.2s;
|
|
|
|
.btns {
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 10px;
|
|
box-sizing: border-box;
|
|
// border: 1px solid var(--el-fill-color-light);
|
|
background-color: var(--el-bg-color);
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
padding: 3px;
|
|
box-sizing: border-box;
|
|
|
|
.cl-svg {
|
|
color: var(--el-text-color-regular);
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
height: 15px;
|
|
width: 15px;
|
|
padding: 4px;
|
|
|
|
&:hover,
|
|
&:focus {
|
|
background-color: var(--el-fill-color-light);
|
|
}
|
|
|
|
&:focus {
|
|
outline: none;
|
|
}
|
|
}
|
|
|
|
&:has(.cl-svg:focus) {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
.head {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 15px;
|
|
height: 44px;
|
|
|
|
.icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 20px;
|
|
width: 20px;
|
|
border-radius: 4px;
|
|
background-color: var(--el-color-primary);
|
|
color: #fff;
|
|
}
|
|
|
|
span {
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
margin-left: 10px;
|
|
line-height: 1;
|
|
}
|
|
|
|
:deep(.el-input__wrapper) {
|
|
box-shadow: none;
|
|
padding: 0 5px;
|
|
|
|
.el-input__inner {
|
|
font-weight: bold;
|
|
padding: 0 5px;
|
|
border-radius: 4px;
|
|
|
|
&:hover {
|
|
background-color: var(--el-fill-color-lighter);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.container {
|
|
padding: 0 15px;
|
|
}
|
|
|
|
.desc {
|
|
padding: 0 15px 15px 15px;
|
|
font-size: 12px;
|
|
color: var(--el-color-info);
|
|
word-break: break-all;
|
|
|
|
:deep(.el-input__wrapper) {
|
|
box-shadow: none;
|
|
font-size: 12px;
|
|
background-color: var(--el-fill-color-lighter);
|
|
border-radius: var(--el-border-radius-base);
|
|
}
|
|
}
|
|
|
|
.tips {
|
|
padding: 0 15px 15px 15px;
|
|
font-size: 12px;
|
|
|
|
.is-loading {
|
|
font-size: 14px;
|
|
position: relative;
|
|
top: 2px;
|
|
margin-right: 3px;
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
box-shadow:
|
|
0px 4px 6px -2px rgba(16, 24, 40, 0.03),
|
|
0px 12px 16px -4px rgba(16, 24, 40, 0.08);
|
|
|
|
.btns {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
&.is-active {
|
|
width: calc(v-bind("width") + 30px);
|
|
border-color: var(--el-color-primary);
|
|
}
|
|
|
|
&.is-waiting,
|
|
&.is-running {
|
|
border-color: var(--el-color-info);
|
|
|
|
.tips {
|
|
color: var(--el-color-info);
|
|
}
|
|
}
|
|
|
|
&.is-running {
|
|
border-color: var(--el-color-primary);
|
|
|
|
.tips {
|
|
color: var(--el-color-primary);
|
|
}
|
|
}
|
|
|
|
&.is-fail {
|
|
border-color: var(--el-color-danger);
|
|
|
|
.tips {
|
|
color: var(--el-color-danger);
|
|
}
|
|
|
|
:deep(.rod) {
|
|
color: var(--el-color-danger);
|
|
}
|
|
}
|
|
|
|
&.is-success {
|
|
border-color: var(--el-color-success);
|
|
|
|
.tips {
|
|
color: var(--el-color-success);
|
|
}
|
|
|
|
:deep(.rod) {
|
|
color: var(--el-color-success);
|
|
}
|
|
}
|
|
}
|
|
</style>
|