automated_admin/src/modules/flow/components/tools/card.vue
2025-01-09 17:58:24 +08:00

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>