Maison >Java >javaDidacticiel >Comment utiliser SpringBoot+Vue+Flowable pour simuler le processus d'approbation des congés
Avant le début officiel, permettez-moi d'abord de vous montrer l'effet que nous allons réaliser aujourd'hui.
Par souci de simplicité, je n'ai pas introduit ici les concepts d'utilisateurs, de rôles, etc. Tous les lieux impliquant des utilisateurs sont saisis manuellement dans les articles suivants, je continuerai à combiner Spring Security pour vous montrer la situation après l'avoir introduit. utilisateurs.
Jetons d'abord un coup d'œil à la page de demande de congé :
Les employés peuvent saisir leur nom, le nombre de jours de congé et le motif du congé sur cette page, puis cliquer sur le bouton pour soumettre une demande de congé.
Lorsqu'un employé soumet une demande de congé, la demande de congé sera traitée par le manager par défaut. Une fois que le manager se connecte, il peut voir la demande soumise par l'employé :
Le manager peut choisir d'approuver ou rejeter à ce moment. Qu'il s'agisse d'une approbation ou d'un rejet, les employés peuvent être informés par SMS ou par e-mail.
Pour les employés, ils peuvent également vérifier le statut final de leur processus de congé sur une seule page :
Je montrerai directement à mes amis comment utiliser flowable dans Spring Boot.
Nous créons d'abord un projet Spring Boot. Lors de la création, introduisez simplement les dépendances des pilotes Web et MySQL. Une fois le projet créé avec succès, introduisez les dépendances fluides. Le fichier de dépendances final est le suivant :
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>6.7.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
Une fois le projet réussi. créé, nous devons d'abord configurer les informations de connexion à la base de données dans application.properties comme suit :
spring.datasource.username=root spring.datasource.password=123 spring.datasource.url=jdbc:mysql:///flowable02?serverTimezone=Asia/Shanghai&useSSL=false&nullCatalogMeansCurrent=true
Une fois la configuration terminée, les tables pertinentes et les données requises seront automatiquement créées lors de la première exécution du projet Spring Boot.
Dans le même temps, le projet Spring Boot créera et exposera également automatiquement des beans tels que ProcessEngine, CmmnEngine, DmnEngine, FormEngine, ContentEngine et IdmEngine dans Flowable.
Tous les services Flowable peuvent être appelés Spring Beans. Par exemple, des services tels que RuntimeService, TaskService, HistoryService, etc. peuvent être directement injectés et utilisés lorsque nous en avons besoin.
En même temps :
Toute définition de processus BPMN 2.0 dans le répertoire resources/processes sera automatiquement déployée, donc dans le projet Spring Boot, il suffit de placer nos fichiers de processus au bon endroit, et le reste est-ce que cela se fera automatiquement.
Tous les cas CMMN 1.1 dans le répertoire des cas seront automatiquement déployés.
Toute définition de formulaire dans le répertoire des formulaires sera automatiquement déployée.
L'exemple d'aujourd'hui est relativement simple, il s'agit d'un processus de demande de congé. Je ne parlerai pas de dessiner un organigramme avec mes amis pour le moment. Utilisons directement une demande de congé toute faite. organigramme du site officiel :
Analysons d'abord brièvement cette image :
Le cercle le plus à gauche est appelé l'événement de départ (start event), qui représente le point de départ d'une instance de processus.
Une fois qu'un processus est démarré, il atteint d'abord le premier rectangle avec une icône d'utilisateur. Ce rectangle est appelé une tâche utilisateur. Dans cette tâche utilisateur, le responsable peut choisir d'approuver ou de rejeter.
La prochaine étape de UserTask est un diamant, c'est ce qu'on appelle la passerelle exclusive, qui acheminera la demande vers différents endroits.
Parlons d'abord de l'approbation. Si le manager choisit l'approbation dans le premier rectangle, il entrera un rectangle avec une icône d'engrenage. Dans ce rectangle, nous pourrons faire quelques choses supplémentaires, puis appeler une tâche utilisateur et enfin terminer la tâche. tout le processus.
Si le gestionnaire choisit de refuser, il entrera dans le rectangle de courrier électronique ci-dessous, où nous pourrons envoyer une notification à l'employé pour l'informer que la demande de congé n'a pas été approuvée.
Lorsque le système atteint le cercle le plus à droite, cela signifie que l'exécution de ce processus est terminée.
Le fichier XML correspondant à cet organigramme se trouve dans src/main/resources/processes/holiday-request.bpmn20.xml. Son contenu est le suivant :
<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef"> <process id="holidayRequest" name="Holiday Request" isExecutable="true"> <startEvent id="startEvent"/> <sequenceFlow sourceRef="startEvent" targetRef="approveTask"/> <userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/> <sequenceFlow sourceRef="approveTask" targetRef="decision"/> <exclusiveGateway id="decision"/> <sequenceFlow sourceRef="decision" targetRef="externalSystemCall"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[ ${approved} ]]> </conditionExpression> </sequenceFlow> <sequenceFlow sourceRef="decision" targetRef="rejectLeave"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[ ${!approved} ]]> </conditionExpression> </sequenceFlow> <serviceTask id="externalSystemCall" name="Enter holidays in external system" flowable:class="org.javaboy.flowable02.flowable.Approve"/> <sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/> <userTask id="holidayApprovedTask" flowable:assignee="${employee}" name="Holiday approved"/> <sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/> <serviceTask id="rejectLeave" name="Send out rejection email" flowable:class="org.javaboy.flowable02.flowable.Reject"/> <sequenceFlow sourceRef="rejectLeave" targetRef="rejectEnd"/> <endEvent id="approveEnd"/> <endEvent id="rejectEnd"/> </process> </definitions>
Beaucoup d'amis qui souhaitent apprendre les moteurs de processus le seront. convaincu par ce fichier XML Retraite, mais ! ! !
Si vous êtes prêt à vous calmer et à lire attentivement ce fichier XML, vous constaterez que le moteur de processus est si simple !
Jetons un coup d'œil à chaque nœud ici un par un :
processus : Cela signifie un processus. Par exemple, la demande de congé partagée avec vous dans cet article est un processus.
startEvent : Ceci indique le début du processus, il s'agit d'un événement de démarrage.
userTask : Il s'agit d'un nœud de processus spécifique. L'attribut flowable:candidateGroups indique quel groupe d'utilisateurs doit gérer ce nœud.
sequenceFlow:这就是连接各个流程节点之间的线条,这个里边一般有两个属性,sourceRef 和 targetRef,前者表示线条的起点,后者表示线条的终点。
exclusiveGateway:表示一个排他性网关,也就是那个菱形选择框。
从排他性网关出来的线条有两个,大家注意看上面的代码,这两个线条中都涉及到一个变量 approved,如果这个变量为 true,则 targeRef 就是 externalSystemCall;如果这个变量为 false,则 targetRef 就是 rejectLeave。
serviceTask:这就是我们定义的一个具体的外部服务,如果在整个流程执行的过程中,你有一些需要自己完成的事情,那么可以通过 serviceTask 来实现,这个节点会有一个 flowable:class 属性,这个属性的值就是一个自定义类。
另外,上文中部分节点中还涉及到变量 ${},这个变量是在流程执行的过程中传入进来的。
总而言之,只要小伙伴们静下心来认真阅读一下上面的 XML,你会发现 So Easy!
好了,接下来我们就来看一个具体的请假申请。由于请假流程只要放对位置,就会自动加载,所以我们并不需要手动加载请假流程,直接开始一个请假申请流程即可。
首先我们需要一个实体类来接受前端传来的请假参数:用户名、请假天数以及请假理由:
public class AskForLeaveVO { private String name; private Integer days; private String reason; // 省略 getter/setter }
再拿出祖传的 RespBean,以便响应数据方便一些:
public class RespBean { private Integer status; private String msg; private Object data; public static RespBean ok(String msg, Object data) { return new RespBean(200, msg, data); } public static RespBean ok(String msg) { return new RespBean(200, msg, null); } public static RespBean error(String msg, Object data) { return new RespBean(500, msg, data); } public static RespBean error(String msg) { return new RespBean(500, msg, null); } private RespBean() { } private RespBean(Integer status, String msg, Object data) { this.status = status; this.msg = msg; this.data = data; } // 省略 getter/setter }
接下来我们提供一个处理请假申请的接口:
@RestController public class AskForLeaveController { @Autowired AskForLeaveService askForLeaveService; @PostMapping("/ask_for_leave") public RespBean askForLeave(@RequestBody AskForLeaveVO askForLeaveVO) { return askForLeaveService.askForLeave(askForLeaveVO); } }
核心逻辑在 AskForLeaveService 中,来继续看:
@Service public class AskForLeaveService { @Autowired RuntimeService runtimeService; @Transactional public RespBean askForLeave(AskForLeaveVO askForLeaveVO) { Map<String, Object> variables = new HashMap<>(); variables.put("name", askForLeaveVO.getName()); variables.put("days", askForLeaveVO.getDays()); variables.put("reason", askForLeaveVO.getReason()); try { runtimeService.startProcessInstanceByKey("holidayRequest", askForLeaveVO.getName(), variables); return RespBean.ok("已提交请假申请"); } catch (Exception e) { e.printStackTrace(); } return RespBean.error("提交申请失败"); } }
小伙伴们看一下,在提交请假申请的时候,分别传入了 name、days 以及 reason 三个参数,我们将这三个参数放入到一个 Map 中,然后通过 RuntimeService#startProcessInstanceByKey 方法来开启一个流程,开启流程的时候一共传入了三个参数:
第一个参数表示流程引擎的名字,这就是我们刚才在流程的 XML 文件中定义的名字。
第二个参数表示当前这个流程的 key,我用了申请人的名字,将来我们可以通过申请人的名字查询这个人曾经提交的所有申请流程。
第三个参数就是我们的变量了。
好了,这服务端就写好了。
接下来我们来开发前端页面。
前端我使用 Vue+ElementUI+Axios,咱们这个案例比较简单,就没有必要搭建单页面了,直接用普通的 HTML 就行了。另外,Vue 我是用了 Vue3:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <!-- Import style --> <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" /> <script src="https://unpkg.com/vue@3"></script> <!-- Import component library --> <script src="//unpkg.com/element-plus"></script> </head> <body> <div id="app"> <h2>开始一个请假流程</h2> <table> <tr> <td>请输入姓名:</td> <td> <el-input type="text" v-model="afl.name"/> </td> </tr> <tr> <td>请输入请假天数:</td> <td> <el-input type="text" v-model="afl.days"/> </td> </tr> <tr> <td>请输入请假理由:</td> <td> <el-input type="text" v-model="afl.reason"/> </td> </tr> </table> <el-button type="primary" @click="submit">提交请假申请</el-button> </div> <script> Vue.createApp( { data() { return { afl: { name: 'javaboy', days: 3, reason: '休息一下' } } }, methods: { submit() { let _this = this; axios.post('/ask_for_leave', this.afl) .then(function (response) { if (response.data.status == 200) { //提交成功 _this.$message.success(response.data.msg); } else { //提交失败 _this.$message.error(response.data.msg); } }) .catch(function (error) { console.log(error); }); } } } ).use(ElementPlus).mount('#app') </script> </body> </html>
这个页面有几个需要注意的点:
通过 Vue.createApp 来创建一个 Vue 实例,这跟以前 Vue2 中直接 new 一个 Vue 实例不一样。
使用 use 方法来配置 ElementPlus 插件,这一点与 Vue2 不同。在 Vue2 中,使用 ElementUI 只需要在HTML页面中进行简单的引用即可,不需要额外的步骤。
剩下的东西就比较简单了,上面先引入 Vue3、Axios 以及 ElementPlus,然后三个输入框,点击按钮提交请求,参数就是三个输入框中的数据,提交成功或者失败,分别弹个框出来提示一下就行了。
好啦,这就写好了。
然而,提交完成后,没有一个直观的展示,虽然前端提示说提交成功了,但是究竟成功没,还得眼见为实。
好了,接下来我们要做的事情就是把用户提交的流程展示出来。
按理说,比如经理登录成功之后,系统页面就自动展示出来经理需要审批的流程,但是我们当前这个例子为了简单,就没有登录这个操作了,需要需要用户将来在网页上选一下自己的身份,接下来就会展示出这个身份所对应的需要操作的流程。
我们来看任务接口:
@GetMapping("/list") public RespBean leaveList(String identity) { return askForLeaveService.leaveList(identity); }
这个请求参数 identity 就表示当前用户的身份(本来应该是登录后自动获取,但是因为我们目前没有登录,所以这个参数是由前端传递过来)。来继续看 askForLeaveService 中的方法:
@Service public class AskForLeaveService { @Autowired TaskService taskService; public RespBean leaveList(String identity) { List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup(identity).list(); List<Map<String, Object>> list = new ArrayList<>(); for (int i = 0; i < tasks.size(); i++) { Task task = tasks.get(i); Map<String, Object> variables = taskService.getVariables(task.getId()); variables.put("id", task.getId()); list.add(variables); } return RespBean.ok("加载成功", list); } }
Task 就是流程中要做的每一件事情,我们首先通过 TaskService,查询出来这个用户需要处理的任务,例如前端前传来的是 managers,那么这里就是查询所有需要由 managers 用户组处理的任务。
这段代码要结合流程图一起来理解,小伙伴们回顾下我们流程图中有如下一句:
<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>
这意思就是说这个 userTask 是由 managers 这个组中的用户来处理,所以上面 Java 代码中的查询就是查询 managers 这个组中的用户需要审批的任务。
我们将所有需要审批的任务查询出来后,通过 taskId 可以进一步查询到这个任务中当时传入的各种变量,我们将这些数据封装成一个对象,并最终返回到前端。
最后,我们再来看下前端页面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <!-- Import style --> <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" /> <script src="https://unpkg.com/vue@3"></script> <!-- Import component library --> <script src="//unpkg.com/element-plus"></script> </head> <body> <div id="app"> <div> <div>请选择你的身份:</div> <div> <el-select name="" id="" v-model="identity" @change="initTasks"> <el-option :value="iden" v-for="(iden,index) in identities" :key="index" :label="iden"></el-option> </el-select> <el-button type="primary" @click="initTasks">刷新一下</el-button> </div> </div> <el-table border strip :data="tasks"> <el-table-column prop="name" label="姓名"></el-table-column> <el-table-column prop="days" label="请假天数"></el-table-column> <el-table-column prop="reason" label="请假原因"></el-table-column> <el-table-column lable="操作"> <template #default="scope"> <el-button type="primary" @click="approveOrReject(scope.row.id,true,scope.row.name)">批准</el-button> <el-button type="danger" @click="approveOrReject(scope.row.id,false,scope.row.name)">拒绝</el-button> </template> </el-table-column> </el-table> </div> <script> Vue.createApp( { data() { return { tasks: [], identities: [ 'managers' ], identity: '' } }, methods: { initTasks() { let _this = this; axios.get('/list?identity=' + this.identity) .then(function (response) { _this.tasks = response.data.data; }) .catch(function (error) { console.log(error); }); } } } ).use(ElementPlus).mount('#app') </script> </body> </html>
我们先选择一个用户身份,具体说就是在下拉菜单中选择。在完成选择后,调用 initTasks 方法,发起网络请求并渲染其结果。
最终效果如下:
当然用户也可以点击刷新按钮,刷新列表。
这样,当第五小节中,员工提交了一个请假审批之后,我们在这个列表中就可以查看到员工提交的请假审批了(在流程图中,我们直接设置了用户的请假审批固定提交给 managers,在后续的文章中,松哥会教大家如何把这个提交的目标用户变成一个动态的)。
接下来经理就可以选择批准或者是拒绝这请假了。
首先我们封装一个实体类用来接受前端传来的请求:
public class ApproveRejectVO { private String taskId; private Boolean approve; private String name; // 省略 getter/setter }
参数都好理解,approve 为 true 表示申请通过,false 表示申请被拒绝。
接下来我们来看接口:
@PostMapping("/handler") public RespBean askForLeaveHandler(@RequestBody ApproveRejectVO approveRejectVO) { return askForLeaveService.askForLeaveHandler(approveRejectVO); }
看具体的 askForLeaveHandler 方法:
@Service public class AskForLeaveService { @Autowired TaskService taskService; public RespBean askForLeaveHandler(ApproveRejectVO approveRejectVO) { try { boolean approved = approveRejectVO.getApprove(); Map<String, Object> variables = new HashMap<String, Object>(); variables.put("approved", approved); variables.put("employee", approveRejectVO.getName()); Task task = taskService.createTaskQuery().taskId(approveRejectVO.getTaskId()).singleResult(); taskService.complete(task.getId(), variables); if (approved) { //如果是同意,还需要继续走一步 Task t = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).singleResult(); taskService.complete(t.getId()); } return RespBean.ok("操作成功"); } catch (Exception e) { e.printStackTrace(); } return RespBean.error("操作失败"); } }
大家注意这个审批流程:
审批时需要两个参数,approved 和 employee,approved 为 true,就会自动进入到审批通过的流程中,approved 为 false 则会自动进入到拒绝流程中。
通过 taskService,结合 taskId,从流程中查询出对应的 task,然后调用 taskService.complete 方法传入 taskId 和 变量,以使流程向下走。
小伙伴们再回顾一下我们前面的流程图,如果请求被批准备了,那么在执行完自定义的 Approve 逻辑后,就会进入到 Holiday approved 这个 userTask 中,注意此时并不会继续向下走了(还差一步到结束事件);如果是请求拒绝,则在执行完自定义的 Reject 逻辑后,就进入到结束事件了,这个流程就结束了。
针对第三条,所以代码中我们还需要额外再加一步,如果是 approved 为 true,那么就再从当前流程中查询出来需要执行的 task,再调用 complete 继续走一步,此时就到了结束事件了,这个流程就结束了。注意这次的查询是根据当前流程的 ID 查询的,一个流程就是一条线,这条线上有很多 Task,我们可以从 Task 中获取到流程的 ID。
好啦,接口就写好了。
当然,这里还涉及到两个自定义的逻辑,就是批准或者拒绝之后的自定义逻辑,这个其实很好写,如下:
public class Approve implements JavaDelegate { @Override public void execute(DelegateExecution execution) { System.out.println("申请通过:"+execution.getVariables()); } }
我们自定义类实现 JavaDelegate 接口即可,然后我们在 execute 方法中做自己想要做的事情即可,execution 中有这个流程中的所有变量。我们可以在这里发邮件、发短信等等。Reject 的定义方式也是类似的。一旦完成这些自定义类的编写,它们就可以被配置到流程图中(请参考上文提供的流程图)。
最后再来看看前端提交方法就简单了(页面源码上文已经列出):
approveOrReject(taskId, approve,name) { let _this = this; axios.post('/handler', {taskId: taskId, approve: approve,name:name}) .then(function (response) { _this.initTasks(); }) .catch(function (error) { console.log(error); }); }
这就一个普通的 Ajax 请求,批准的话第二个参数就为 true,拒绝的话第二个参数就为 false。
最后,每个用户都可以查看自己曾经的申请记录。本来这个登录之后就可以展示了,但是因为我们没有登录,所以这里也是需要手动输入查询的用户,然后根据用户名查询这个用户的历史记录,我们先来看查询接口:
@GetMapping("/search") public RespBean searchResult(String name) { return askForLeaveService.searchResult(name); }
参数就是要查询的用户名。具体的查询流程如下:
public RespBean searchResult(String name) { List<HistoryInfo> historyInfos = new ArrayList<>(); List<HistoricProcessInstance> historicProcessInstances = historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(name).finished().orderByProcessInstanceEndTime().desc().list(); for (HistoricProcessInstance historicProcessInstance : historicProcessInstances) { HistoryInfo historyInfo = new HistoryInfo(); Date startTime = historicProcessInstance.getStartTime(); Date endTime = historicProcessInstance.getEndTime(); List<HistoricVariableInstance> historicVariableInstances = historyService.createHistoricVariableInstanceQuery() .processInstanceId(historicProcessInstance.getId()) .list(); for (HistoricVariableInstance historicVariableInstance : historicVariableInstances) { String variableName = historicVariableInstance.getVariableName(); Object value = historicVariableInstance.getValue(); if ("reason".equals(variableName)) { historyInfo.setReason((String) value); } else if ("days".equals(variableName)) { historyInfo.setDays(Integer.parseInt(value.toString())); } else if ("approved".equals(variableName)) { historyInfo.setStatus((Boolean) value); } else if ("name".equals(variableName)) { historyInfo.setName((String) value); } } historyInfo.setStartTime(startTime); historyInfo.setEndTime(endTime); historyInfos.add(historyInfo); } return RespBean.ok("ok", historyInfos); }
我们当时在开启流程的时候,传入了一个参数 key,这里就是再次通过这个 key,也就是用户名去查询历史流程,查询的时候还加上了 finished 方法,这个表示要查询的流程必须是执行完毕的流程,对于没有执行完毕的流程,这里不查询,查完之后,按照流程最后的处理时间进行排序。
遍历第一步的查询结果,从 HistoricProcessInstance 中提取出每一个流程的详细信息,并存入到集合中,并最终返回。
这里涉及到两个历史数据查询,createHistoricProcessInstanceQuery 用来查询历史流程,而 createHistoricVariableInstanceQuery 则主要是用来查询流程变量的。
最后,前端通过表格展示这个数据即可:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <!-- Import style --> <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" /> <script src="https://unpkg.com/vue@3"></script> <!-- Import component library --> <script src="//unpkg.com/element-plus"></script> </head> <body> <div id="app"> <div > <el-input v-model="name" placeholder="请输入用户名"></el-input> <el-button type="primary" @click="search">查询</el-button> </div> <div> <el-table border strip :data="historyInfos"> <el-table-column prop="name" label="姓名"></el-table-column> <el-table-column prop="startTime" label="提交时间"></el-table-column> <el-table-column prop="endTime" label="审批时间"></el-table-column> <el-table-column prop="reason" label="事由"></el-table-column> <el-table-column prop="days" label="天数"></el-table-column> <el-table-column label="状态"> <template #default="scope"> <el-tag type="success" v-if="scope.row.status">已通过</el-tag> <el-tag type="danger" v-else>已拒绝</el-tag> </template> </el-table-column> </el-table> </div> </div> <script> Vue.createApp( { data() { return { historyInfos: [], name: 'zhangsan' } }, methods: { search() { let _this = this; axios.get('/search?name=' + this.name) .then(function (response) { if (response.data.status == 200) { _this.historyInfos=response.data.data; } else { _this.$message.error(response.data.msg); } }) .catch(function (error) { console.log(error); }); } } ).use(ElementPlus).mount('#app') </script> </body> </html>
这个都是一些常规操作,我就不多说了,最终展示效果如下:
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!