feat: 视频播放器
This commit is contained in:
parent
6dfa2be64e
commit
5be800e579
@ -1,103 +0,0 @@
|
|||||||
|
|
||||||
import { GroupNodeModel, GroupNode } from '@logicflow/extension'
|
|
||||||
import { recursiveFilter } from "@/utils/convertNodeRedToLogicFlow";
|
|
||||||
|
|
||||||
class CustomGroupModel extends GroupNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
this.children = new Set(data.children || [])
|
|
||||||
this.zIndex = 0;
|
|
||||||
|
|
||||||
this.width = data.w + 80; // 增加内边距
|
|
||||||
this.height = data.h + 80;
|
|
||||||
this.x = data.x;
|
|
||||||
this.y = data.y;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateSize()
|
|
||||||
const nodes = this.graphModel.nodes;
|
|
||||||
const arr = recursiveFilter(nodes, this.id)
|
|
||||||
arr.forEach((item) => {
|
|
||||||
item.updateSize()
|
|
||||||
})
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
setAttributes() {
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTextStyle() {
|
|
||||||
const style = super.getTextStyle();
|
|
||||||
style.fill = this.properties.style?.color || '#333';
|
|
||||||
style.fontSize = 16;
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize() {
|
|
||||||
const children = []
|
|
||||||
this.children.forEach((id) => {
|
|
||||||
children.push(this.graphModel.getNodeModelById(id))
|
|
||||||
});
|
|
||||||
if (children.length === 0) return;
|
|
||||||
|
|
||||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
||||||
children.forEach((node) => {
|
|
||||||
minX = Math.min(minX, node.x - node.width / 2);
|
|
||||||
maxX = Math.max(maxX, node.x + node.width / 2);
|
|
||||||
minY = Math.min(minY, node.y - node.height / 2);
|
|
||||||
maxY = Math.max(maxY, node.y + node.height / 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.width = maxX - minX + 80; // 增加内边距
|
|
||||||
this.height = maxY - minY + 80;
|
|
||||||
this.x = (minX + maxX) / 2;
|
|
||||||
this.y = (minY + maxY) / 2;
|
|
||||||
this.setTextCoordinate(this.width, this.height, this.x, this.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTextCoordinate(width, height, x, y) {
|
|
||||||
const strategies = {
|
|
||||||
"n": () => {
|
|
||||||
this.text.x = x
|
|
||||||
this.text.y = y - height / 2 + 20
|
|
||||||
},
|
|
||||||
"ne": () => {
|
|
||||||
this.text.x = x + width / 2 - 20
|
|
||||||
this.text.y = y - height / 2 + 20
|
|
||||||
},
|
|
||||||
"sw": () => {
|
|
||||||
this.text.x = x - width / 2 + 20
|
|
||||||
this.text.y = y + height / 2 - 20
|
|
||||||
},
|
|
||||||
"s": () => {
|
|
||||||
this.text.x = x
|
|
||||||
this.text.y = y + height / 2 - 20
|
|
||||||
},
|
|
||||||
"se": () => {
|
|
||||||
this.text.x = x + width / 2 - 20
|
|
||||||
this.text.y = y + height / 2 - 20
|
|
||||||
},
|
|
||||||
"default": () => {
|
|
||||||
this.text.x = x - width / 2 + 20
|
|
||||||
this.text.y = y - height / 2 + 20
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const labelPosition = this.properties.style?.['label-position'] || 'default'
|
|
||||||
const action = strategies[labelPosition]
|
|
||||||
action();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeChild(id) {
|
|
||||||
this.children.delete(id) // 现在可安全调用
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerCustomGroup(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "customGroup",
|
|
||||||
view: GroupNode,
|
|
||||||
model: CustomGroupModel
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,238 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="custom_element" :class="props.properties.state">
|
|
||||||
<div class="custom_element__header">
|
|
||||||
<div class="custom_element__title">
|
|
||||||
{{ props.text || "--" }}
|
|
||||||
</div>
|
|
||||||
<el-button v-if="showDetailTypeList.includes(props.properties.type)" v-popover="popoverRef" class="custom_element__btn" link @click="openPop"
|
|
||||||
>详情</el-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="nodeType">节点类型: {{ props.properties.type }}</div>
|
|
||||||
<div class="nodeState">
|
|
||||||
<div>状态:{{ stateMap[props.properties.state] }}</div>
|
|
||||||
</div>
|
|
||||||
<el-popover
|
|
||||||
ref="popoverRef"
|
|
||||||
virtual-triggering
|
|
||||||
persistent
|
|
||||||
placement="right-start"
|
|
||||||
:visible="popoverVisible"
|
|
||||||
popper-class="popover__container"
|
|
||||||
>
|
|
||||||
<template #default>
|
|
||||||
<div class="popover__header">
|
|
||||||
<div>{{ props.text || "--" }}</div>
|
|
||||||
<div class="closeBtn" @click="popoverVisible = !popoverVisible">
|
|
||||||
<el-icon><CircleClose /></el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="popover__context">
|
|
||||||
<div class="container">
|
|
||||||
<div class="title">输入参数</div>
|
|
||||||
<el-input
|
|
||||||
v-if="getTypeByParams(props.properties.input) === 0"
|
|
||||||
v-model="props.properties.input"
|
|
||||||
:readonly="true"
|
|
||||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
|
||||||
type="textarea"
|
|
||||||
/>
|
|
||||||
<div v-if="getTypeByParams(props.properties.input) === 2">
|
|
||||||
<el-image
|
|
||||||
style="width: 200px; height: 100px"
|
|
||||||
:src="getUrlByParams(props.properties.input)"
|
|
||||||
:zoom-rate="2"
|
|
||||||
:max-scale="7"
|
|
||||||
:min-scale="0.2"
|
|
||||||
:preview-src-list="[getUrlByParams(props.properties.input)]"
|
|
||||||
show-progress
|
|
||||||
:initial-index="4"
|
|
||||||
fit="cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="title">输出参数</div>
|
|
||||||
<el-input
|
|
||||||
v-if="getTypeByParams(props.properties.output) === 0"
|
|
||||||
v-model="props.properties.output"
|
|
||||||
:readonly="true"
|
|
||||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
|
||||||
type="textarea"
|
|
||||||
/>
|
|
||||||
<div v-if="getTypeByParams(props.properties.output) === 2" class="imageContainer">
|
|
||||||
<el-image
|
|
||||||
v-for="item in getUrlByParams(props.properties.output)"
|
|
||||||
style="width: 60px; height: 60px"
|
|
||||||
:src="item"
|
|
||||||
:preview-src-list="getUrlByParams(props.properties.output)"
|
|
||||||
show-progress
|
|
||||||
fit="fill"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="getTypeByParams(props.properties.output) === 4" class="videoContainer">
|
|
||||||
<IPlayer v-for="item in getUrlByParams(props.properties.output)" :videoUrl="item" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-popover>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { watch } from "vue";
|
|
||||||
import { CircleClose } from '@element-plus/icons-vue'
|
|
||||||
import IPlayer from './IPlayer/index.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
model: Object,
|
|
||||||
graphModel: Object,
|
|
||||||
properties: Object,
|
|
||||||
text: String,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stateMap = {
|
|
||||||
success: "已执行",
|
|
||||||
unexecuted: "未执行",
|
|
||||||
running: "执行中",
|
|
||||||
failed: "异常",
|
|
||||||
paused: "暂停",
|
|
||||||
stopped: "停止"
|
|
||||||
};
|
|
||||||
|
|
||||||
const showDetailTypeList = ['inject', 'startCamera', 'getImage', 'stopCamera', 'process', 'end', 'stopVideo']
|
|
||||||
|
|
||||||
const popoverVisible = ref(false);
|
|
||||||
const popoverRef = ref();
|
|
||||||
|
|
||||||
const openPop = (e) => {
|
|
||||||
popoverVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTypeByParams = (str) => {
|
|
||||||
let type = 0;
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(str);
|
|
||||||
type = data?.type || 0;
|
|
||||||
} catch {
|
|
||||||
type = 0;
|
|
||||||
}
|
|
||||||
return type;
|
|
||||||
};
|
|
||||||
|
|
||||||
const propertiesTypeMap = {
|
|
||||||
'2': 'imageUrl',
|
|
||||||
'4': 'videoUrl',
|
|
||||||
'default': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUrlByParams = (str) => {
|
|
||||||
let url = [];
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(str);
|
|
||||||
url = data?.[propertiesTypeMap[data?.type || 'default']] || 0;
|
|
||||||
} catch {
|
|
||||||
url = [];
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.properties.nowTime,
|
|
||||||
() => {
|
|
||||||
popoverVisible.value = false;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.custom_element {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__btn {
|
|
||||||
color: skyblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.running, .paused, .stopped {
|
|
||||||
background-color: #8bbf86;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unexecuted {
|
|
||||||
background-color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.failed {
|
|
||||||
background-color: rgb(128, 41, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeType {
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState {
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style lang="scss">
|
|
||||||
.popover__container {
|
|
||||||
width: 300px !important;
|
|
||||||
|
|
||||||
.popover__header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.closeBtn {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover__context {
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imageContainer {
|
|
||||||
width: 260px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: stretch;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoContainer {
|
|
||||||
width: 260px;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from '@logicflow/core';
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h } from 'vue';
|
|
||||||
import ElementPlus from 'element-plus'
|
|
||||||
// 导入 Vue 组件
|
|
||||||
import CustomFlow from './customFlow.vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class CustomFlowHtmlNode extends HtmlNode {
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.isMounted = false;
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(CustomFlow, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
text: props.model.text.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
setHtml(rootEl) {
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
const node = document.createElement('div');
|
|
||||||
node.style.width = '100%'
|
|
||||||
node.style.height = '100%'
|
|
||||||
rootEl.appendChild(node);
|
|
||||||
this.app.use(ElementPlus) // 关键:单独注册ElementPlus
|
|
||||||
this.app.mount(node);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class CustomFlowHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
// 仅允许从底部锚点连出
|
|
||||||
this.sourceRules.push({
|
|
||||||
message: '只能从输出锚点连线',
|
|
||||||
validate: (sourceNode, targetNode, sourceAnchor) =>
|
|
||||||
// sourceAnchor.name === 'right'
|
|
||||||
sourceAnchor.properties.connectionType === 'source'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 仅允许连接到顶部锚点
|
|
||||||
this.targetRules.push({
|
|
||||||
message: '只能连接到输入锚点',
|
|
||||||
validate: (sourceNode, targetNode, targetAnchor) =>
|
|
||||||
// targetAnchor.name === 'left'
|
|
||||||
targetAnchor.properties.connectionType === 'target'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
this.width = 200;
|
|
||||||
this.height = 100;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this, x = _a.x, y = _a.y, width = _a.width, height = _a.height;
|
|
||||||
return [
|
|
||||||
// { x: x, y: y - height / 2, id: "".concat(this.id, "_0") },
|
|
||||||
{ x: x + width / 2, y: y, name: 'left', id: "".concat(this.id, "_1"), properties: { connectionType: 'target' } },
|
|
||||||
// { x: x, y: y + height / 2, id: "".concat(this.id, "_2") },
|
|
||||||
{ x: x - width / 2, y: y, name: 'right', id: "".concat(this.id, "_3"), properties: { connectionType: 'source' } },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = 'none';
|
|
||||||
style.hover.stroke = 'none';
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerCustomNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "CustomNode",
|
|
||||||
view: CustomFlowHtmlNode,
|
|
||||||
model: CustomFlowHtmlModel
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flow_container">
|
|
||||||
<div class="btn_container">
|
|
||||||
<div class="instState">实例状态:<span :class="instStatus">{{ stateMap[instStatus]}}</span></div>
|
|
||||||
<el-button key=" translateCenter" type="primary" @click="() => lfRef.translateCenter()">居中</el-button>
|
|
||||||
<el-button key="fitView" type="primary" @click="() => lfRef.fitView()">适应屏幕</el-button>
|
|
||||||
<el-button v-if="['RUNNING','PAUSED'].includes(instStatus)" type="primary" @click="stopFlow">停止</el-button>
|
|
||||||
<el-button v-if="instStatus === 'RUNNING'" type="primary" @click="pauseFlow">暂停</el-button>
|
|
||||||
<el-button v-if="instStatus === 'PAUSED'" type="primary" @click="restartFlow">恢复</el-button>
|
|
||||||
</div>
|
|
||||||
<div ref="containerRef" class="containerRef"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import LogicFlow from "@logicflow/core";
|
|
||||||
import { Group } from "@logicflow/extension";
|
|
||||||
import "@logicflow/core/es/index.css";
|
|
||||||
import { convertNodeRedToLogicFlow, recursiveFilter, changeOtherNodeState } from '@/utils/convertNodeRedToLogicFlow'
|
|
||||||
import { registerCustomNode } from "./customFlowNode";
|
|
||||||
import { registerCustomGroup } from "./CustomGroup";
|
|
||||||
import { getLogicFlow } from '@/api/test/log'
|
|
||||||
import { pauseInst, resumeInst, stopInst } from '@/api/device/terminal'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
const containerRef = ref(null);
|
|
||||||
const lfRef = ref(null);
|
|
||||||
// 在新窗口的脚本中
|
|
||||||
const queryParams = new URLSearchParams(window.location.search);
|
|
||||||
const instId = queryParams.get('instId') || '';
|
|
||||||
const taskId = queryParams.get('taskId') || '';
|
|
||||||
const itemId = queryParams.get('itemId') || '03e0842c2fd7ab7575636a473aeefc89';
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: "custom-node-1",
|
|
||||||
text: "node-1",
|
|
||||||
type: "CustomNode",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'custom-node-2',
|
|
||||||
type: 'CustomNode',
|
|
||||||
x: 300,
|
|
||||||
y: 300,
|
|
||||||
text: 'node-2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// 边
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
type: 'polyline',
|
|
||||||
sourceNodeId: 'custom-node-1',
|
|
||||||
targetNodeId: 'custom-node-2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataNodes = ref([])
|
|
||||||
|
|
||||||
const loadNodeRedData = async () => {
|
|
||||||
const {nodes, edges} = await convertNodeRedToLogicFlow(itemId)
|
|
||||||
lfRef.value.render({
|
|
||||||
nodes,
|
|
||||||
edges
|
|
||||||
})
|
|
||||||
dataNodes.value = nodes
|
|
||||||
setTimeout(() => {
|
|
||||||
lfRef.value.fitView()
|
|
||||||
}, 20)
|
|
||||||
getLog()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLog = async () => {
|
|
||||||
const res = await getLogicFlow({
|
|
||||||
instId,
|
|
||||||
taskId,
|
|
||||||
itemId
|
|
||||||
})
|
|
||||||
if (res.code === 200) {
|
|
||||||
const arr = res.data.filter(item => {
|
|
||||||
return item.nodeType
|
|
||||||
})
|
|
||||||
const map = new Map()
|
|
||||||
arr.forEach(item => {
|
|
||||||
if (['inject', 'start'].includes(item.nodeType)) { // , 'end'
|
|
||||||
map.set(item.nodeId, {
|
|
||||||
state: 'success',
|
|
||||||
nodeType: item.nodeType,
|
|
||||||
input: item.params
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let obj
|
|
||||||
if (map.has(item.nodeId)) {
|
|
||||||
obj = map.get(item.nodeId)
|
|
||||||
} else {
|
|
||||||
obj = {
|
|
||||||
nodeType: item.nodeType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.paramsType === 'INPUT') {
|
|
||||||
obj.state = item.instStatus.toLowerCase(),
|
|
||||||
obj.input = item.instStatus === 'FAILED' ? item.params : item.message
|
|
||||||
} else {
|
|
||||||
obj.state = item.instStatus.toLowerCase() === 'running' ? 'success' : item.instStatus.toLowerCase(),
|
|
||||||
obj.output = item.instStatus === 'FAILED' ? item.message : item.params
|
|
||||||
}
|
|
||||||
map.set(item.nodeId, obj)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [key, value] of map) {
|
|
||||||
lfRef.value.setProperties(key, value)
|
|
||||||
}
|
|
||||||
changeOtherNodeState(dataNodes.value, lfRef.value)
|
|
||||||
if (arr[arr.length - 1].nodeType !== 'end' && arr[arr.length - 1].instStatus !== 'FAILED') {
|
|
||||||
setTimeout(() => {
|
|
||||||
getLog()
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopFlow = async () => {
|
|
||||||
const res = await stopInst(instId)
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('停止成功')
|
|
||||||
getInstDetail()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pauseFlow = async () => {
|
|
||||||
const res = await pauseInst(instId)
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('暂停成功')
|
|
||||||
getInstDetail()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const restartFlow = async () => {
|
|
||||||
const res = await resumeInst(instId)
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('恢复成功')
|
|
||||||
getInstDetail()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const instStatus = ref('RUNNING')
|
|
||||||
const stateMap = {
|
|
||||||
SUCCESS: "已完成",
|
|
||||||
RUNNING: "执行中",
|
|
||||||
FAILED: "失败",
|
|
||||||
PAUSED: "已暂停",
|
|
||||||
STOPPED: "已终止"
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInstDetail = async () => {
|
|
||||||
const res = await getLogicFlow({
|
|
||||||
instId,
|
|
||||||
taskId,
|
|
||||||
itemId
|
|
||||||
})
|
|
||||||
if (res.code === 200) {
|
|
||||||
const item = res.data[res.data.length - 1]
|
|
||||||
instStatus.value = item.instStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (containerRef.value) {
|
|
||||||
const lf = new LogicFlow({
|
|
||||||
container: containerRef.value,
|
|
||||||
isSilentMode: true,
|
|
||||||
adjustNodePosition: true,
|
|
||||||
stopZoomGraph: false,
|
|
||||||
stopScrollGraph: false,
|
|
||||||
stopMoveGraph: false,
|
|
||||||
autoExpand: true,
|
|
||||||
adjustEdgeStartAndEnd: true,
|
|
||||||
allowRotate: false,
|
|
||||||
edgeTextEdit: false,
|
|
||||||
keyboard: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
partial: true,
|
|
||||||
background: {
|
|
||||||
color: "#FFFFFF",
|
|
||||||
},
|
|
||||||
grid: true,
|
|
||||||
edgeTextDraggable: false,
|
|
||||||
nodeTextEdit: false, //节点是否可编辑。false不可编辑
|
|
||||||
textEdit: false, //是否开启文本编辑
|
|
||||||
style: {
|
|
||||||
inputText: {
|
|
||||||
background: "black",
|
|
||||||
color: "white",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
idGenerator(type) {
|
|
||||||
return type + "_" + Math.random();
|
|
||||||
},
|
|
||||||
plugins: [Group],
|
|
||||||
group: {
|
|
||||||
foldable: true, // 启用折叠功能
|
|
||||||
foldSize: 30, // 折叠后显示的图标尺寸
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 注册自定义节点
|
|
||||||
registerCustomNode(lf)
|
|
||||||
registerCustomGroup(lf)
|
|
||||||
// 渲染数据 data
|
|
||||||
lf.render(data);
|
|
||||||
|
|
||||||
lf.on('node:dragstart', (data, e) => {
|
|
||||||
lfRef.value.setProperties(data.data.id, {
|
|
||||||
nowTime: new Date().getTime()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听子节点拖动事件
|
|
||||||
lf.on("node:mousemove", ({ data, e }) => {
|
|
||||||
const { nodes } = lf.getGraphData();
|
|
||||||
const arr = recursiveFilter(nodes, data.id)
|
|
||||||
arr.forEach(item => {
|
|
||||||
lf.getNodeModelById(item.id).updateSize()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听子节点拖动事件
|
|
||||||
lf.on("blank:drop", () => {
|
|
||||||
getLog()
|
|
||||||
});
|
|
||||||
|
|
||||||
lfRef.value = lf;
|
|
||||||
}
|
|
||||||
loadNodeRedData()
|
|
||||||
getInstDetail()
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.flow_container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.btn_container {
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: end;
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
.instState {
|
|
||||||
margin-right: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
.RUNNING, .PAUSED, .STOPPED {
|
|
||||||
color: #8bbf86;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SUCCESS {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.FAILED {
|
|
||||||
color: rgb(128, 41, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.containerRef) {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.lf-graph {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -54,7 +54,7 @@ import NodeTitle from "../../components/NodeTitle.vue";
|
|||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
|
|||||||
@ -50,7 +50,7 @@ import { ref, onMounted, onUnmounted } from "vue";
|
|||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
|
|||||||
@ -37,7 +37,7 @@ import NodeTitle from "../../components/NodeTitle.vue";
|
|||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
|
|||||||
@ -53,7 +53,7 @@ import NodeTitle from "../../components/NodeTitle.vue";
|
|||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
|
|||||||
@ -37,7 +37,7 @@ import NodeTitle from "../../components/NodeTitle.vue";
|
|||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
|
|||||||
@ -38,7 +38,7 @@ import NodeTitle from "../../components/NodeTitle.vue";
|
|||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import IPlayer from "@/views/device/register/components/LogicFlow/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user