1、什么是工作流
1.1、概述
工作流(Workflow),就是“业务过程的部分或整体在计算机应用环境下的自动化”,它主要解决的是“使在多个参与者之间按照某种预定义的规则传递文档、信息或任务的过程自动进行,从而实现某个预期的业务目标,或者促使此目标的实现”。
1.2、常见工作流
采用工作流有以下优点:
- 提高系统的柔性,适应业务流程的变化
- 实现更好的业务过程控制,提高顾客服务质量
- 降低系统开发和维护成本
- 常见工作流:
Activiti、JBPM、OSWorkflow、ActiveBPEL、YAWL等。本文主要采用 Activiti7 工作流开发。
1.3、流程实例
流程实例(ProcessInstance) 代表流程定义的执行实例,一个流程实例包括了所有的运行节点,我们可以利用这个对象来了解当前流程实例的进度等信息。
例如:用户或者程序安装流程定义的内容发起了一个流程,这个就是一个流程实例
1.4、业务管理
流程定义部署在 Activiti 后,我们就可以在系统中通过Activiti 去管理流程的执行,但是如果我们要将我们的流程实例和业务数据关联,这时我们需要使用到 Activiti 中预留的 BusinessKey(业务标识) 来关联
2、Activiti7 介绍
2.1、概念
Alfresco软件在2010年5月17日宣布Activiti业务流程管理(BPM)开源项目的正式启动。Activiti是一个工作流引擎, Activiti可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN2.0进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由activiti进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统开发维护成本。
2.2、基本术语
- ProcessEngine对象: 这是Activiti工作的核心.负责生成流程运行时的各种实例及数据,监控和管理流程的运行
- BPM (业务流程管理):是一种以规范化的构造端到端的卓越业务流程为中心,以持续的提高组织业务绩效为目的的系统化方法
- BPMN(Business Process Model and Notation): 业务流程建模与标注,描述流程的基本符号,包括这些图元如何组合成一个业务流程图(Business Process Diagram)bpmn文件又可以叫做流程定义文件,它需要遵循BPMN语言规。
流对象(process engine):通过它可以获得我们需要的一切activiti服务,一个业务流程图有三个流对象的核心元素:
- 事件:一个事件用圆圈来描述,表示一个业务流程期间发生的东西。事件影响流程的流动,一般有一个原因(触发器)或一个影响(结果),基于它们对流程的影响,有三种事件:开始事件、中间事件、终止事件。
- 活动:用圆角矩形表示,一个流程由一个活动或多个活动组成。
- 条件:条件用菱形表示,用于控制序列流的分支与合并,可以作为选择,包括路径的分支与合,内部的标记会给出控制流的类型。
2.3、系统服务类
- 结构图
- 核心类
ProcessEngine: 流程引擎的抽象,可以通过此类获取需要的所有服务 - 服务类
通过ProcessEngine获取,Activiti将不同生命周期的服务封装在不同Service中,包括定义、部署、运行。通过服务类可获取相关生命周期中的服务信息:
- RepositoryService:Repository Service提供了对repository的存取服,Activiti中每一个不同版本的业务流程的定义都需要使用一些定义文件,部署文件和支持数据(例如BPMN2.0XML文件,表单定义文件,流程定义图像文件等),这些文件都存储在Activiti内建的Repository中。
- RuntimeService:Runtime Service提供了启动流程,查询流程实例,设置获取流程实例变量等功能。此外它还提供了对流程部署,流程定义和流程实例的存取服务。
- TaskService:Task Service提供了对用户Task和Form相关的操作。它提供了运行时任务查询、领取、完成、删除以及变量设置等功能。
- HistoryService:History Service用于获取正在运行或已经完成的流程实例的信息,与Runtime Service获取的流程信息不同,历史信息包含已经持久化存储的永久信息,并已经被针对查询优化。
FormService:使用Form Service可以存取启动和完成任务所需的表单数据并且根据需要来渲染表单。Activiti中的流程和状态Task均可以关联业务相关的数据。IdentityService:Identity Service提供了对Activiti系统中的用户和组的管理功,Activiti中内置了用户以及组管理的功能,必须使用这些用户和组的信息才能获取到相应的Task。- ManagementService:Management Service提供了对Activiti流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用。
2.4、数据库表结构
Activiti7 工作流总共包含25张数据表(Activiti6 包含23张表):
所有的表名默认以“ACT_”开头。表名的第二部分用两个字母表明表的用途:
- ACT_GE (GE) :表示 general 全局通用数据及设置,各种情况都使用的数据。
- ACT_HI (HI) :表示 history 历史数据表,包含着程执行的历史相关数据,如结束的流程实例,变量,任务,等等。
- ACT_RE (RE) :表示 repository 存储,包含的是静态信息,如,流程定义,流程的资源(图片,规则等)。
- ACT_RU (RU) :表示 runtime 运行时,运行时的流程变量,用户任务,变量,职责(job)等运行时的数据。
- Activiti 只存储实例执行期间的运行时数据,当流程实例结束时,将删除这些记录。这就保证了这些运行时的表小且快。
表名 | 解释 |
---|---|
act_evt_log | 流程引擎通用日志表 |
act_ge_bytearray | 二进制表,存储通用的流程资源 |
act_ge_property | 系统存储表,存储整个流程引擎数据,默认存储三条数据 |
act_hi_actinst | 历史节点表 |
act_hi_attachment | 历史附件表 |
act_hi_comment | 历史意见表 |
act_hi_detail | 历史详情表 |
act_hi_identitylink | 历史用户信息表 |
act_hi_procinst | 历史流程实例表 |
act_hi_taskinst | 历史任务实例表 |
act_hi_varinst | 历史变量表 |
act_procdef_info | 流程定义的动态变更信息 |
act_re_deployment | 部署信息表 |
act_re_model | 流程设计实体表 |
act_re_procdef | 流程定义数据表 |
act_ru_deadletter_job | 作业失败表,失败次数>重试次数 |
act_ru_event_subscr | 运行时事件表 |
act_ru_execution | 运行时流程执行实例表 |
act_ru_identitylink | 运行时用户信息表 |
act_ru_integration | 运行时综合表 |
act_ru_job | 作业表 |
act_ru_suspended_job | 作业暂停表 |
act_ru_task | 运行时任务信息表 |
act_ru_timer_job | 运行时定时器表 |
act_ru_variable | 运行时变量表 |
3、集成 IDEA
3.1、部分效果
- 流程图
- 列表页
- 请假申请
- 同意申请
- 驳回申请
- 流程结束
3.2、下载插件
进到 IDEA 的插件页面,搜索插件 Activiti BPMN visualizing,点击安装,应用并完成:
3.3、配置文件
<dependencies>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--spring boot test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- web依赖springmvc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--activiti 工作流的依赖-->
<!-- <dependency>-->
<!-- <groupId>org.activiti</groupId>-->
<!-- <artifactId>activiti-spring-boot-starter-basic</artifactId>-->
<!-- <version>5.1.17</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.0.0.Beta2</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
<exclusion>
<groupId>javax.el</groupId>
<artifactId>el-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-image-generator</artifactId>
<version>7.0.0.Beta2</version>
</dependency>
<!--svg转png-->
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-transcoder</artifactId>
<version>1.13</version>
</dependency>
<dependency>
<groupId>batik</groupId>
<artifactId>batik-util</artifactId>
<version>1.6-1</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-codec</artifactId>
<version>1.7</version>
</dependency>
</dependencies>
server:
port: 9204
spring:
application:
name: llh-activiti # 注册到eureka上面的应用名称
# 数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/activiti?useSSL=false&useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&nullCatalogMeansCurrent=true
username: root
password: root
# 工作流配置
activiti:
database-schema-update: true
check-process-definitions: true
process-definition-location-prefix: classpath:/process/
history-level: full
db-history-used: true
# mybatis plus配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
package com.llh.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security框架配置
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭csrf防护
.csrf().disable()
.headers().frameOptions().disable()
.and()
//定制url访问权限
.authorizeRequests()
//无限登录即可访问
.antMatchers("/**").permitAll()
//需要特定权限
// .antMatchers("/sysUser/**","/sysAuthority/**").hasAnyAuthority("ROLE_ADMIN","ROLE_SA")
//其他接口登录才能访问
// .anyRequest().authenticated()
.and();
}
}
package com.llh.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* User: lilinhan
* DateTime: 2023/10/13 11:53
*/
@Configuration
@MapperScan(value = {"com.llh.mapper"})
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 分页拦截器
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return mybatisPlusInterceptor;
}
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(5);// 核心线程数量
taskExecutor.setMaxPoolSize(10);// 最大线程数量
taskExecutor.setKeepAliveSeconds(2);// 设置时长(秒)
taskExecutor.setQueueCapacity(10);// 设置队列容量
taskExecutor.setThreadNamePrefix("llh-thread:");// 线程名前缀
return taskExecutor;
}
}
package com.llh.utils;
import org.springframework.beans.BeanUtils;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
public class CommonUtils {
/**
* 把指定的复杂对象属性,按照指定的内容,封装到新的map中
*
* @param source 目标对象
* @param ps 需要封装到map中的属性
* @return
*/
public static Map<String, Object> objmap(Object source, String[] ps) {
Map<String, Object> map = new HashMap<>();
if (source == null)
return null;
if (ps == null || ps.length < 1) {
return null;
}
for (String p : ps) {
PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(
source.getClass(), p);
if (sourcePd != null && sourcePd.getReadMethod() != null) {
try {
Method readMethod = sourcePd.getReadMethod();
if (!Modifier.isPublic(readMethod.getDeclaringClass()
.getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source, new Object[0]);
map.put(p, value);
} catch (Exception ex) {
throw new RuntimeException(
"Could not copy properties from source to target",
ex);
}
}
}
return map;
}
}
3.4、创建流程图
- 由于在 yml 配置的路径在 resource 下面的 process 里面:
- 右键 process 文件夹新建:
- 右键生成的 xml 文件:
- 流程图画完之后保存到 process 下,如图所示:
3.5、部分代码
3.5.1、后端功能
package com.llh;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.repository.ProcessDefinitionQuery;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Unit test for simple App.
*/
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class AppTest {
@Autowired
RepositoryService repositoryService;
@Autowired
RuntimeService runtimeService;
@Autowired
TaskService taskService;
// 部署流程
@Test
public void deploy() {
repositoryService.createDeployment()
.name("请假审批")
.addClasspathResource("process/test.bpmn20.xml")
.addClasspathResource("process/diagram.png")
.deploy();
}
// 发起请假:创建一个流程实例
@Test
public void starter(){
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("test");
System.err.println("流程定义id-----"+processInstance.getProcessDefinitionId());
System.err.println("流程实例id-----"+processInstance.getId());
System.err.println("当前活动id-----"+processInstance.getActivityId());
}
// 根据负责人查询任务列表:代办任务
@Test
public void taskList(){
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey("test")
// .taskAssignee("zhangsan")
.list();
System.err.println(list);
list.forEach(task -> {
System.err.println(task.getProcessInstanceId());
System.err.println(task.getId());
System.err.println(task.getAssignee());
System.err.println(task.getName());
});
}
// 审批任务,处理任务
@Test
public void doTask(){
// 处理任务前,先根据负责人查询出ta当前的代办任务
Task task = taskService.createTaskQuery()
.processDefinitionKey("test")
.taskAssignee("xiaomi")
.singleResult();
// 处理代办任务,到下个节点
// 传入负责人,appro==true|false
Map<String,Object> map = new HashMap<>();
// map.put("appro",true);
taskService.complete(task.getId(),map);
System.err.println("任务已完成");
}
// 查询流程定义信息
@Test
public void taskInfo(){
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
List<ProcessDefinition> test = processDefinitionQuery.processDefinitionKey("test")
.orderByProcessDefinitionVersion()
.desc()
.list();
test.forEach(t->{
System.err.println("流程定义id-----"+t.getId());
System.err.println("流程定义name-----"+t.getName());
System.err.println("流程定义key-----"+t.getKey());
System.err.println("流程定义version-----"+t.getVersion());
System.err.println("流程部署ID-----"+t.getDeploymentId());
});
}
// 删除流程
@Test
public void delTask(){
repositoryService.deleteDeployment("",true);
}
}
package com.llh.controller;
import cn.hutool.core.date.DateUtil;
import com.llh.domain.Exam;
import com.llh.domain.UserTask;
import com.llh.service.ExamService;
import com.llh.utils.CommonUtils;
import lombok.extern.slf4j.Slf4j;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* User: lilinhan
* DateTime: 2023/11/21 11:41
*/
@CrossOrigin
@RestController
@RequestMapping("/exam")
@Slf4j
public class ExamController {
@Autowired
RuntimeService runtimeService;
@Autowired
HistoryService historyService;
@Autowired
ExamService examService;
@Autowired
TaskService taskService;
/**
* 处理代办任务,同意和驳回共用这个方法
* @param taskId 任务id
* @param appro 跟流程图网关的条件表达式名称一致,具体操作 true|false
* @return map集合
*/
@RequestMapping("/doTask")
public Map<String,Object> doTask(String taskId,String appro){
Map<String, Object> map = new HashMap<>();
Map<String, Object> taskMap = new HashMap<>();
taskMap.put("appro",appro);
try {
// 处理任务
taskService.complete(taskId,taskMap);
map.put("code",1001);
// 查询代办任务
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey("test")
.list();
// 遍历集合
list.forEach(task -> {
// 将当前负责人、流程名称返回给前端
map.put("taskName",task.getName());
map.put("assignee",task.getAssignee());
});
}catch (Exception e){
map.put("code",1005);
map.put("msg",e.getMessage());
return map;
}
return map;
}
// 代办任务列表
@RequestMapping("/taskList")
public List<UserTask> taskList(Exam exam) {
List<UserTask> list = new ArrayList<>();
// 获取代办任务
List<Task> tasks = taskService.createTaskQuery()
.processDefinitionKey("test")
.taskAssignee(exam.getName())
.list();
// 数组中写入要获取的数据
String[] ps = {"id", "name", "assignee", "processDefinitionId", "processInstanceId"};
for (Task task : tasks) {
// 获取绑定业务表的主键
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult();
String businessKey = historicProcessInstance.getBusinessKey();
// 根据关联id查询业务表
Exam examDB = examService.getById(businessKey);
// 计算请假天数
long days = DateUtil.betweenDay(examDB.getStartDate(), examDB.getEndDate(), true);
UserTask userTask = new UserTask();
// 参数:source--要转换的对象,ps--需要封装到map中的属性
Map<String, Object> objmap = CommonUtils.objmap(task, ps);
userTask.setTask(objmap);
userTask.setServiceKey(businessKey);
userTask.setRemark(examDB.getRemark());
userTask.setDays(days);
userTask.setHistoryId(historicProcessInstance.getId());
userTask.setExamName(examDB.getName());
list.add(userTask);
}
return list;
}
// 请假申请
@RequestMapping("/start")
public Map<String, Object> start(Exam exam) {
Map<String, Object> map = new HashMap<>();
boolean save = examService.save(exam);
String businessKey = exam.getId() + "";
if (save) {
// 创建一个流程,将业务表的id与历史状态表的businessKey绑定
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("test", businessKey);
log.info("实例id:" + processInstance.getProcessDefinitionId());
if (processInstance != null) {
map.put("code", 1001);
map.put("msg", "申请成功");
return map;
}
}
map.put("code", 1005);
map.put("msg", "申请失败");
return map;
}
}
3.5.2、前端页面
<script>
import qs from "qs";
export default {
data(){
return{
dialogFormVisible:false,
form:{},
formLabelWidth:'150px',
value1:'',
tableData:[],
formInline:{
name:'zhangsan'
},
}
},
methods:{
success(row){
this.axios.get("http://localhost:9204/exam/doTask?taskId="+row.task.id+"&appro=true").then(res=>{
if(res.data.code==1001){
var assignee = res.data.assignee;
var taskName = res.data.taskName;
if(assignee!=undefined && taskName!=undefined){
this.$message.success("已同意!当前需要审批的是:{负责人:"+assignee+",流程名称:"+taskName+"}");
// 通过当前负责人查询代办任务
this.formInline.name = assignee;
this.onSubmit();
}else {
this.$message.warning("当前没有任务咯!")
this.onSubmit();
}
}else {
this.$message.error(res.data.msg);
}
})
},
redound(row){
this.axios.get("http://localhost:9204/exam/doTask?taskId="+row.task.id+"&appro=false").then(res=>{
if(res.data.code==1001){
var assignee = res.data.assignee;
var taskName = res.data.taskName;
if(assignee!=undefined && taskName!=undefined){
this.$message.warning("已驳回!当前需要审批的是:{负责人:"+assignee+",流程名称:"+taskName+"}");
// 通过当前负责人查询代办任务
this.formInline.name = assignee;
this.onSubmit();
}else {
this.$message.warning("当前没有任务咯!")
this.onSubmit();
}
}else {
this.$message.error(res.data.msg);
}
})
},
onSubmit(){
this.axios.post("http://localhost:9204/exam/taskList",qs.stringify(this.formInline)).then(res=>{
this.tableData = res.data
})
},
toExam(){
this.form.startDate = this.value1[0]
this.form.endDate = this.value1[1]
this.axios.post("http://localhost:9204/exam/start",qs.stringify(this.form)).then(res=>{
if(res.data.code==1001){
this.$message.success(res.data.msg);
this.dialogFormVisible = false
this.onSubmit();
}else {
this.$message.error(res.data.msg);
}
})
}
},
created() {
}
}
</script>
<template>
<div>
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="负责人">
<el-select v-model="formInline.name" placeholder="负责人" clearable="clearable">
<el-option label="张三" value="zhangsan"></el-option>
<el-option label="李四" value="lisi"></el-option>
<el-option label="小米" value="xiaomi"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
<el-button type="primary" @click="dialogFormVisible = true">请假申请</el-button>
</el-form-item>
</el-form>
<el-dialog title="请假申请" :visible.sync="dialogFormVisible">
<el-form :model="form">
<el-form-item label="申请人姓名" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="请假日期" :label-width="formLabelWidth">
<div class="block">
<el-date-picker
v-model="value1"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期">
</el-date-picker>
</div>
</el-form-item>
<el-form-item label="请假说明" :label-width="formLabelWidth">
<el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="toExam">确 定</el-button>
</div>
</el-dialog>
<el-table
:data="tableData"
border
style="width: 100%">
<el-table-column prop="task.id" label="任务实例id" width="180"></el-table-column>
<el-table-column prop="task.name" label="任务名称" width="180"></el-table-column>
<el-table-column prop="task.assignee" label="负责人" width="180"></el-table-column>
<el-table-column prop="remark" label="请假原因" width="180"></el-table-column>
<el-table-column prop="days" label="请假天数" width="180"></el-table-column>
<el-table-column prop="examName" label="申请人" width="180"></el-table-column>
<el-table-column prop="historyId" label="历史流程实例id" width="180"></el-table-column>
<el-table-column prop="serviceKey" label="绑定的业务id" width="180"></el-table-column>
<el-table-column label="操作" width="180">
<template v-slot="scope">
<el-button type="primary" size="mini" @click="success(scope.row)">同意</el-button>
<el-button type="warning" size="mini" @click="redound(scope.row)">驳回</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<style scoped>
</style>
106 comments
这篇文章不错!
这篇文章不错!
这篇文章如同一幅色彩斑斓的画卷,每一笔都充满了独特的创意。
文章深入浅出,既有深度思考,又不乏广度覆盖,令人叹为观止。
创新略显不足,可尝试引入多元视角。
建议标注关键步骤的注意事项。
?金句式评语?
?幽默类评语?
案例丰富且贴合主题,论证逻辑环环相扣。
文章深入浅出,既有深度思考,又不乏广度覆盖,令人叹为观止。
作者以简洁明了的语言,传达了深刻的思想和情感。
字里行间流露出真挚的情感,让人感同身受,共鸣不已。
作者以简洁明了的语言,传达了深刻的思想和情感。
理论深度可再挖掘,以提升文章厚重感。
这篇文章如同一幅色彩斑斓的画卷,每一笔都充满了独特的创意。
这篇文章提供了宝贵的经验和见解,对读者有很大的启发和帮助。
独特的构思和新颖的观点,让这篇文章在众多作品中脱颖而出。
文章中的实用建议和操作指南,让读者受益匪浅,值得珍藏。
内容的丰富性和深度让人仿佛置身于知识的海洋,受益匪浅。
情感浓度过高可适当留白,以达平衡。