mart-admin/src/views/pages/workflow/index.vue
2022-05-30 10:01:00 +08:00

694 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="workflow-container">
<div class="workflow-mask" v-if="isShow"></div>
<div class="layout-view-bg-white flex" :style="{ height: `calc(100vh - ${setViewHeight}` }">
<div class="workflow">
<!-- 顶部工具栏 -->
<Tool @tool="onToolClick" />
<!-- 左侧导航区 -->
<div class="workflow-content">
<div class="workflow-left">
<el-scrollbar>
<div
ref="leftNavRefs"
v-for="(val, key) in leftNavList"
:key="val.id"
:style="{ height: val.isOpen ? 'auto' : '50px', overflow: 'hidden' }"
class="workflow-left-id"
>
<div class="workflow-left-title" @click="onTitleClick(val)">
<span>{{ val.title }}</span>
<SvgIcon :name="val.isOpen ? 'ele-ArrowDown' : 'ele-ArrowRight'" />
</div>
<div class="workflow-left-item" v-for="(v, k) in val.children" :key="k" :data-name="v.name" :data-icon="v.icon" :data-id="v.id">
<div class="workflow-left-item-icon">
<SvgIcon :name="v.icon" class="workflow-icon-drag" />
<div class="font10 pl5 name">{{ v.name }}</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
<!-- 右侧绘画区 -->
<div class="workflow-right" ref="workflowRightRef">
<div
v-for="(v, k) in jsplumbData.nodeList"
:key="v.nodeId"
:id="v.nodeId"
:data-node-id="v.nodeId"
:class="v.class"
:style="{ left: v.left, top: v.top }"
@click="onItemCloneClick(k)"
@contextmenu.prevent="onContextmenu(v, k, $event)"
>
<div class="workflow-right-box" :class="{ 'workflow-right-active': jsPlumbNodeIndex === k }">
<div class="workflow-left-item-icon">
<SvgIcon :name="v.icon" class="workflow-icon-drag" />
<div class="font10 pl5 name">{{ v.name }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 节点右键菜单 -->
<Contextmenu :dropdown="dropdownNode" ref="contextmenuNodeRef" @current="onCurrentNodeClick" />
<!-- 线右键菜单 -->
<Contextmenu :dropdown="dropdownLine" ref="contextmenuLineRef" @current="onCurrentLineClick" />
<!-- 抽屉表单、线 -->
<Drawer ref="drawerRef" @label="setLineLabel" @node="setNodeContent" />
<!-- 顶部工具栏-帮助弹窗 -->
<Help ref="helpRef" />
</div>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, computed, onMounted, onUnmounted, nextTick, ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { jsPlumb } from 'jsplumb';
import Sortable from 'sortablejs';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import Tool from './component/tool/index.vue';
import Help from './component/tool/help.vue';
import Contextmenu from './component/contextmenu/index.vue';
import Drawer from './component/drawer/index.vue';
import commonFunction from '/@/utils/commonFunction';
import { leftNavList } from './js/mock';
import { jsplumbDefaults, jsplumbMakeSource, jsplumbMakeTarget, jsplumbConnect } from './js/config';
// 定义接口来定义对象的类型
interface NodeListState {
id: string | number;
nodeId: string | undefined;
class: HTMLElement | string;
left: number | string;
top: number | string;
icon: string;
name: string;
}
interface LineListState {
sourceId: string;
targetId: string;
label: string;
}
interface XyState {
x: string | number;
y: string | number;
}
interface WorkflowState {
workflowRightRef: HTMLDivElement | null;
leftNavRefs: any[];
leftNavList: any[];
dropdownNode: XyState;
dropdownLine: XyState;
isShow: boolean;
jsPlumb: any;
jsPlumbNodeIndex: null | number;
jsplumbDefaults: any;
jsplumbMakeSource: any;
jsplumbMakeTarget: any;
jsplumbConnect: any;
jsplumbData: {
nodeList: Array<NodeListState>;
lineList: Array<LineListState>;
};
}
export default defineComponent({
name: 'pagesWorkflow',
components: { Tool, Contextmenu, Drawer, Help },
setup() {
const contextmenuNodeRef = ref();
const contextmenuLineRef = ref();
const drawerRef = ref();
const helpRef = ref();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(stores);
const { copyText } = commonFunction();
const state = reactive<WorkflowState>({
workflowRightRef: null as HTMLDivElement | null,
leftNavRefs: [],
leftNavList: [],
dropdownNode: { x: '', y: '' },
dropdownLine: { x: '', y: '' },
isShow: false,
jsPlumb: null,
jsPlumbNodeIndex: null,
jsplumbDefaults,
jsplumbMakeSource,
jsplumbMakeTarget,
jsplumbConnect,
jsplumbData: {
nodeList: [],
lineList: [],
},
});
// 设置 view 的高度
const setViewHeight = computed(() => {
let { isTagsview } = themeConfig.value;
if (isTagsViewCurrenFull.value) {
return `30px`;
} else {
if (isTagsview) return `114px`;
else return `80px`;
}
});
// 设置 宽度小于 768不支持操
const setClientWidth = () => {
const clientWidth = document.body.clientWidth;
clientWidth < 768 ? (state.isShow = true) : (state.isShow = false);
};
// 左侧导航-数据初始化
const initLeftNavList = () => {
state.leftNavList = leftNavList;
state.jsplumbData = {
nodeList: [
{ nodeId: 'huej738hbji', left: '148px', top: '93px', class: 'workflow-right-clone', icon: 'iconfont icon-gongju', name: '引擎', id: '11' },
{
nodeId: '52kcszzyxrd',
left: '458px',
top: '203px',
class: 'workflow-right-clone',
icon: 'iconfont icon-shouye_dongtaihui',
name: '模版',
id: '12',
},
{
nodeId: 'nltskl6k4me',
left: '164px',
top: '350px',
class: 'workflow-right-clone',
icon: 'iconfont icon-zhongduancanshuchaxun',
name: '名称',
id: '13',
},
],
lineList: [
{ sourceId: 'huej738hbji', targetId: '52kcszzyxrd', label: '传送' },
{ sourceId: 'huej738hbji', targetId: 'nltskl6k4me', label: '' },
],
};
};
// 左侧导航-初始化拖动
const initSortable = () => {
state.leftNavRefs.forEach(v => {
Sortable.create(v as HTMLDivElement, {
group: {
name: 'vue-next-admin-1',
pull: 'clone',
put: false,
},
animation: 0,
sort: false,
draggable: '.workflow-left-item',
forceFallback: true,
onEnd: function (evt: any) {
const { name, icon, id } = evt.clone.dataset;
const { layerX, layerY, clientX, clientY } = evt.originalEvent;
const el = state.workflowRightRef!;
const { x, y, width, height } = el.getBoundingClientRect();
if (clientX < x || clientX > width + x || clientY < y || y > y + height) {
ElMessage.warning('请把节点拖入到画布中');
} else {
// 节点id唯一
const nodeId = Math.random().toString(36).substr(2, 12);
// 处理节点数据
const node = {
nodeId,
left: `${layerX - 40}px`,
top: `${layerY - 15}px`,
class: 'workflow-right-clone',
name,
icon,
id,
};
// 右侧视图内容数组
state.jsplumbData.nodeList.push(node);
// 元素加载完毕时
nextTick(() => {
// 整个节点作为source或者target
state.jsPlumb.makeSource(nodeId, state.jsplumbMakeSource);
// // 整个节点作为source或者target
state.jsPlumb.makeTarget(nodeId, state.jsplumbMakeTarget, jsplumbConnect);
// 设置节点可以拖拽此处为id值非class
state.jsPlumb.draggable(nodeId, {
containment: 'parent',
stop: (el: any) => {
state.jsplumbData.nodeList.forEach((v) => {
if (v.nodeId === el.el.id) {
// 节点x, y重新赋值防止再次从左侧导航中拖拽节点时x, y恢复默认
v.left = `${el.pos[0]}px`;
v.top = `${el.pos[1]}px`;
}
});
},
});
});
}
},
});
});
};
// 初始化 jsPlumb
const initJsPlumb = () => {
(<any>jsPlumb).ready(() => {
state.jsPlumb = (<any>jsPlumb).getInstance({
detachable: false,
Container: 'workflow-right',
});
state.jsPlumb.fire('jsPlumbDemoLoaded', state.jsPlumb);
// 导入默认配置
state.jsPlumb.importDefaults(state.jsplumbDefaults);
// 会使整个jsPlumb立即重绘。
state.jsPlumb.setSuspendDrawing(false, true);
// 初始化节点、线的链接
initJsPlumbConnection();
// 点击线弹出右键菜单
state.jsPlumb.bind('contextmenu', (conn: any, originalEvent: MouseEvent) => {
originalEvent.preventDefault();
const { sourceId, targetId } = conn;
const { clientX, clientY } = originalEvent;
state.dropdownLine.x = clientX;
state.dropdownLine.y = clientY;
const v: any = state.jsplumbData.nodeList.find((v) => v.nodeId === targetId);
const line: any = state.jsplumbData.lineList.find((v) => v.sourceId === sourceId && v.targetId === targetId);
v.type = 'line';
v.label = line.label;
contextmenuLineRef.value.openContextmenu(v, conn);
});
// 连线之前
state.jsPlumb.bind('beforeDrop', (conn: any) => {
const { sourceId, targetId } = conn;
const item = state.jsplumbData.lineList.find((v) => v.sourceId === sourceId && v.targetId === targetId);
if (item) {
ElMessage.warning('关系已存在,不可重复连接');
return false;
} else {
return true;
}
});
// 连线时
state.jsPlumb.bind('connection', (conn: any) => {
const { sourceId, targetId } = conn;
state.jsplumbData.lineList.push({
sourceId,
targetId,
label: '',
});
});
// 删除连线时回调函数
state.jsPlumb.bind('connectionDetached', (conn: any) => {
const { sourceId, targetId } = conn;
state.jsplumbData.lineList = state.jsplumbData.lineList.filter((line) => {
if (line.sourceId == sourceId && line.targetId == targetId) {
return false;
}
return true;
});
});
});
};
// 初始化节点、线的链接
const initJsPlumbConnection = () => {
// 节点
state.jsplumbData.nodeList.forEach((v) => {
// 整个节点作为source或者target
state.jsPlumb.makeSource(v.nodeId, state.jsplumbMakeSource);
// 整个节点作为source或者target
state.jsPlumb.makeTarget(v.nodeId, state.jsplumbMakeTarget, jsplumbConnect);
// 设置节点可以拖拽此处为id值非class
state.jsPlumb.draggable(v.nodeId, {
containment: 'parent',
stop: (el: any) => {
state.jsplumbData.nodeList.forEach((v) => {
if (v.nodeId === el.el.id) {
// 节点x, y重新赋值防止再次从左侧导航中拖拽节点时x, y恢复默认
v.left = `${el.pos[0]}px`;
v.top = `${el.pos[1]}px`;
}
});
},
});
});
// 线
state.jsplumbData.lineList.forEach((v) => {
state.jsPlumb.connect(
{
source: v.sourceId,
target: v.targetId,
label: v.label,
},
state.jsplumbConnect
);
});
};
// 左侧导航-菜单标题点击
const onTitleClick = (val: any) => {
val.isOpen = !val.isOpen;
};
// 右侧内容区-当前项点击
const onItemCloneClick = (k: number) => {
state.jsPlumbNodeIndex = k;
};
// 右侧内容区-当前项右键菜单点击
const onContextmenu = (v: any, k: number, e: MouseEvent) => {
state.jsPlumbNodeIndex = k;
const { clientX, clientY } = e;
state.dropdownNode.x = clientX;
state.dropdownNode.y = clientY;
v.type = 'node';
v.label = '';
let item: any = {};
state.leftNavList.forEach((l) => {
if (l.children) if (l.children.find((c: any) => c.id === v.id)) item = l.children.find((c: any) => c.id === v.id);
});
v.from = item.form;
contextmenuNodeRef.value.openContextmenu(v);
};
// 右侧内容区-当前项右键菜单点击回调(节点)
const onCurrentNodeClick = (item: any) => {
const { contextMenuClickId, nodeId } = item;
if (contextMenuClickId === 0) {
const nodeIndex = state.jsplumbData.nodeList.findIndex((item) => item.nodeId === nodeId);
state.jsplumbData.nodeList.splice(nodeIndex, 1);
state.jsPlumb.removeAllEndpoints(nodeId);
state.jsPlumbNodeIndex = null;
} else if (contextMenuClickId === 1) {
drawerRef.value.open(item);
}
};
// 右侧内容区-当前项右键菜单点击回调(线)
const onCurrentLineClick = (item: any, conn: any) => {
const { contextMenuClickId } = item;
const { endpoints } = conn;
const intercourse: any = [];
endpoints.forEach((v: any) => {
intercourse.push({
id: v.element.id,
innerText: v.element.innerText,
});
});
item.contact = `${intercourse[0].innerText}(${intercourse[0].id}) => ${intercourse[1].innerText}(${intercourse[1].id})`;
if (contextMenuClickId === 0) state.jsPlumb.deleteConnection(conn);
else if (contextMenuClickId === 1) drawerRef.value.open(item, conn);
};
// 设置线的 label
const setLineLabel = (obj: any) => {
const { sourceId, targetId, label } = obj;
const conn = state.jsPlumb.getConnections({
source: sourceId,
target: targetId,
})[0];
conn.setLabel(label);
if (!label || label === '') {
conn.addClass('workflow-right-empty-label');
} else {
conn.removeClass('workflow-right-empty-label');
conn.addClass('workflow-right-label');
}
state.jsplumbData.lineList.forEach((v) => {
if (v.sourceId === sourceId && v.targetId === targetId) v.label = label;
});
};
// 设置节点内容
const setNodeContent = (obj: any) => {
const { nodeId, name, icon } = obj;
// 设置节点 name 与 icon
state.jsplumbData.nodeList.forEach((v) => {
if (v.nodeId === nodeId) {
v.name = name;
v.icon = icon;
}
});
// 重绘
nextTick(() => {
state.jsPlumb.setSuspendDrawing(false, true);
});
};
// 顶部工具栏-当前项点击
const onToolClick = (fnName: String) => {
switch (fnName) {
case 'help':
onToolHelp();
break;
case 'download':
onToolDownload();
break;
case 'submit':
onToolSubmit();
break;
case 'copy':
onToolCopy();
break;
case 'del':
onToolDel();
break;
case 'fullscreen':
onToolFullscreen();
break;
}
};
// 顶部工具栏-帮助
const onToolHelp = () => {
nextTick(() => {
helpRef.value.open();
});
};
// 顶部工具栏-下载
const onToolDownload = () => {
const { globalTitle } = themeConfig.value;
const href = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(state.jsplumbData, null, '\t'));
const aLink = document.createElement('a');
aLink.setAttribute('href', href);
aLink.setAttribute('download', `${globalTitle}工作流.json`);
aLink.click();
aLink.remove();
ElMessage.success('下载成功');
};
// 顶部工具栏-提交
const onToolSubmit = () => {
// console.log(state.jsplumbData);
ElMessage.success('数据提交成功');
};
// 顶部工具栏-复制
const onToolCopy = () => {
copyText(JSON.stringify(state.jsplumbData));
};
// 顶部工具栏-删除
const onToolDel = () => {
ElMessageBox.confirm('此操作将清空画布,是否继续?', '提示', {
confirmButtonText: '清空',
cancelButtonText: '取消',
})
.then(() => {
state.jsplumbData.nodeList.forEach((v) => {
state.jsPlumb.removeAllEndpoints(v.nodeId);
});
nextTick(() => {
state.jsplumbData = {
nodeList: [],
lineList: [],
};
ElMessage.success('清空画布成功');
});
})
.catch(() => {});
};
// 顶部工具栏-全屏
const onToolFullscreen = () => {
stores.setCurrenFullscreen(true);
};
// 页面加载时
onMounted(async () => {
await initLeftNavList();
initSortable();
initJsPlumb();
setClientWidth();
window.addEventListener('resize', setClientWidth);
});
// 页面卸载时
onUnmounted(() => {
window.removeEventListener('resize', setClientWidth);
});
return {
setViewHeight,
setClientWidth,
setLineLabel,
setNodeContent,
onTitleClick,
onItemCloneClick,
onContextmenu,
onCurrentNodeClick,
onCurrentLineClick,
contextmenuNodeRef,
contextmenuLineRef,
drawerRef,
helpRef,
onToolClick,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.workflow-container {
position: relative;
.workflow {
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
.workflow-content {
display: flex;
height: calc(100% - 35px);
.workflow-left {
width: 220px;
height: 100%;
border-right: 1px solid var(--el-border-color-light, #ebeef5);
::v-deep(.el-collapse-item__content) {
padding-bottom: 0;
}
.workflow-left-title {
height: 50px;
display: flex;
align-items: center;
padding: 0 10px;
border-top: 1px solid var(--el-border-color-light, #ebeef5);
color: var(--el-text-color-primary);
cursor: default;
span {
flex: 1;
}
}
.workflow-left-item {
display: inline-block;
width: calc(50% - 15px);
position: relative;
cursor: move;
margin: 0 0 10px 10px;
.workflow-left-item-icon {
height: 35px;
display: flex;
align-items: center;
transition: all 0.3s ease;
padding: 5px 10px;
border: 1px dashed transparent;
background: var(--next-bg-color);
border-radius: 3px;
i,
.name {
color: var(--el-text-color-secondary);
transition: all 0.3s ease;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&:hover {
transition: all 0.3s ease;
border: 1px dashed var(--el-color-primary);
background: var(--el-color-primary-light-9);
border-radius: 5px;
i,
.name {
transition: all 0.3s ease;
color: var(--el-color-primary);
}
}
}
}
& .workflow-left-id:first-of-type {
.workflow-left-title {
border-top: none;
}
}
}
.workflow-right {
flex: 1;
position: relative;
overflow: hidden;
height: 100%;
background-image: linear-gradient(90deg, rgb(156 214 255 / 15%) 10%, rgba(0, 0, 0, 0) 10%),
linear-gradient(rgb(156 214 255 / 15%) 10%, rgba(0, 0, 0, 0) 10%);
background-size: 10px 10px;
.workflow-right-clone {
position: absolute;
.workflow-right-box {
height: 35px;
align-items: center;
color: var(--el-text-color-secondary);
padding: 0 10px;
border-radius: 3px;
cursor: move;
transition: all 0.3s ease;
min-width: 94.5px;
background: var(--el-color-white);
border: 1px solid var(--el-border-color-light, #ebeef5);
.workflow-left-item-icon {
display: flex;
align-items: center;
height: 35px;
}
&:hover {
border: 1px dashed var(--el-color-primary);
background: var(--el-color-primary-light-9);
transition: all 0.3s ease;
color: var(--el-color-primary);
i {
cursor: Crosshair;
}
}
}
.workflow-right-active {
border: 1px dashed var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
}
::v-deep(.jtk-overlay):not(.aLabel) {
padding: 4px 10px;
border: 1px solid var(--el-border-color-light, #ebeef5) !important;
color: var(--el-text-color-secondary) !important;
background: var(--el-color-white) !important;
border-radius: 3px;
font-size: 10px;
}
::v-deep(.jtk-overlay.workflow-right-empty-label) {
display: none;
}
}
}
}
.workflow-mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
&::after {
content: '手机版不支持 jsPlumb 操作';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: rgba(255, 255, 255, 0.9);
color: #666666;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>