This commit is contained in:
那些花儿
2025-07-31 10:36:02 +08:00
parent 73974dbdd3
commit 8118ea9687
21 changed files with 472 additions and 333 deletions
@@ -20,6 +20,7 @@ import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.HighLightVO;
import com.budwk.app.flow.vo.ProcessInstanceVO;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -137,11 +138,23 @@ public class FlowCommonController {
List<ProcessTaskVO> res = new ArrayList<>();
processTaskList.forEach(processTask -> {
ProcessTaskVO vo = BeanUtil.toBean(processTask, ProcessTaskVO.class);
if (vo.getTaskState().equals(ProcessTaskStateEnum.FINISHED.getCode()) && StrUtil.isNotBlank(vo.getOperator())) {
}
res.add(vo);
});
return Result.success(res);
}
@At
@SaCheckLogin
@ApiOperation("流程图高亮")
public Result instanceHighLight(@Param("instanceId") Long instanceId) {
HighLightVO highLightVO = flowEngine.processInstanceService().highLight(instanceId);
return Result.success(highLightVO);
}
@At
@SaCheckLogin
@Aop(TransAop.READ_COMMITTED)
@@ -209,6 +222,7 @@ public class FlowCommonController {
@At("/executeTask")
@SaCheckLogin
@ApiOperation("执行任务")
@Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String param) {
Dict args = Json.fromJson(Dict.class, param);
Long processTaskId = args.getLong(FlowConst.PROCESS_TASK_ID_KEY);
@@ -43,7 +43,8 @@ public class FlowDefineController {
@Ok("beetl:/platform/flow/define/preview.html")
@SaCheckLogin
public void preview(HttpServletRequest request) {
request.setAttribute("id", request.getParameter("id"));
request.setAttribute("defineId", request.getParameter("defineId"));
request.setAttribute("instanceId", request.getParameter("instanceId"));
}
@@ -125,6 +125,7 @@ public class FlowDesignController {
jsonObject.set("instanceViewUrl", processDesign.getInstanceViewUrl());
jsonObject.set("h5InstanceViewUrl", processDesign.getH5InstanceViewUrl());
jsonObject.set("icon", processDesign.getIcon());
jsonObject.set("description", processDesign.getDescription());
processDesign.setContent(jsonObject);
dao.insert(processDesign);
return Result.success();
@@ -143,6 +144,7 @@ public class FlowDesignController {
jsonObject.set("instanceViewUrl", processDesign.getInstanceViewUrl());
jsonObject.set("h5InstanceViewUrl", processDesign.getH5InstanceViewUrl());
jsonObject.set("icon", processDesign.getIcon());
jsonObject.set("description", processDesign.getDescription());
processDesign.setContent(jsonObject);
processDesignService.update(processDesign);
return Result.success();
@@ -161,12 +163,12 @@ public class FlowDesignController {
jsonObject.set("instanceViewUrl", processDesign.getInstanceViewUrl());
jsonObject.set("h5InstanceViewUrl", processDesign.getH5InstanceViewUrl());
jsonObject.set("icon", processDesign.getIcon());
jsonObject.set("description", processDesign.getDescription());
processDesign.setContent(jsonObject);
processDesignService.update(processDesign);
return Result.success();
}
@At
@SaCheckLogin
@ApiOperation("删除流程设计")
@@ -13,8 +13,8 @@ import java.util.List;
import java.util.stream.Collectors;
/**
*
* 流程模型
*
* @author mldong
* @date 2023/4/25
*/
@@ -24,6 +24,8 @@ public class ProcessModel extends BaseModel {
private String type; // 流程定义分类
private String icon; // 流程定义图标
private String category; // 流程定义分类
private String remark; // 流程备注
private String description; // 流程说明
private String instanceUrl; // 启动实例要填写的表单key
private String h5InstanceUrl; // 启动实例要填写的手机端表单key
private String instanceViewUrl;
@@ -39,13 +41,14 @@ public class ProcessModel extends BaseModel {
/**
* 获取开始节点
*
* @return
*/
public StartModel getStart() {
StartModel startModel = null;
for (int i = 0; i < nodes.size(); i++) {
NodeModel nodeModel = nodes.get(i);
if(nodeModel instanceof StartModel) {
if (nodeModel instanceof StartModel) {
startModel = (StartModel) nodeModel;
break;
}
@@ -55,12 +58,13 @@ public class ProcessModel extends BaseModel {
/**
* 获取process定义的指定节点名称的节点模型
*
* @param nodeName 节点名称
* @return
*/
public NodeModel getNode(String nodeName) {
for(NodeModel node : nodes) {
if(node.getName().equals(nodeName)) {
for (NodeModel node : nodes) {
if (node.getName().equals(nodeName)) {
return node;
}
}
@@ -69,23 +73,24 @@ public class ProcessModel extends BaseModel {
/**
* 获取下一个任务节点模型集合
*
* @param nodeName
* @return
*/
public List<TaskModel> getNextTaskModels(String nodeName) {
List<TaskModel> res = new ArrayList<>();
NodeModel nodeModel = getNode(nodeName);
if(nodeModel == null) return res;
if (nodeModel == null) return res;
// 获取所有输出边的目标节点
List<NodeModel> nextNodeModelList = nodeModel.getOutputs().stream().map(TransitionModel::getTarget).toList();
nextNodeModelList.forEach(item->{
if(item instanceof TaskModel) {
nextNodeModelList.forEach(item -> {
if (item instanceof TaskModel) {
res.add((TaskModel) item);
}
});
if(res.isEmpty()) {
if (res.isEmpty()) {
// 如果下一个节点不存在任务节点,递归往下找
nextNodeModelList.forEach(item->{
nextNodeModelList.forEach(item -> {
List<TaskModel> taskModelList = getNextTaskModels(item.getName());
res.addAll(taskModelList);
});
@@ -95,19 +100,22 @@ public class ProcessModel extends BaseModel {
/**
* 获取下一个任务节点的候选人
*
* @param nodeName
* @return
*/
public List<Candidate> getNextTaskModelCandidates(String nodeName) {
List<Candidate> res = new ArrayList<>();
List<TaskModel> nextTaskModels = getNextTaskModels(nodeName);
nextTaskModels.forEach(item->{
nextTaskModels.forEach(item -> {
res.addAll(getCandidates(item));
});
return res;
}
/**
* 根据任务模型获取候选人
*
* @param taskModel
* @return
*/
@@ -115,29 +123,31 @@ public class ProcessModel extends BaseModel {
List<Candidate> res = new ArrayList<>();
// 从上下文中查找候选人处理人
List<CandidateHandler> handlerList = ServiceContext.findList(CandidateHandler.class);
handlerList.forEach(handler->{
handlerList.forEach(handler -> {
// 通过候选从处理类获取候选人集合
List<Candidate> candidateList = handler.handle(taskModel);
if(candidateList!=null) {
if (candidateList != null) {
res.addAll(candidateList);
}
});
// 通过候选人处理类获取修选人
String candidateHandler = taskModel.getCandidateHandler();
if(StrUtil.isNotEmpty(candidateHandler)) {
if (StrUtil.isNotEmpty(candidateHandler)) {
CandidateHandler candidateHandlerClass = ReflectUtil.newInstance(candidateHandler);
List<Candidate> candidateList = candidateHandlerClass.handle(taskModel);
if(candidateList!=null) {
if (candidateList != null) {
res.addAll(candidateList);
}
}
// 去重
return res.stream().distinct().collect(Collectors.toList());
}
/**
* 根据指定的节点类型返回流程定义中所有模型对象
*
* @param clazz 节点类型
* @param <T> 泛型
* @param <T> 泛型
* @return 节点列表
*/
public <T> List<T> getModels(Class<T> clazz) {
@@ -147,10 +157,10 @@ public class ProcessModel extends BaseModel {
}
private <T> void buildModels(List<T> models, List<T> nextModels, Class<T> clazz) {
for(T nextModel : nextModels) {
if(!models.contains(nextModel)) {
for (T nextModel : nextModels) {
if (!models.contains(nextModel)) {
models.add(nextModel);
buildModels(models, ((NodeModel)nextModel).getNextModels(clazz), clazz);
buildModels(models, ((NodeModel) nextModel).getNextModels(clazz), clazz);
}
}
}
@@ -15,6 +15,7 @@ import java.util.List;
public class LfModel extends BaseModel {
private String type; // 流程定义分类
private String category;
private String description; // 流程描述
private String expireTime;// 过期时间(常量或变量)
private String instanceUrl; // 启动实例的url,前后端分离后,定义为路由名或或路由地址
private String h5InstanceUrl;
@@ -1,20 +1,21 @@
package com.budwk.app.flow.engine.parser;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.engine.model.NodeModel;
import com.budwk.app.flow.engine.model.ProcessModel;
import com.budwk.app.flow.engine.model.TaskModel;
import com.budwk.app.flow.engine.model.TransitionModel;
import com.budwk.app.flow.engine.model.logicflow.*;
import com.budwk.app.flow.engine.model.logicflow.LfEdge;
import com.budwk.app.flow.engine.model.logicflow.LfModel;
import com.budwk.app.flow.engine.model.logicflow.LfNode;
import java.io.ByteArrayInputStream;
import java.util.List;
public class ModelParser {
private ModelParser(){}
private ModelParser() {
}
// /**
// * 将json定义文件解析成流程模型对象
@@ -71,7 +72,7 @@ public class ModelParser {
ProcessModel processModel = new ProcessModel();
List<LfNode> nodes = lfModel.getNodes();
List<LfEdge> edges = lfModel.getEdges();
if(CollectionUtil.isEmpty(nodes) || CollectionUtil.isEmpty(edges) ) {
if (CollectionUtil.isEmpty(nodes) || CollectionUtil.isEmpty(edges)) {
return processModel;
}
// 流程定义基本信息
@@ -79,6 +80,7 @@ public class ModelParser {
processModel.setDisplayName(lfModel.getDisplayName());
processModel.setType(lfModel.getType());
processModel.setCategory(lfModel.getCategory());
processModel.setDescription(lfModel.getDescription());
processModel.setInstanceUrl(lfModel.getInstanceUrl());
processModel.setH5InstanceUrl(lfModel.getH5InstanceUrl());
processModel.setInstanceViewUrl(lfModel.getInstanceViewUrl());
@@ -87,10 +89,10 @@ public class ModelParser {
processModel.setPostInterceptors(lfModel.getPostInterceptors());
processModel.setPreInterceptors(lfModel.getPreInterceptors());
// 流程节点信息
nodes.forEach(node->{
String type = node.getType().replace(NodeParser.NODE_NAME_PREFIX,"");
NodeParser nodeParser = ServiceContext.findByName(type,NodeParser.class);
if(nodeParser!=null) {
nodes.forEach(node -> {
String type = node.getType().replace(NodeParser.NODE_NAME_PREFIX, "");
NodeParser nodeParser = ServiceContext.findByName(type, NodeParser.class);
if (nodeParser != null) {
nodeParser.parse(node, edges);
NodeModel nodeModel = nodeParser.getModel();
processModel.getNodes().add(nodeParser.getModel());
@@ -100,11 +102,11 @@ public class ModelParser {
}
});
// 循环节点模型,构造输入边、输出边的source、target
for(NodeModel node : processModel.getNodes()) {
for(TransitionModel transition : node.getOutputs()) {
for (NodeModel node : processModel.getNodes()) {
for (TransitionModel transition : node.getOutputs()) {
String to = transition.getTo();
for(NodeModel node2 : processModel.getNodes()) {
if(to.equalsIgnoreCase(node2.getName())) {
for (NodeModel node2 : processModel.getNodes()) {
if (to.equalsIgnoreCase(node2.getName())) {
node2.getInputs().add(transition);
transition.setTarget(node2);
}
@@ -73,9 +73,14 @@ public class ProcessDefine extends BaseModel {
@Column
private Integer version;
@Comment
@Comment("首字母")
@Column
@ColDefine(type = ColType.CHAR)
private Character pinyinName;
@Comment("流程描述")
@Column
@ColDefine(type = ColType.TEXT)
private String description;
}
@@ -20,8 +20,6 @@ import org.nutz.dao.entity.annotation.*;
@Comment("流程设计")
public class ProcessDesign extends BaseModel {
private static final long serialVersionUID = 1L;
@Comment("主键")
@Id
private Long id;
@@ -70,6 +68,11 @@ public class ProcessDesign extends BaseModel {
@Column
private String remark;
@Comment("流程描述")
@Column
@ColDefine(type = ColType.TEXT)
private String description;
@Comment("流程定义")
@Column
@ColDefine(type = ColType.MYSQL_JSON)
@@ -219,12 +219,6 @@ public interface ProcessInstanceService extends BaseService<ProcessInstance> {
* @param actorId
*/
void updateCCStatus(Long processInstanceId, String actorId);
/**
* 自定义分页查询我的抄送
* @param param
* @return
*/
// CommonPage<ProcessInstanceVO> ccInstancePage(ProcessInstancePageParam param);
/**
* 获取流程参与人回显文本数据
@@ -117,6 +117,7 @@ public class ProcessDefineServiceImpl extends BaseServiceImpl<ProcessDefine> imp
define.setH5InstanceViewUrl(processModel.getH5InstanceViewUrl());
define.setIcon(processModel.getIcon());
define.setPinyinName(PinyinUtil.getFirstLetter(define.getDisplayName().charAt(0)));
define.setDescription(processModel.getDescription());
define.setState(1);
define.setContent(JSONUtil.parseObj(defineJsonStr));
@@ -175,8 +175,18 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
processTask.setTaskType(taskModel.getTaskType().getCode());
processTask.setFormKey(taskModel.getForm());
processTask.setProcessInstanceId(execution.getProcessInstanceId());
execution.getArgs().put(FlowConst.IS_FIRST_TASK_NODE, FlowUtil.isFistTaskName(execution.getProcessModel(), taskModel.getName()));
// 增加是否为第一个任务节点标识
execution.getArgs().put(FlowConst.IS_FIRST_TASK_NODE, FlowUtil.isFistTaskName(execution.getProcessModel(), taskModel.getName()));
//过滤上个任务的表单数据 避免造成污染 每个任务只保存自己的表单数据
Dict cleanArgs = Dict.create();
execution.getArgs().forEach((k, v) -> {
if (!k.startsWith(FlowConst.TASK_FORM_DATA_PREFIX)) {
cleanArgs.set(k, v);
}
});
execution.setArgs(cleanArgs);
processTask.setVariable(JSONUtil.toJsonStr(execution.getArgs()));
processTask.setCreatedAt(now);
processTask.setUpdatedAt(now);
@@ -44,6 +44,9 @@ public class ProcessTaskVO extends ProcessTask {
private List<JSONObject> taskActorList;
// 任务参与者
private List<JSONObject> taskOperatorList;
// @Schema(description = "当前用户是否可执行")
private boolean executable;
// @Schema(description = "节点定义信息")
File diff suppressed because one or more lines are too long
@@ -6,8 +6,6 @@
<div class="title-section">
<h2 class="page-title">{{ pageTitle }}</h2>
<div class="page-breadcrumb">
<!-- <span>工作流管理</span>-->
<!-- <span class="separator">></span>-->
<span>{{ isNewApplication ? "发起申请" : "任务处理" }}</span>
</div>
</div>
@@ -27,7 +25,7 @@
<div class="panel-header">
<h3 class="panel-title">流程信息</h3>
<div class="panel-actions">
<el-button size="small" type="text" @click="showProcessDiagram" v-if="canShowDiagram">
<el-button size="small" type="text" @click="showProcessDiagram" v-if="isNewApplication">
<i class="el-icon-view"></i>
查看流程图
</el-button>
@@ -36,14 +34,16 @@
<div class="panel-content">
<div class="info-table">
<!-- 新申请场景 -->
<template v-if="isNewApplication || (processInstance && processInstance.state === 30)">
<template v-if="isNewApplication">
<div class="info-row">
<div class="info-label">流程名称</div>
<div class="info-value">{{ processDefinition.displayName || processDefinition.name }}</div>
<div class="info-value">{{ processDefinition.displayName }}</div>
</div>
<div class="info-row">
<div class="info-label">流程说明</div>
<div class="info-value">{{ processDefinition.description }}</div>
<div class="info-value">
<div v-html="processDefinition.description"></div>
</div>
</div>
</template>
@@ -61,6 +61,12 @@
<div class="info-label">发起时间</div>
<div class="info-value">{{ formatDate(processInstance.createdAt) }}</div>
</div>
<div class="info-row">
<div class="info-label">流程进度</div>
<div class="info-value">
<span @click="showProcessDiagram" style="color: var(--color-primary); cursor: pointer">查看流程图</span>
</div>
</div>
</template>
</div>
</div>
@@ -82,20 +88,13 @@
</div>
<!-- 表单处理区域 -->
<div
class="form-panel"
v-if="isNewApplication || (processInstance && processInstance.state === 30) || todoTasks.map((v) => v.id).includes(taskId)"
>
<div class="form-panel" v-if="isNewApplication || todoTasks.map((v) => v.id).includes(taskId)">
<div class="panel-header">
<h3 class="panel-title">{{ formPanelTitle }}</h3>
</div>
<div class="panel-content">
<!-- 新申请表单 -->
<div
v-if="isNewApplication || (processInstance && processInstance.state === 30)"
id="application-form-container"
class="form-content"
>
<div v-if="isNewApplication" id="application-form-container" class="form-content">
<div v-if="pjaxLoading.apply" class="loading-state">
<i class="el-icon-loading"></i>
<span>正在加载申请表单...</span>
@@ -191,6 +190,7 @@ class PjaxSync {
}
}
}
// 创建全局实例
const pjaxSync = new PjaxSync()
@@ -289,7 +289,7 @@ module.exports = {
// 表单面板标题
formPanelTitle() {
if (this.isNewApplication) {
return "申请表单"
return "填写申请"
} else if (this.shouldShowTaskForm) {
return this.currentTask.displayName || "任务表单"
} else if (this.shouldShowHistoryProcess) {
@@ -301,9 +301,7 @@ module.exports = {
// 是否显示历史办理过程
shouldShowHistoryProcess() {
// debugger
// return this.businessId && this.isTaskInProgress && !this.isFirstTaskNode
return true
return this.instanceId !== null
},
// 是否显示任务表单
@@ -311,11 +309,6 @@ module.exports = {
return this.taskId != null
},
// 是否可以显示流程图
canShowDiagram() {
return (this.isNewApplication && this.processDefinition.id) || (this.isExistingProcess && this.processInstance.processId)
},
// 判断是否为新申请
isNewApplication() {
return !this.taskId && !this.instanceId && this.defineKey
@@ -338,10 +331,11 @@ module.exports = {
// 流程图预览URL
processDesignUrl() {
debugger
if (this.isNewApplication && this.processDefinition.id) {
return `/flow/define/preview?id=${this.processDefinition.id}`
} else if (this.isExistingProcess && this.processInstance.processId) {
return `/flow/define/preview?id=${this.processInstance.processId}`
return `/flow/define/preview?defineId=${this.processDefinition.id}`
} else if (this.isExistingProcess && this.processInstance.processDefineId) {
return `/flow/define/preview?defineId=${this.processInstance.processDefineId}&instanceId=${this.instanceId}`
}
return ""
}
@@ -474,6 +468,7 @@ module.exports = {
// 显示流程图
showProcessDiagram() {
debugger
if (this.processDesignUrl) {
this.designUrl = this.processDesignUrl
this.designVisible = true
@@ -1,45 +0,0 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<div id="approvalApp" v-cloak>
<snaker-flow
:task_id="taskId"
:instance_id="instanceId"
:business_id="businessId"
:pjax_config="{
push: false,
replace: false,
timeout: 10000
}"
></snaker-flow>
</div>
<script>
new Vue({
el: "#approvalApp",
data() {
return {
taskId: null,
instanceId: null,
businessId: null,
formConfig: {}
}
},
methods: {
handleTaskSubmitted() {},
handleCancel() {
window.history.back()
}
},
created() {
this.taskId = new URLSearchParams(window.location.search).get("taskId")
this.instanceId = new URLSearchParams(window.location.search).get("instanceId")
this.businessId = new URLSearchParams(window.location.search).get("businessId")
}
})
</script>
<!--#
}
#-->
@@ -22,16 +22,13 @@ layout("/layouts/platform.html"){
</el-card>
<el-card shadow="never">
<table-tool>
</table-tool>
<table-tool></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="displayName" label="流程定义名称"></el-table-column>
<el-table-column prop="name" label="流程定义编码"></el-table-column>
<el-table-column prop="categoryName" label="流程分类">
<template slot-scope="{row}">{{categoryOptions.find(item => item.id ===
row.category)?.categoryName}}
</template>
<template slot-scope="{row}">{{categoryOptions.find(item => item.id === row.category)?.categoryName}}</template>
</el-table-column>
<el-table-column prop="version" label="版本号"></el-table-column>
<el-table-column prop="state" label="状态">
@@ -44,12 +41,9 @@ layout("/layouts/platform.html"){
<el-table-column label="操作" fixed="right" width="350px">
<template slot-scope="{row}">
<el-button type="primary" size="mini" @click="onView(row)">查看</el-button>
<el-button v-if="row.state===1" type="danger" size="mini" @click="onDisable(row)">停用
</el-button>
<el-button v-if="row.state===0" type="primary" size="mini" @click="onEnable(row)">启用
</el-button>
<el-button v-if="row.state===0" type="danger" size="mini" @click="onDelete(row)">删除
</el-button>
<el-button v-if="row.state===1" type="danger" size="mini" @click="onDisable(row)">停用</el-button>
<el-button v-if="row.state===0" type="primary" size="mini" @click="onEnable(row)">启用</el-button>
<el-button v-if="row.state===0" type="danger" size="mini" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -59,11 +53,9 @@ layout("/layouts/platform.html"){
<el-dialog title="查看流程图" :visible.sync="designVisible" class="design-dialog" width="80%">
<div style="height: calc(100vh - 150px); width: 100%">
<iframe v-if="designVisible" :src="designUrl" frameborder="0" height="100%"
style="height: 100%; width: inherit"></iframe>
<iframe v-if="designVisible" :src="designUrl" frameborder="0" height="100%" style="height: 100%; width: inherit"></iframe>
</div>
</el-dialog>
</div>
<script>
@@ -80,7 +72,7 @@ layout("/layouts/platform.html"){
methods: {
onView(row) {
this.designVisible = true
this.designUrl = "/flow/define/preview?id=" + row.id
this.designUrl = "/flow/define/preview?defineId=" + row.id
},
onDisable(row) {
@@ -89,7 +81,7 @@ layout("/layouts/platform.html"){
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/define/disable", { id: row.id }).then(res => {
this.$axios.post("/flow/define/disable", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("停用成功")
this.pageData()
@@ -103,7 +95,7 @@ layout("/layouts/platform.html"){
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/define/enable", { id: row.id }).then(res => {
this.$axios.post("/flow/define/enable", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success("启用成功")
this.pageData()
@@ -1,66 +1,348 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>流程设计器</title>
<script src="/assets/platform/plugins/vue/vue.js"></script>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/element-ui/lib/index.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
<!-- 引入 core 包和对应 css-->
<script src="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/logic-flow.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/style/index.css" />
<script src="/assets/platform/plugins/snaker/SnakerflowDesigner.umd.js"></script>
<style>
#app {
height: 100vh;
}
</style>
</head>
<body>
<div id="app">
<snaker-flow-designer ref="designer"
v-model="flowData"
:show-doc="false"
:viewer="true"
node-render-type="html"
:wf-config="{
showHelp: false,
}"
:config="{
grid:{ size: 25,visible: true,type: 'dot',config:{color: '#ababab',thickness: 1}}
}"
></snaker-flow-designer>
</div>
</body>
<script>
Vue.use(SnakerflowDesigner.default)
new Vue({
el: "#app",
data() {
return {
id: "${id!}",
flowData: {}
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>流程设计器</title>
<script src="/assets/platform/plugins/vue/vue.js"></script>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/element-ui/lib/index.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
<!-- 引入 core 包和对应 css-->
<script src="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/logic-flow.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/style/index.css" />
<script src="/assets/platform/plugins/snaker/SnakerflowDesigner.umd.js"></script>
<style>
#snaker-flow-preview {
height: 100vh;
}
},
methods: {
getDetail() {
$.get("/flow/define/detail", { id: this.id }).then((res) => {
if (res.code === 0) {
this.flowData = res.data.content
.approval-card {
position: absolute;
width: 320px;
background: #ffffff;
border-radius: 8px;
border: 1px solid #d9e3f0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
z-index: 1000;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
transform: translate(10px, 10px);
overflow: hidden;
}
.tab-container {
border-bottom: 1px solid #e8e8e8;
}
.tab-header {
display: flex;
background: #fafafa;
}
.tab-item {
padding: 8px 16px;
cursor: pointer;
border-right: 1px solid #e8e8e8;
font-size: 12px;
color: #666;
background: #fafafa;
transition: all 0.2s;
}
.tab-item:last-child {
border-right: none;
}
.tab-item.active {
background: #fff;
color: #333;
border-bottom: 2px solid #1890ff;
margin-bottom: -1px;
}
.tab-item:hover {
background: #f0f0f0;
color: #333;
}
/* 卡片标题栏(OA风格顶部色条) */
.approval-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #1a73e8;
color: white;
}
.approval-card .card-header h3 {
margin: 0;
font-size: 15px;
font-weight: 500;
letter-spacing: 0.5px;
}
.approval-card .card-header button {
background: rgba(255, 255, 255, 0.2);
border: none;
width: 24px;
height: 24px;
border-radius: 50%;
color: white;
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: all 0.2s;
}
.approval-card .card-header button:hover {
background: rgba(255, 255, 255, 0.3);
}
/* 卡片内容区域 */
.approval-card .card-body {
padding: 16px;
}
/* 信息行样式 */
.approval-card .info-row {
display: flex;
margin-bottom: 12px;
align-items: flex-start;
}
.approval-card .info-label {
width: 80px;
color: #666;
font-size: 13px;
line-height: 1.5;
}
.approval-card .info-value {
flex: 1;
color: #333;
font-size: 13px;
line-height: 1.5;
word-break: break-all;
}
/*!* 分隔线 *!*/
/*.approval-card .divider {*/
/* height: 1px;*/
/* background: #f0f0f0;*/
/* margin: 12px 0;*/
/*}*/
</style>
</head>
<body>
<div id="snaker-flow-preview">
<snaker-flow-designer
ref="designer"
v-model="flowData"
:show-doc="false"
:viewer="true"
node-render-type="html"
:high-light="highLight"
></snaker-flow-designer>
<div
v-if="showApprovalCard"
class="approval-card"
:style="{
left: cardPosition.x+'px',
top: cardPosition.y+'px'
}"
>
<div class="card-header">
<h3>详情</h3>
<button @click="closeCard">×</button>
</div>
<!-- Tab切换 -->
<div class="tab-container" v-if="approvalInfos.length > 1">
<div class="tab-header">
<div
v-for="(item, index) in approvalInfos"
:key="index"
class="tab-item"
:class="{active: activeTabIndex === index}"
@click="activeTabIndex = index"
>
记录{{index + 1}}
</div>
</div>
</div>
<div class="card-body">
<div v-for="(approvalInfo, index) in approvalInfos" :key="index" v-show="activeTabIndex === index">
<div class="info-row">
<span class="info-label">节点类型:</span>
<span class="info-value">{{approvalInfo.displayName}}</span>
</div>
<div class="info-row">
<span class="info-label">审批人:</span>
<span class="info-value" v-if="currentNode.id === 'startTask'">
{{approvalInfo.ext?.initiatorName}}({{approvalInfo.ext?.initiatorAccount}})
</span>
<span class="info-value" v-else>{{approvalInfo.taskFormData.userName}}({{approvalInfo.taskFormData?.loginName}})</span>
</div>
<div class="info-row">
<span class="info-label">审核状态:</span>
<span class="info-value">{{getTaskStateText(approvalInfo.taskState)}}</span>
</div>
<div class="info-row">
<span class="info-label">提交结果:</span>
<span class="info-value">{{getSubmitTypeText(approvalInfo.ext.submitType)}}</span>
</div>
<div class="info-row">
<span class="info-label">提交意见:</span>
<span class="info-value">{{approvalInfo.taskFormData.opinion}}</span>
</div>
<!-- <div class="divider"></div>-->
<div class="info-row">
<span class="info-label">提交时间:</span>
<span class="info-value">{{approvalInfo.finishTime}}</span>
</div>
</div>
</div>
</div>
</div>
</body>
<script>
Vue.use(SnakerflowDesigner.default)
new Vue({
el: "#snaker-flow-preview",
data() {
return {
// 流程定义ID
defineId: "${defineId!}",
// 流程实例ID
instanceId: "${instanceId!}",
// 流程定义数据
flowData: {},
// 高亮数据
highLight: {},
// 历史审批记录
hisApproval: [],
// 展示已审核信息
showApprovalCard: false,
// 审批信息卡片位置
cardPosition: { x: 0, y: 0 },
// 当前节点
currentNode: [],
// 审批信息数组
approvalInfos: [],
// 当前选中的tab索引
activeTabIndex: 0
}
},
computed: {
// approvalInfo() {
// if (this.currentNode) {
// return this.hisApproval.find((item) => item.taskName === this.currentNode.id)
// }
// }
},
methods: {
getDetail() {
$.get("/flow/define/detail", { id: this.defineId }).then((res) => {
if (res.code === 0) {
this.flowData = res.data.content
this.initNodeEvent()
}
})
},
getHighLight() {
$.get("/flow/common/instanceHighLight", { instanceId: this.instanceId }).then((res) => {
if (res.code === 0) {
this.highLight = res.data
}
})
},
getHisApproval() {
$.get("/flow/common/approvalRecord", { instanceId: this.instanceId }).then((res) => {
if (res.code === 0) {
this.hisApproval = res.data
}
})
},
initNodeEvent() {
this.$nextTick(() => {
console.log(this.$refs.designer.lf.graphModel.eventCenter)
this.$refs.designer.lf.graphModel.eventCenter.on("node:click", ({ e, data }) => {
console.log(data)
console.log(e)
// 获取点击位置(考虑页面滚动情况)
const scrollX = window.scrollX || window.pageXOffset
const scrollY = window.scrollY || window.pageYOffset
this.cardPosition = {
x: e.clientX + scrollX,
y: e.clientY + scrollY
}
if (data.type !== "snaker:task") return
const approvalInfos = this.hisApproval.filter((item) => item.taskName === data.id && item.taskState === 20)
if (approvalInfos && approvalInfos.length > 0) {
this.approvalInfos = approvalInfos
this.currentNode = data
this.activeTabIndex = 0
this.showApprovalCard = true
}
})
this.$refs.designer.lf.graphModel.eventCenter.on("blank:click", (args) => {
this.closeCard()
})
})
},
closeCard() {
this.showApprovalCard = false
this.activeTabIndex = 0
},
// 获取任务状态文本
getTaskStateText(taskState) {
const stateMap = {
10: "进行中",
20: "已完成",
30: "已撤回"
}
})
return stateMap[taskState] || taskState
},
// 获取提交结果文本
getSubmitTypeText(submitType) {
const typeMap = {
0: "发起申请",
1: "同意申请",
2: "拒绝申请",
3: "退回上一步",
4: "跳转",
5: "重新提交",
6: "退回发起人",
20: "拒绝申请"
}
return typeMap[submitType] || submitType
}
},
mounted() {
if (this.defineId) {
this.getDetail()
}
if (this.instanceId) {
this.getHighLight()
this.getHisApproval()
}
}
},
mounted() {
if (this.id) {
this.getDetail()
}
}
})
</script>
})
</script>
</html>
@@ -41,7 +41,7 @@ layout("/layouts/platform.html"){
<el-button size="mini" type="primary" @click="onDesign(row)">设计</el-button>
</el-button>
<el-button size="mini" type="primary" @click="onEdit(row)">编辑</el-button>
<!-- v-if="row.isDeployed===0"-->
<!-- v-if="row.isDeployed===0"-->
<el-button type="primary" size="mini" @click="onDeploy(row)">部署
</el-button>
<el-button type="danger" size="mini" @click="onDelete(row)">删除
@@ -53,7 +53,7 @@ layout("/layouts/platform.html"){
</el-card>
</guava>
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="800px">
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="70%">
<el-form :model="formData" ref="formRef" label-width="120px">
<el-form-item label="流程定义名称" prop="displayName"
:rules="{ required: true, message: '请输入流程定义名称', trigger: 'blur' }">
@@ -87,6 +87,9 @@ layout("/layouts/platform.html"){
<el-input maxlength="100" placeholder="图标" v-model="formData.icon"></el-input>
<i :class="formData.icon" v-if="formData.icon"></i>
</el-form-item>
<el-form-item label="说明" prop="description">
<text-editor v-model="formData.description"></text-editor>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -1,80 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>流程设计器</title>
<script src="/assets/platform/plugins/vue/vue.js"></script>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/element-ui/lib/index.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
<!-- 引入 core 包和对应 css-->
<script src="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/logic-flow.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/style/index.css" />
<script src="/assets/platform/plugins/snaker/SnakerflowDesigner.umd.js"></script>
<style>
#app {
height: 100vh;
}
</style>
</head>
<body>
<div id="app">
<snaker-flow-designer ref="designer"
v-model="flowData"
@on-save="handleSave"
:show-doc="false"
node-render-type="html"
:extendPropertyKeys="[
'candidateUsers',
'candidateGroups',
'candidateHandler',
'assigneeText',
]"
></snaker-flow-designer>
</div>
</body>
<script>
Vue.use(SnakerflowDesigner.default)
new Vue({
el: "#app",
data() {
return {
id: "${id!}",
flowData: {}
}
},
methods: {
handleSave(val) {
console.log(val)
const data = {
processDesignId: this.id,
...val.json
}
$.post("/flow/design/updateDesign", { json: JSON.stringify(data) }).then((res) => {
if (res.code === 0) {
window.parent.postMessage("success")
} else {
window.parent.postMessage("error")
}
})
},
getDetail() {
$.get("/flow/design/detail", { id: this.id }).then((res) => {
if (res.code === 0) {
this.flowData = res.data.jsonObject
}
})
}
},
mounted() {
if (this.id) {
this.getDetail()
}
}
})
</script>
</html>
@@ -1,54 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>流程状态</title>
<link rel="stylesheet" href="/assets/platform/plugins/snaker/snaker.css" type="text/css" media="all"/>
<link rel="stylesheet" href="/assets/platform/plugins/snaker/style.css" type="text/css" media="all"/>
</head>
<body>
</div>
<table class="properties_all" align="center" border="1" cellpadding="0" cellspacing="0" style="margin-top: 0px">
<div id="snakerflow"
style="border: 1px solid #d2dde2; margin-top:10px; margin-left:10px; margin-bottom:10px; width:98%;">
</div>
</table>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/snaker/raphael-min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/jquery-ui-1.8.4.custom/js/jquery.min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/jquery-ui-1.8.4.custom/js/jquery-ui.min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/dialog.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.designer.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.model.js" charset="utf-8" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.editors.js" type="text/javascript"></script>
<script>
const orderId = "${orderId!}"
const processId = "${processId!}"
$(function(){
$.post("/snakerflow/define/process/json", { processId, orderId }).then(res => {
if (res.code === 0) {
display(res.data.process, res.data.state)
}
})
function display(process, state) {
/** view*/
$('#snakerflow').snakerflow($.extend(true, {
basePath: "/assets/platform/plugins/snaker/",
// ctxPath: easyAdmin.GetAdminServerUrl(),
// token: easyAdmin.GetTokenQueryString(),
orderId: orderId,
restore: eval("(" + process + ")"),
editable: false
}, eval("(" + state + ")")
));
}
})
</script>
</body>
</html>
@@ -1,6 +1,6 @@
<div id="suggestion-box-view">
<!-- <el-form ref="formRef" label-width="0" label-suffix="" class="flow-task-form">-->
<el-descriptions :column="2" border>
<el-descriptions :column="2" border class="flow-task-form">
<el-descriptions-item label="姓名">{{viewData.userName}}</el-descriptions-item>
<el-descriptions-item label="工号">{{viewData.loginName}}</el-descriptions-item>
<el-descriptions-item label="单位">{{viewData.unitName}}</el-descriptions-item>