Vue自定义流程图式菜单解决方案
【点赞收藏加关注,前端技术不迷路~】
一、前言
1.现状
目前市面上基于Vue的中大型PC端业务管理系统,大部分广泛采用vue-router来构建三级/四级菜单结构。
2.痛点
由于大中型管理系统的业务复杂程度,一项业务可能涉及到10+的业务流程流转,仅采用最基础的vue-router构建的菜单结构,对于业务的流转方向,前后关系均不能显示的呈现出来,这将会带来一系列的问题:
1)对于每一位新承接的业务人员,需要培训学习完整的业务流程;
2)每当业务流程发生变更时,需要重新对所有相关业务人员进行培训;
3)企业的人员流动很有可能造成相关业务流程的丢失,那将是一件很可怕的事儿;
4)系统的业务流程会越来越多,流程越来越多,管理成本越来越大。
3.优化方案
菜单结构的流程图化是显式管理的最佳方案,有效的降低了管理部门业务模块的培训成本。通过配置式开发理念,采用单元集成模式,完成对流程图配置及嵌入的解决方案设计及开发方案制定。
二、流程图配置
1.整体思路
1)流程图信息存储在次末级菜单上,每一个流程节点对应的菜单为末级菜单;
2)通过单元集成模式思想,将整个流程图分解成每一个通用模块来单独配置,配置采用可视化配置。其中通用模块配置样式如下图;
3)通过遍历通用模块来生成整个流程图;
4)流程图嵌入页面应游离在vouter-view外,当次末级菜单路径变更时切换流程图;末级菜单变更时流程图保持不动。
2.通用模块配置
1)页面参数说明:
不同节点类型,含有不同参数字段
【连接线】:
1.说明文字:对当前业务流转的文字说明;
2.说明显示类型:如何展示说明文字,包含弹框、上横线上、上横线下、中横线上、中横线下、下横线上、下横线下。
【流程图】:
1.图标类型:内置图标为el-icon图标,自定义图标为UI设计图标;
2.跳转方式:包含禁用、本系统内部跳转、其他网站跳转(显示效果不同);
3.节点名称:当前节点名称显示;
4.图标ID:根据不同图标类型,选择不同图标ID;
5.跳转地址:为当前节点指定的路由地址;
6.透传参数:多个节点对应同一vue实例时,列表查询透传参数;
7.显示数量:是否显示当前业务节点,待处理业务数量;
8.数量关键字:当前业务节点,待处理业务数量统一查询接口对应的数据关键字;
9.说明文字:对当前业务流转的文字说明;
10.说明显示类型:如何展示说明文字,包含弹框、上横线上、上横线下、中横线上、中横线下、下横线上、下横线下。
// 流程节点默认信息
const detailInfoSource = {flowType: 'flow',flowCode: '',flowName: '',iconKeyword: '',iconType: 'system',jumpUrl: '',jumpParams: '',numberKeyword: '',tipsTitle: '',leftFlowType: '00000000000000000000',jumpFlag: 1,showLeftLine: 0,showNumber: 0
};
2)流程节点连接线参数说明
为节约存储开支,连接线及箭头等信息采用一个长度为20的数字字符串来存储,初始值为:"00000000000000000000"。外围8个线段、内部4条线段,再加上8个箭头,共计20个信息点位。其中:0-5点位为横线,6-11点位为竖线,12/13/18/19点位为竖向箭头,14-17点位为横向箭头。
线段点位说明:0为无(灰色),1为蓝色实线,2为蓝色虚线。
箭头点位说明:0为无(空白),1为向左或向上箭头,2为向右或向下箭头。
上图中点位信息为:"00110000002000020000"。
3.整体流程图
整体流程图每行流程节点数量为12个。12是2,3,4,6的最小公倍数,且element-ui中el-col布局span总值为24,可直接使用。从使用角度来看,每行12个流程节点是一个不错的选择。
1)页面参数说明:
1.定制流程图:支持默认为vue-router菜单列表、通用流程图、还支持为指定业务模块单独开发流程图;
2.默认跳转地址:进入当前业务模块默认跳转的菜单路由;
3.查询流程项数量:是否调用查询接口,default为进入当前业务模块时调用一次,也可直接设置多少ms查询一次;
4.查询接口地址:待处理业务数量统一查询接口(含当前业务模块字段);
5.新增一行节点:用Array.push增加12个空节点;
6.删除最后一行节点:用Array.pop()删除最后一行12个节点信息。
2)流程图操作方法:
1.新增一行节点:用Array.push增加12个空节点;
2.删除最后一行节点:用Array.pop()删除最后一行12个节点信息。
3)流程图的可视化展示
图中的流程图展示即为实际菜单显示效果,一方面方便配置;另一方面该处的流程图显示和实际菜单出的流程图显示可以进行组件抽离,提高代码复用。
三、关键代码实现
1.流程图节点
1)template
<div v-if="detailInfo.flowType === 'flow'" class="flow_item_box"> // 流程节点类型
// 节点图标及节点文字<div :class="'flow_line_content ' + (detailInfo.jumpFlag === 1 ? 'can_click' : (detailInfo.jumpFlag === 2 ? 'can_click go' : ''))"><div class="flow_line_icon"><svg-icon v-if="detailInfo.iconType === 'system'" :icon-class="detailInfo.iconKeyword || ''" /><img v-else-if="detailInfo.iconType === 'custom' && customIconsList.includes(detailInfo.iconKeyword)" :src="require('@/assets/system_icons/' + detailInfo.iconKeyword)" style="" /> </div><div class="flow_line_label">{{ detailInfo.flowName }}</div></div>
// 横向线段<div v-for="sub in [0, 1, 2, 3, 4, 5]" :key="'line_' + sub" :class="'flow_line line_' + sub + ' ' + (detailInfo.leftFlowType[sub] === '1' ? 'isActive' : (detailInfo.leftFlowType[sub] === '2' ? 'isActiveDashed' : ''))" @click="lineChange(sub)"/>
// 竖向线段<div v-for="sub in [6, 7, 8, 9, 10, 11]" :key="'line_' + sub" :class="'flow_line_vertical line_' + sub + ' ' + (detailInfo.leftFlowType[sub] === '1' ? 'isActive' : (detailInfo.leftFlowType[sub] === '2' ? 'isActiveDashed' : ''))" @click="lineChange(sub)"/>
// 竖向箭头<div v-for="sub in [12, 13, 18, 19]" :key="'arrow_' + sub" :class="'flow_line_arrow arrow_' + sub" @click="lineChange(sub)"><i v-if="detailInfo.leftFlowType[sub] === '1'" class="el-icon-caret-top color_theme_main" /><i v-else-if="detailInfo.leftFlowType[sub] === '2'" class="el-icon-caret-bottom color_theme_main" /><i v-else class="el-icon-full-screen" /></div>
// 横向箭头<div v-for="sub in [14, 15, 16, 17]" :key="'arrow_' + sub" :class="'flow_line_arrow arrow_' + sub" @click="lineChange(sub)"><i v-if="detailInfo.leftFlowType[sub] === '1'" class="el-icon-caret-left color_theme_main" /><i v-else-if="detailInfo.leftFlowType[sub] === '2'" class="el-icon-caret-right color_theme_main" /><i v-else class="el-icon-full-screen" /></div>
// 流程说明文字<div v-if="[1, 2, 3, 4, 5, 6].includes(detailInfo.showLeftLine)" :class="tipsClass(detailInfo)">{{ detailInfo.tipsTitle }}</div>
</div>
<div v-else class="flow_line_box"> // 流程线类型
// 横向线段<div v-for="sub in [0, 1, 2, 3, 4, 5]" :key="'line_' + sub" :class="'flow_line line_' + sub + ' ' + (detailInfo.leftFlowType[sub] === '1' ? 'isActive' : (detailInfo.leftFlowType[sub] === '2' ? 'isActiveDashed' : ''))" @click="lineChange(sub)"/>
// 竖向线段<div v-for="sub in [6, 7, 8, 9, 10, 11]" :key="'line_' + sub" :class="'flow_line_vertical line_' + sub + ' ' + (detailInfo.leftFlowType[sub] === '1' ? 'isActive' : (detailInfo.leftFlowType[sub] === '2' ? 'isActiveDashed' : ''))" @click="lineChange(sub)"/>
// 竖向箭头<div v-for="sub in [12, 19]" :key="'arrow_' + sub" :class="'flow_line_arrow arrow_' + sub" @click="lineChange(sub)"><i v-if="detailInfo.leftFlowType[sub] === '1'" class="el-icon-caret-top color_theme_main" /><i v-else-if="detailInfo.leftFlowType[sub] === '2'" class="el-icon-caret-bottom color_theme_main" /><i v-else class="el-icon-full-screen" /></div>
// 横向箭头<div v-for="sub in [14, 17]" :key="'arrow_' + sub" :class="'flow_line_arrow arrow_' + sub" @click="lineChange(sub)"><i v-if="detailInfo.leftFlowType[sub] === '1'" class="el-icon-caret-left color_theme_main" /><i v-else-if="detailInfo.leftFlowType[sub] === '2'" class="el-icon-caret-right color_theme_main" /><i v-else class="el-icon-full-screen" /></div>
// 流程说明文字<div v-if="[1, 2, 3, 4, 5, 6].includes(detailInfo.showLeftLine)" :class="tipsClass(detailInfo)">{{ detailInfo.tipsTitle }}</div>
</div>
2)节点编辑入口
handleEdit(item) { // 点击流程图中的单个节点,触发该节点编辑this.detailInfo = object.assign({}, item);this.detailVisible = true;
},
3)线段、箭头点击事件
lineChange(index) {const max = 2; // 线段、箭头均有3个值,0, 1, 2let target = parseInt(this.detailInfo.leftFlowType[index]);if (target < max) {target++;} else {target = 0;}this.detailInfo.leftFlowType = this.detailInfo.leftFlowType.substring(0, index) + target + this.detailInfo.leftFlowType.substring(index + 1, this.detailInfo.leftFlowType.length);
},
4)节点重置及保存
handleDetailReset() { // 节点重置// detailInfoSource 节点默认信息const resetData = Object.assign({}, detailInfoSource);Object.keys(resetData).forEach(dataKey => {this.detailInfo[dataKey] = resetData[dataKey];});
},
handleDetailSave() { // 节点保存// 保存使用splice替换原位置节点信息// flowCode保存的当前节点的索引this.list.splice(this.detailInfo.flowCode, 1, Object.assign({}, this.detailInfo));this.detailVisible = false;
},
2.流程图预览
1)template
<el-col v-for="item in list" :span="2" :key="item.flowCode"><div v-if="item.flowType === 'flow'" class="flow_item_box_mini" @click="handleEdit(item)"> // 流程节点类型
// 节点图标及节点文字<div :class="'flow_line_content ' + (item.jumpFlag === 1 ? 'can_click' : (item.jumpFlag === 2 ? 'can_click go' : ''))"><div class="flow_line_icon"><svg-icon v-if="item.iconType === 'system'" :icon-class="item.iconKeyword || ''" /><img v-else-if="item.iconType === 'custom' && customIconsList.includes(item.iconKeyword)" :src="require('@/assets/system_icons/' + item.iconKeyword)" style="" /> </div><div class="flow_line_label">{{ item.flowName }}</div></div>
// 横向线段<div v-for="sub in [0, 1, 2, 3, 4, 5]" :key="'line_' + sub" :class="'flow_line line_' + sub + ' ' + (item.leftFlowType[sub] === '1' ? 'isActive' : (item.leftFlowType[sub] === '2' ? 'isActiveDashed' : ''))"/>
// 竖向线段<div v-for="sub in [6, 7, 8, 9, 10, 11]" :key="'line_' + sub" :class="'flow_line_vertical line_' + sub + ' ' + (item.leftFlowType[sub] === '1' ? 'isActive' : (item.leftFlowType[sub] === '2' ? 'isActiveDashed' : ''))"/>
// 竖向箭头<div v-for="sub in [12, 13, 18, 19]" :key="'arrow_' + sub" :class="'flow_line_arrow arrow_' + sub"><i v-if="item.leftFlowType[sub] === '1'" class="el-icon-caret-top color_theme_main" /><i v-else-if="item.leftFlowType[sub] === '2'" class="el-icon-caret-bottom color_theme_main" /><i v-else class="el-icon-full-screen" /></div>
// 横向箭头<div v-for="sub in [14, 15, 16, 17]" :key="'arrow_' + sub" :class="'flow_line_arrow arrow_' + sub"><i v-if="item.leftFlowType[sub] === '1'" class="el-icon-caret-left color_theme_main" /><i v-else-if="item.leftFlowType[sub] === '2'" class="el-icon-caret-right color_theme_main" /><i v-else class="el-icon-full-screen" /></div></div><div v-else class="flow_line_box_mini" @click="handleEdit(item)"> // 流程线类型
// 横向线段<div v-for="sub in [0, 1, 2, 3, 4, 5]" :key="'line_' + sub" :class="'flow_line line_' + sub + ' ' + (item.leftFlowType[sub] === '1' ? 'isActive' : (item.leftFlowType[sub] === '2' ? 'isActiveDashed' : ''))"/>
// 竖向线段<div v-for="sub in [6, 7, 8, 9, 10, 11]" :key="'line_' + sub" :class="'flow_line_vertical line_' + sub + ' ' + (item.leftFlowType[sub] === '1' ? 'isActive' : (item.leftFlowType[sub] === '2' ? 'isActiveDashed' : ''))"/>
// 竖向箭头<div v-for="sub in [12, 19]" :key="'arrow_' + sub" :class="'flow_line_arrow arrow_' + sub"><i v-if="item.leftFlowType[sub] === '1'" class="el-icon-caret-top color_theme_main" /><i v-else-if="item.leftFlowType[sub] === '2'" class="el-icon-caret-bottom color_theme_main" /><i v-else class="el-icon-full-screen" /></div>
// 横向箭头<div v-for="sub in [14, 17]" :key="'arrow_' + sub" :class="'flow_line_arrow arrow_' + sub"><i v-if="item.leftFlowType[sub] === '1'" class="el-icon-caret-left color_theme_main" /><i v-else-if="item.leftFlowType[sub] === '2'" class="el-icon-caret-right color_theme_main" /><i v-else class="el-icon-full-screen" /></div>
// 流程说明文字<div v-if="[1, 2, 3, 4, 5, 6].includes(item.showLeftLine)" :class="tipsClass(item)">{{ item.tipsTitle }}</div></div>
</el-col>
2)流程初始化
created() {// http请求落库数据,若没有数据,则默认增加第一行节点this.handleAdd();
},
3)流程节点增删操作
handleAdd(){// flowCode保存当前节点索引,方便节点编辑保存for (let i = 0;i <= 11; i++) {this.list.push(Object.assign({}, detailInfoSource, { flowCode:this.list.length }));}
},
handleDeleteBatch() {// 剔除最后的一行节点(12个)for (let i = 0;i < 11;i++){this.list.pop();}
},
四、样式具体代码
<style lang="scss" scoped>
.flow_item_box {width:480px;height:400px;text-align:center;.flow_line_content {width:240px;height:200px;float:left;margin-left:120px;margin-top:100px;padding-top:24px;border:1px dashed #eaeaea;.flow_line_icon {font-size:72px;}.flow_line_label {margin-top:12px;font-size:24px;font-weight:bold;}&.can_click {color:$theme-color-main;&.go {color:#f56c6c;}}}.flow_line {position:absolute;height:6px;width:230px;background:#ccc;cursor:pointer;&.line_1 {right:0;}&.line_2 {width:80px;left:20px;top:calc(50% - 3px);}&.line_3 {width:80px;right:20px;top:calc(50% - 3px);}&.line_4 {bottom:0;}&.line_5 {right:0;bottom:0;}&.isActive {background:$theme-color-main;}&.isActiveDashed {background:linear-gradient(90deg, $theme-color-main 50%, #fff 50%);background-size:12px 100%;}}.flow_line_vertical {position:absolute;height:190px;width:6px;background:#ccc;cursor:pointer;&.line_7 {top:20px;height:60px;right:calc(50% - 3px);}&.line_8 {right:0;}&.line_9 {bottom:0;}&.line_10 {bottom:20px;height:60px;right:calc(50% - 3px);}&.1ine_11 {right:0;bottom:0;}&.isActive {background:$theme-color-main;}&.isActiveDashed {background:linear-gradient($theme-color-main 50%, #fff 50%);background-size:100% 12px;}}.flow_line_arrow {position:absolute;width:20px;height:20px;text-align:center;cursor:pointer;font-size:18px;font-weight:bold;line-height:20px;&.arrow_12 {right:calc(50% - 10px);}&.arrow_13 {top:80px;right:calc(50% - 10px);}&.arrow_14 {top:calc(50% - 10px);}&.arrow_15 {top:calc(50% - 10px);left:100px;}&.arrow_16 {top:calc(50% - 10px);right:100px;}&.arrow_17 {top:calc(50% - 10px);right:0;}&.arrow_18 {bottom:80px;right:calc(50% - 10px);}&.arrow_19 {bottom:0;right:calc(50% - 10px);}}.flow_line_tips {position:absolute;width:80px;line-height:20px;font-size:14px;left:calc(75% + 20px);text-align:left;&.tips_t_u {top:-20px;}&.tips_t_d {top:4px;}&.tips_m_u {top:calc(50% - 20px);}&.tips_m_d {top:calc(50% + 4px);}&.tips_b_u {top:calc(100% - 24px);}&.tips_b_d {top:100%;}}
}
.flow_line_box {width:480px;height:400px;text-align:center;.flow_line {position:absolute;height:6px;width:230px;background:#ccc;cursor:pointer;&.line_1 {right:0;}&.line_2 {width:220px;left:20px;top:calc(50% - 3px);}&.line_3 {width:220px;right:20px;top:calc(50% - 3px);}&.line_4 {bottom:0;}&.line_5 {right:0;bottom:0;}&.isActive {background:$theme-color-main;}&.isActiveDashed {background:linear-gradient(90deg, $theme-color-main 50%, #fff 50%);background-size:12px 100%;}}.flow_line_vertical {position:absolute;height:190px;width:6px;background:#ccc;cursor:pointer;&.line_7 {height:180px;top:20px;right:calc(50% - 3px);}&.line_8 {right:0;}&.line_9 {bottom:0;}&.line_10 {height:180px;bottom:20px;right:calc(50% - 3px);}&.1ine_11 {right:0;bottom:0;}&.isActive {background:$theme-color-main;}&.isActiveDashed {background:linear-gradient($theme-color-main 50%,#fff 50%);background-size:100% 12px;}}.flow_line_arrow {position:absolute;width:20px;height:20px;text-align:center;cursor:pointer;font-size:18px;font-weight:bold;line-height:20px;&.arrow_12 {right:calc(50% - 10px);}&.arrow_14 {top:calc(50% - 10px);}&.arrow_17 {top:calc(50% - 10px);right:0;}&.arrow_19 {bottom:0;right:calc(50% - 10px);}}.flow_line_tips {position:absolute;width:440px;line-height:20px;font-size:14px;left:20px;text-align:left;&.tips_t_u {top:-20px;}&.tips_t_d {top:4px;}&.tips_m_u {top:calc(50% - 20px);}&.tips_m_d {top:calc(50% + 4px);}&.tips_b_u {top:calc(100% - 24px);}&.tips_b_d {top:100%;}}
}
.flow_item_box_mini {width:120px;height:120px;text-align:centercursor:pointer;.flow_line_content {width:60px;height:60px;float:left;margin-left:30px;margin-top:30px;padding-top:2px;border:1px dashed #eaeaea;.flow_line_icon {font-size:24px;}flow_line_label {margin-top:2px;font-size:12px;font-weight:bold;}&.can_click {color:$theme-color-main;&.go {color:#f56c6c;}}}.flow_line {position:absolute;height:1px;width:60px;background:#ccc;&.line_1 {margin-left:60px;}&.line_2 {width:30px;margin-top:59px;}&.line_3 {margin-left:90px;width:30px;margin-top:59px;}&.line_4 {margin-top:118px;}&.line_5 {margin-left:60px;margin-top:118px;}&.isActive {height:2px;background:$theme-color-main;}&.isActiveDashed {height:2px;background:linear-gradient(90deg, $theme-color-main 50%, #fff 50%);background-size:4px 100%;}}.flow_line_vertical {position:absolute;height:60px;width:1px;background:#ccc;&.line_7 {height:30px;margin-left:59px;}&.1ine_8 {margin-left:118px;}&.line_9 {margin-top:60px;}&.line_10 {margin-top:90px;height:30px;margin-left:59px;}&.line_11 {margin-left:118px;margin-top:60px;}&.isActive {width:2px;background:$theme-color-main;}&.isActiveDashed {width:2px;background:linear-gradient($theme-color-main 50%, #fff 50%);background-size:100% 4px;}}.flow_line_arrow {position:absolute;width:14px;height:14px;text-align:center;font-size:14px;font-weight:bold;line-height:14px;&.arrow_12 {margin-left:53px;margin-top:-4px;}&.arrow_13 {margin-top:21px;margin-left:53px;}&.arrow_14 {margin-top:53px;margin-left:-4px;}&.arrow_15 {margin-top:53px;margin-left:20px;}&.arrow_16 {margin-top:53px;margin-Left:85px;}&.arrow_17 {margin-top:53px;margin-left:108px;}&.arrow_18 {margin-top:85px;margin-left:53px;}&.arrow_19 {margin-top:110px;margin-left:53px;}}
}
.flow_line_box_mini {width:120px;height:120px;text-align:center;cursor:pointer;.flow_line{position:absolute;height:1px;width:60px;background:#ccc;&.line_1 {margin-left:60px;}&.line_2 {margin-top:59px;}&.line_3 {margin-left:60px;margin-top:59px;}&.line_4 {margin-top:118px;}&.line_5 {margin-lLeft:60px;margin-top:118px;}&.isActive {height:2px;background:$theme-color-main;}&.isActiveDashed {height:2px;background:linear-gradient(90deg, $theme-color-main 50%, #fff 50%);background-size:4px 100%;}}.flow_line_vertical {position:absolute;height:60px;width:1px;background:#ccc;&.line_7 {height:60px;margin-left:59px;}&.line_8 {margin-left:118px;}&.line_9 {margin-top:60px;}&.line_10 {margin-top:60px;margin-left:59px;}&.1ine_11 {margin-left:118px;margin-top:60px;}&.isActive {width:2px;background:$theme-color-main;}&.isActiveDashed {width:2px;background:linear-gradient($theme-color-main 50%, #fff 50%);background-size:100% 4px;}}.flow_line_arrow {position:absolute;width:14px;height:14px;text-align:center;font-size:14px;font-weight:bold;line-height:14px;&.arrow_12 {margin-left:53px;margin-top:-4px;}&.arrow_14 {margin-top:53px;margin-left:-4px;}&.arrow_17 {margin-top:53px;margin-left:108px;}&.arrow_19 {margin-top:110px;margin-left:53px;}}.flow_line_tips {position:absolute;width:102px;line-height:16px;font-size:12px;margin-left:8px;text-align:left;max-height:66px;overflow:hidden;&.tips_t_u {margin-top:-16px;}&.tips_t_d {margin-top:2px;}&.tips_m_u {margin-top:43px;}&.tips_m_d {margin-top:60px;}&.tips_b_u {margin-top:102px;}&.tips_b_d {margin-top:120px;}}
}
</style>
五、总结
以上为显式管理业务流程的全部内容,各位感兴趣的筒子们,若在实践过程中有哪些好的优化方向或具体优化方案,欢迎踊跃讨论~