123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684 |
- <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 id="workflow-left">
- <el-scrollbar>
- <div
- :id="`left${key}`"
- 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 id="workflow-right">
- <div
- v-for="(v, k) in jsplumbData.nodeList"
- :key="v.nodeId"
- :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 { useStore } from '/@/store/index';
- 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 {
- 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 store = useStore();
- const { copyText } = commonFunction();
- const state = reactive<WorkflowState>({
- 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 } = store.state.themeConfig.themeConfig;
- let { isTagsViewCurrenFull } = store.state.tagsViewRoutes;
- if (isTagsViewCurrenFull) {
- 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.leftNavList.forEach((v, k) => {
- Sortable.create(document.getElementById(`left${k}`) as HTMLElement, {
- 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 = document.querySelector('#workflow-right') as HTMLElement;
- 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 } = store.state.themeConfig.themeConfig;
- 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 = () => {
- store.dispatch('tagsViewRoutes/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>
|