初始化项目
This commit is contained in:
107
src/views/pages/workflow/component/contextmenu/index.vue
Normal file
107
src/views/pages/workflow/component/contextmenu/index.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||
role="tooltip"
|
||||
data-popper-placement="bottom"
|
||||
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
|
||||
:key="Math.random()"
|
||||
v-show="isShow"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<li
|
||||
v-for="(v, k) in dropdownList"
|
||||
class="el-dropdown-menu__item"
|
||||
aria-disabled="false"
|
||||
tabindex="-1"
|
||||
:key="k"
|
||||
@click="onCurrentClick(v.contextMenuClickId)"
|
||||
>
|
||||
<SvgIcon :name="v.icon" />
|
||||
<span>{{ v.txt }}{{ item.type === 'line' ? '线' : '节点' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="el-popper__arrow" style="left: 10px"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, toRefs, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'pagesWorkflowContextmenu',
|
||||
props: {
|
||||
dropdown: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const state = reactive({
|
||||
isShow: false,
|
||||
dropdownList: [
|
||||
{ contextMenuClickId: 0, txt: '删除', icon: 'ele-Delete' },
|
||||
{ contextMenuClickId: 1, txt: '编辑', icon: 'ele-Edit' },
|
||||
],
|
||||
item: {
|
||||
type: 'node',
|
||||
},
|
||||
conn: {},
|
||||
});
|
||||
// 父级传过来的坐标 x,y 值
|
||||
const dropdowns = computed(() => {
|
||||
return <any>props.dropdown;
|
||||
});
|
||||
// 当前项菜单点击
|
||||
const onCurrentClick = (contextMenuClickId: number) => {
|
||||
emit('current', Object.assign({}, { contextMenuClickId }, state.item), state.conn);
|
||||
};
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any, conn = {}) => {
|
||||
state.item = item;
|
||||
state.conn = conn;
|
||||
closeContextmenu();
|
||||
setTimeout(() => {
|
||||
state.isShow = true;
|
||||
}, 10);
|
||||
};
|
||||
// 关闭右键菜单
|
||||
const closeContextmenu = () => {
|
||||
state.isShow = false;
|
||||
};
|
||||
// 监听页面监听进行右键菜单的关闭
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('click', closeContextmenu);
|
||||
document.body.addEventListener('contextmenu', closeContextmenu);
|
||||
});
|
||||
// 页面卸载时,移除右键菜单监听事件
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('click', closeContextmenu);
|
||||
document.body.removeEventListener('contextmenu', closeContextmenu);
|
||||
});
|
||||
return {
|
||||
dropdowns,
|
||||
openContextmenu,
|
||||
closeContextmenu,
|
||||
onCurrentClick,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-contextmenu {
|
||||
transform-origin: center top;
|
||||
z-index: 2190;
|
||||
position: fixed;
|
||||
.el-dropdown-menu__item {
|
||||
font-size: 12px !important;
|
||||
white-space: nowrap;
|
||||
i {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
src/views/pages/workflow/component/drawer/index.vue
Normal file
73
src/views/pages/workflow/component/drawer/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="`${nodeData.type === 'line' ? '线' : '节点'}操作`" v-model="isOpen" size="320px">
|
||||
<el-scrollbar>
|
||||
<Line v-if="nodeData.type === 'line'" @change="onLineChange" @close="close" ref="lineRef" />
|
||||
<Node v-else @submit="onNodeSubmit" @close="close" ref="nodeRef" />
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, nextTick } from 'vue';
|
||||
import Line from './line.vue';
|
||||
import Node from './node.vue';
|
||||
|
||||
// 定义接口来定义对象的类型
|
||||
interface WorkflowDrawerState {
|
||||
isOpen: boolean;
|
||||
nodeData: {
|
||||
type: string;
|
||||
};
|
||||
jsplumbConn: any;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'pagesWorkflowDrawer',
|
||||
components: { Line, Node },
|
||||
setup(props, { emit }) {
|
||||
const lineRef = ref();
|
||||
const nodeRef = ref();
|
||||
const state = reactive<WorkflowDrawerState>({
|
||||
isOpen: false,
|
||||
nodeData: {
|
||||
type: 'node',
|
||||
},
|
||||
jsplumbConn: {},
|
||||
});
|
||||
// 打开抽屉
|
||||
const open = (item: any, conn: any) => {
|
||||
state.isOpen = true;
|
||||
state.jsplumbConn = conn;
|
||||
state.nodeData = item;
|
||||
nextTick(() => {
|
||||
if (item.type === 'line') lineRef.value.getParentData(item);
|
||||
else nodeRef.value.getParentData(item);
|
||||
});
|
||||
};
|
||||
// 关闭
|
||||
const close = () => {
|
||||
state.isOpen = false;
|
||||
};
|
||||
// 线 label 内容改变时
|
||||
const onLineChange = (label: any) => {
|
||||
state.jsplumbConn.label = label;
|
||||
emit('label', state.jsplumbConn);
|
||||
};
|
||||
// 节点内容改变时
|
||||
const onNodeSubmit = (data: object) => {
|
||||
emit('node', data);
|
||||
};
|
||||
return {
|
||||
lineRef,
|
||||
nodeRef,
|
||||
open,
|
||||
close,
|
||||
onLineChange,
|
||||
onNodeSubmit,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
62
src/views/pages/workflow/component/drawer/line.vue
Normal file
62
src/views/pages/workflow/component/drawer/line.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="pt15 pr15 pb15 pl15">
|
||||
<el-form :model="line" size="default" label-width="50px">
|
||||
<el-form-item label="来往">
|
||||
<el-input v-model="line.contact" placeholder="来往" clearable disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-input v-model="line.type" placeholder="类型" clearable disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="label">
|
||||
<el-input v-model="line.label" placeholder="请输入label内容" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="onLineTextReset">
|
||||
<SvgIcon name="ele-RefreshRight" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button @click="onLineTextChange" type="primary">
|
||||
<SvgIcon name="ele-Check" />
|
||||
保存
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs } from 'vue';
|
||||
|
||||
// 定义接口来定义对象的类型
|
||||
interface WorkflowDrawerLineState {
|
||||
line: any;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'pagesWorkflowDrawerLine',
|
||||
setup(props, { emit }) {
|
||||
const state = reactive<WorkflowDrawerLineState>({
|
||||
line: {},
|
||||
});
|
||||
// 获取父组件数据
|
||||
const getParentData = (data: object) => {
|
||||
state.line = data;
|
||||
};
|
||||
// 重置
|
||||
const onLineTextReset = () => {
|
||||
state.line.label = '';
|
||||
};
|
||||
// 保存
|
||||
const onLineTextChange = () => {
|
||||
emit('change', state.line.label);
|
||||
emit('close');
|
||||
};
|
||||
return {
|
||||
getParentData,
|
||||
onLineTextReset,
|
||||
onLineTextChange,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
272
src/views/pages/workflow/component/drawer/node.vue
Normal file
272
src/views/pages/workflow/component/drawer/node.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<div class="workflow-drawer-node">
|
||||
<el-tabs type="border-card" v-model="tabsActive">
|
||||
<!-- 节点编辑 -->
|
||||
<el-tab-pane label="节点编辑" name="1">
|
||||
<el-scrollbar>
|
||||
<el-form :model="node" :rules="nodeRules" ref="nodeFormRef" size="default" label-width="80px" class="pt15 pr15 pb15 pl15">
|
||||
<el-form-item label="数据id" prop="id">
|
||||
<el-input v-model="node.id" placeholder="请输入数据id" clearable disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="节点id" prop="nodeId">
|
||||
<el-input v-model="node.nodeId" placeholder="请输入节点id" clearable disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-input v-model="node.type" placeholder="请输入类型" clearable disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="left坐标" prop="left">
|
||||
<el-input v-model="node.left" placeholder="请输入left坐标" clearable disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="top坐标" prop="top">
|
||||
<el-input v-model="node.top" placeholder="请输入top坐标" clearable disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="icon图标" prop="icon">
|
||||
<el-input v-model="node.icon" placeholder="请输入icon图标" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="node.name" placeholder="请输入名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="mb15" @click="onNodeRefresh">
|
||||
<SvgIcon name="ele-RefreshRight" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button type="primary" class="mb15" @click="onNodeSubmit">
|
||||
<SvgIcon name="ele-Check" />
|
||||
保存
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 扩展表单 -->
|
||||
<el-tab-pane label="扩展表单" name="2">
|
||||
<el-scrollbar>
|
||||
<el-form :model="form" ref="extendFormRef" size="default" label-width="80px" class="pt15 pr15 pb15 pl15">
|
||||
<el-form-item
|
||||
:label="val.label"
|
||||
:prop="val.prop"
|
||||
v-for="(val, key) in node.from"
|
||||
:key="key"
|
||||
:rules="[{ required: val.required, message: `${val.label}不能为空`, trigger: 'blur' }]"
|
||||
>
|
||||
<el-input
|
||||
v-model="form[val.prop]"
|
||||
:placeholder="val.placeholder"
|
||||
clearable
|
||||
v-if="val.type === 'input'"
|
||||
:disabled="val.disabled"
|
||||
></el-input>
|
||||
<el-select v-model="form[val.prop]" placeholder="请选择" v-if="val.type === 'select'" clearable :disabled="val.disabled">
|
||||
<el-option v-for="item in val.options" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||
</el-select>
|
||||
<el-checkbox-group v-model="form[val.prop]" v-if="val.type === 'checkbox'" :disabled="val.disabled">
|
||||
<el-checkbox label="美食推荐" name="type"></el-checkbox>
|
||||
<el-checkbox label="统计分析" name="type"></el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="mb15" @click="onExtendRefresh">
|
||||
<SvgIcon name="ele-RefreshRight" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button type="primary" class="mb15" @click="onExtendSubmit" :loading="loading.extend">
|
||||
<SvgIcon name="ele-Check" />
|
||||
保存
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 图表可视化 -->
|
||||
<el-tab-pane label="图表可视化" name="3">
|
||||
<el-scrollbar>
|
||||
<div class="flex-content-right">
|
||||
<div style="height: 200px; width: 320px" ref="chartsMonitorRef"></div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, ref, nextTick, getCurrentInstance } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
// 定义接口来定义对象的类型
|
||||
interface WorkflowDrawerNodeState {
|
||||
node: { [key: string]: any };
|
||||
nodeRules: any;
|
||||
form: any;
|
||||
tabsActive: string;
|
||||
loading: {
|
||||
extend: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'pagesWorkflowDrawerNode',
|
||||
setup(props, { emit }) {
|
||||
const { proxy } = <any>getCurrentInstance();
|
||||
const nodeFormRef = ref();
|
||||
const extendFormRef = ref();
|
||||
const chartsMonitorRef = ref();
|
||||
const state = reactive<WorkflowDrawerNodeState>({
|
||||
node: {},
|
||||
nodeRules: {
|
||||
id: [{ required: true, message: '请输入数据id', trigger: 'blur' }],
|
||||
nodeId: [{ required: true, message: '请输入节点id', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请输入类型', trigger: 'blur' }],
|
||||
left: [{ required: true, message: '请输入left坐标', trigger: 'blur' }],
|
||||
top: [{ required: true, message: '请输入top坐标', trigger: 'blur' }],
|
||||
icon: [{ required: true, message: '请输入icon图标', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
},
|
||||
form: {
|
||||
module: [],
|
||||
},
|
||||
tabsActive: '1',
|
||||
loading: {
|
||||
extend: false,
|
||||
},
|
||||
});
|
||||
// 获取父组件数据
|
||||
const getParentData = (data: object) => {
|
||||
state.tabsActive = '1';
|
||||
state.node = data;
|
||||
initChartsMonitor();
|
||||
};
|
||||
// 节点编辑-重置
|
||||
const onNodeRefresh = () => {
|
||||
state.node.icon = '';
|
||||
state.node.name = '';
|
||||
};
|
||||
// 节点编辑-保存
|
||||
const onNodeSubmit = () => {
|
||||
nodeFormRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
emit('submit', state.node);
|
||||
emit('close');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
// 扩展表单-重置
|
||||
const onExtendRefresh = () => {
|
||||
extendFormRef.value.resetFields();
|
||||
};
|
||||
// 扩展表单-保存
|
||||
const onExtendSubmit = () => {
|
||||
extendFormRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
state.loading.extend = true;
|
||||
setTimeout(() => {
|
||||
state.loading.extend = false;
|
||||
ElMessage.success('保存成功');
|
||||
emit('close');
|
||||
}, 1000);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
// 图表可视化-初始化
|
||||
const initChartsMonitor = () => {
|
||||
const myChart = echarts.init(proxy.$refs.chartsMonitorRef);
|
||||
const numsOne = [];
|
||||
const numsTwo = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
numsOne.push(`${Math.floor(Math.random() * 52 + 10)}:${Math.floor(Math.random() * 52 + 1)}`);
|
||||
numsTwo.push(Math.floor(Math.random() * 52 + 1));
|
||||
}
|
||||
const option = {
|
||||
grid: {
|
||||
top: 50,
|
||||
right: 30,
|
||||
bottom: 30,
|
||||
left: 50,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: numsOne,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#289df5',
|
||||
borderColor: '#289df5',
|
||||
areaStyle: {
|
||||
type: 'default',
|
||||
opacity: 0.1,
|
||||
},
|
||||
},
|
||||
data: numsTwo,
|
||||
type: 'line',
|
||||
areaStyle: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
myChart.setOption(option);
|
||||
nextTick(() => {
|
||||
myChart.resize();
|
||||
});
|
||||
};
|
||||
return {
|
||||
nodeFormRef,
|
||||
extendFormRef,
|
||||
chartsMonitorRef,
|
||||
getParentData,
|
||||
onNodeRefresh,
|
||||
onNodeSubmit,
|
||||
onExtendRefresh,
|
||||
onExtendSubmit,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.workflow-drawer-node {
|
||||
:deep {
|
||||
.el-tabs {
|
||||
box-shadow: unset;
|
||||
border: unset;
|
||||
.el-tabs__nav {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
.el-tabs__item {
|
||||
flex: 1;
|
||||
padding: unset;
|
||||
text-align: center;
|
||||
&:first-of-type.is-active {
|
||||
border-left-color: transparent;
|
||||
}
|
||||
&:last-of-type.is-active {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-tabs__content {
|
||||
padding: 0;
|
||||
height: calc(100vh - 90px);
|
||||
.el-tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
src/views/pages/workflow/component/tool/help.vue
Normal file
39
src/views/pages/workflow/component/tool/help.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="workflow-tool-help">
|
||||
<el-dialog v-model="isShow" width="769px">
|
||||
<template #header>
|
||||
<div v-drag="['.workflow-tool-help .el-dialog', '.workflow-tool-help .el-dialog__header']">使用帮助</div>
|
||||
</template>
|
||||
<div>1、拖入:鼠标移入左侧导航中,鼠标形状改变时拖动到右侧网格状的视图中。</div>
|
||||
<div class="mt10">2、移动:鼠标移入到视图中的某个节点元素,鼠标形状改变时拖动改变位置。</div>
|
||||
<div class="mt10">3、连线:鼠标移入到视图中的某个节点元素的icon(图标),鼠标形状改变(变成"+"),按下鼠标左键进行拖线连接。</div>
|
||||
<div class="mt10">4、节点:鼠标移入到视图中的某个节点元素,点击鼠标右键可进行删除、编辑节点。</div>
|
||||
<div class="mt10 mb10">5、线条:鼠标移入到视图中的某个线条,线条颜色改变时,点击鼠标右键可进行删除、编辑线条。</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs } from 'vue';
|
||||
export default defineComponent({
|
||||
name: 'pagesWorkflowToolHelp',
|
||||
setup() {
|
||||
const state = reactive({
|
||||
isShow: false,
|
||||
});
|
||||
// 打开弹窗
|
||||
const open = () => {
|
||||
state.isShow = true;
|
||||
};
|
||||
// 关闭弹窗
|
||||
const close = () => {
|
||||
state.isShow = false;
|
||||
};
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
79
src/views/pages/workflow/component/tool/index.vue
Normal file
79
src/views/pages/workflow/component/tool/index.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="workflow-tool">
|
||||
<div class="pl15">{{ setToolTitle }}</div>
|
||||
<div class="workflow-tool-right">
|
||||
<div class="workflow-tool-icon" v-for="(v, k) in toolList" :key="k" :title="v.title" @click="onToolClick(v.fnName)">
|
||||
<SvgIcon :name="v.icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, reactive, toRefs } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '/@/stores/themeConfig';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'pagesWorkflowTool',
|
||||
setup(props, { emit }) {
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
const state = reactive({
|
||||
toolList: [
|
||||
{ icon: 'ele-Help', title: '帮助', fnName: 'help' },
|
||||
{ icon: 'ele-Download', title: '下载', fnName: 'download' },
|
||||
{ icon: 'ele-Check', title: '提交', fnName: 'submit' },
|
||||
{ icon: 'ele-DocumentCopy', title: '复制', fnName: 'copy' },
|
||||
{ icon: 'ele-Delete', title: '删除', fnName: 'del' },
|
||||
{ icon: 'ele-FullScreen', title: '全屏', fnName: 'fullscreen' },
|
||||
],
|
||||
});
|
||||
// 设置 tool 标题
|
||||
const setToolTitle = computed(() => {
|
||||
let { globalTitle } = themeConfig.value;
|
||||
return `${globalTitle}工作流`;
|
||||
});
|
||||
// 顶部工具栏
|
||||
const onToolClick = (fnName: string) => {
|
||||
emit('tool', fnName);
|
||||
};
|
||||
return {
|
||||
setToolTitle,
|
||||
onToolClick,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.workflow-tool {
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
color: var(--el-text-color-primary);
|
||||
.workflow-tool-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
&-icon {
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
color: var(--next-bg-topBarColor);
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
i {
|
||||
display: inline-block;
|
||||
animation: logoAnimation 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
693
src/views/pages/workflow/index.vue
Normal file
693
src/views/pages/workflow/index.vue
Normal file
@@ -0,0 +1,693 @@
|
||||
<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);
|
||||
: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);
|
||||
}
|
||||
}
|
||||
: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;
|
||||
}
|
||||
: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>
|
||||
99
src/views/pages/workflow/js/config.ts
Normal file
99
src/views/pages/workflow/js/config.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// jsplumb 默认配置
|
||||
export const jsplumbDefaults = {
|
||||
// 多个锚点 [源锚点,目标锚点]
|
||||
Anchors: [
|
||||
'Top',
|
||||
'TopCenter',
|
||||
'TopRight',
|
||||
'TopLeft',
|
||||
'Right',
|
||||
'RightMiddle',
|
||||
'Bottom',
|
||||
'BottomCenter',
|
||||
'BottomRight',
|
||||
'BottomLeft',
|
||||
'Left',
|
||||
'LeftMiddle',
|
||||
],
|
||||
// 连线的容器id
|
||||
Container: 'workflow-right',
|
||||
// 设置链接线的形状,如直线或者曲线之类的。anchor可以去设置锚点的位置。可选值"<Bezier|Flowchart|StateMachine|Straight>"
|
||||
Connector: ['Bezier', { curviness: 100 }],
|
||||
// 节点是否可以用鼠标拖动使其断开,默认为true。即用鼠标链接上的连线,也可以使用鼠标拖动让其断开。设置成false,可以让其拖动也不会自动断开
|
||||
ConnectionsDetachable: false,
|
||||
// 删除线的时候节点不删除
|
||||
DeleteEndpointsOnDetach: false,
|
||||
// 每当添加或以其他方式创建 Endpoint 并且 jsPlumb 尚未给出任何明确的 Endpoint 定义时将使用
|
||||
Endpoint: ['Blank', { Overlays: '' }],
|
||||
// 连接中源和目标端点的默认外观
|
||||
EndpointStyle: { fill: '#1879ffa1', outlineWidth: 1 },
|
||||
// jsPlumb 的内部日志记录是否打开
|
||||
LogEnabled: true,
|
||||
// 连接器的默认外观
|
||||
PaintStyle: {
|
||||
stroke: '#E0E3E7',
|
||||
strokeWidth: 1,
|
||||
outlineStroke: 'transparent',
|
||||
outlineWidth: 10,
|
||||
},
|
||||
// 用于配置任何可拖动元素的默认选项jsPlumb.draggable
|
||||
DragOptions: { cursor: 'pointer', zIndex: 2000 },
|
||||
// 添加到连接器和端点的默认叠加层。已弃用:从 4.x 开始,将不支持此功能。并非所有叠加层都可以连接到连接器和端点。
|
||||
Overlays: [
|
||||
[
|
||||
'Arrow',
|
||||
{
|
||||
width: 10, // 箭头尾部的宽度
|
||||
length: 8, // 从箭头的尾部到头部的距离
|
||||
location: 1, // 位置,建议使用0~1之间
|
||||
direction: 1, // 方向,默认值为1(表示向前),可选-1(表示向后)
|
||||
foldback: 0.623, // 折回,也就是尾翼的角度,默认0.623,当为1时,为正三角
|
||||
},
|
||||
],
|
||||
[
|
||||
'Label',
|
||||
{
|
||||
label: '',
|
||||
location: 0.5,
|
||||
cssClass: 'aLabel',
|
||||
},
|
||||
],
|
||||
],
|
||||
// 默认渲染模式 svg、canvas
|
||||
RenderMode: 'svg',
|
||||
// 悬停状态下连接的默认外观
|
||||
HoverPaintStyle: { stroke: '#b0b2b5', strokeWidth: 1 },
|
||||
// 悬停状态下端点的默认外观
|
||||
EndpointHoverStyle: { fill: 'red' },
|
||||
// 端点和连接的默认范围。范围提供了对哪些端点可以连接到哪些其他端点的基本控制
|
||||
Scope: 'jsPlumb_DefaultScope',
|
||||
};
|
||||
|
||||
// 整个节点作为source或者target
|
||||
export const jsplumbMakeSource = {
|
||||
// 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线
|
||||
filter: '.workflow-icon-drag',
|
||||
filterExclude: false,
|
||||
anchor: 'Continuous',
|
||||
// 是否允许自己连接自己
|
||||
allowLoopback: true,
|
||||
maxConnections: -1,
|
||||
};
|
||||
|
||||
// 整个节点作为source或者target
|
||||
export const jsplumbMakeTarget = {
|
||||
filter: '.workflow-icon-drag',
|
||||
filterExclude: false,
|
||||
// 是否允许自己连接自己
|
||||
anchor: 'Continuous',
|
||||
allowLoopback: true,
|
||||
dropOptions: { hoverClass: 'ef-drop-hover' },
|
||||
};
|
||||
|
||||
// 连线参数
|
||||
export const jsplumbConnect = {
|
||||
isSource: true,
|
||||
isTarget: true,
|
||||
// 动态锚点、提供了4个方向 Continuous、AutoDefault
|
||||
anchor: 'Continuous',
|
||||
};
|
||||
262
src/views/pages/workflow/js/mock.ts
Normal file
262
src/views/pages/workflow/js/mock.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
// 左侧菜单导航数据
|
||||
export const leftNavList = [
|
||||
{
|
||||
title: '工作流',
|
||||
icon: 'iconfont icon-shouye',
|
||||
isOpen: true,
|
||||
id: '1',
|
||||
children: [
|
||||
{
|
||||
icon: 'iconfont icon-gongju',
|
||||
name: '引擎',
|
||||
id: '11',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '客户姓名',
|
||||
prop: 'name',
|
||||
placeholder: '请输入客户姓名',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '性别',
|
||||
prop: 'sex',
|
||||
placeholder: '请选择性别',
|
||||
required: true,
|
||||
disabled: false,
|
||||
options: [
|
||||
{
|
||||
value: '0',
|
||||
label: '女',
|
||||
},
|
||||
{
|
||||
value: '1',
|
||||
label: '男',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: '员工编号',
|
||||
prop: 'number',
|
||||
placeholder: '请输入员工编号',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: '办公电话',
|
||||
prop: 'mobile',
|
||||
placeholder: '请输入办公电话',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '权限分配',
|
||||
prop: 'role',
|
||||
placeholder: '请选择性别',
|
||||
required: true,
|
||||
disabled: false,
|
||||
options: [
|
||||
{
|
||||
value: '0',
|
||||
label: '编辑权限',
|
||||
},
|
||||
{
|
||||
value: '1',
|
||||
label: '删除权限',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: '模块选择',
|
||||
prop: 'module',
|
||||
placeholder: '请选择模块',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-shouye_dongtaihui',
|
||||
name: '模版',
|
||||
id: '12',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '等级',
|
||||
prop: 'grade',
|
||||
placeholder: '请输入等级',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: '登记密码',
|
||||
prop: 'password',
|
||||
placeholder: '请输入登记密码',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-zhongduancanshuchaxun',
|
||||
name: '名称',
|
||||
id: '13',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '数据表',
|
||||
prop: 'dataSheet',
|
||||
placeholder: '请输入数据表',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: '字段配置',
|
||||
prop: 'field',
|
||||
placeholder: '请输入字段配置',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-zhongduancanshu',
|
||||
name: '版本',
|
||||
id: '14',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '发布模板',
|
||||
prop: 'publish',
|
||||
placeholder: '请输入发布模板',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-bolangnengshiyanchang',
|
||||
name: '建模',
|
||||
id: '15',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '内容模板',
|
||||
prop: 'content',
|
||||
placeholder: '请输入内容模板',
|
||||
required: true,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-xingqiu',
|
||||
name: '节点',
|
||||
id: '16',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '活动名称6',
|
||||
prop: 'name16',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '流程',
|
||||
isOpen: true,
|
||||
icon: 'iconfont icon-caijian',
|
||||
id: '2',
|
||||
children: [
|
||||
{
|
||||
icon: 'iconfont icon-fuwenben',
|
||||
name: '实例',
|
||||
id: '21',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '活动名称7',
|
||||
prop: 'name21',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-fuwenbenkuang',
|
||||
name: '轨迹',
|
||||
id: '22',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '活动名称8',
|
||||
prop: 'name22',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-shangchuan',
|
||||
name: '数据',
|
||||
id: '23',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '活动名称9',
|
||||
prop: 'name23',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '任务',
|
||||
isOpen: true,
|
||||
icon: 'iconfont icon-shuju',
|
||||
id: '3',
|
||||
children: [
|
||||
{
|
||||
icon: 'iconfont icon-icon-',
|
||||
name: '参与人',
|
||||
id: '31',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '活动名称1',
|
||||
prop: 'name31',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-gerenzhongxin',
|
||||
name: '执行人',
|
||||
id: '32',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '活动名称2',
|
||||
prop: 'name32',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-fangkuang',
|
||||
name: '工单',
|
||||
id: '33',
|
||||
form: [
|
||||
{
|
||||
type: 'input',
|
||||
label: '活动名称3',
|
||||
prop: 'name33',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user