一个完整的流程表单流转
1.写在前面
一个完整的流程表单审批(起表单-->各环节审批-->回退-->重新审批-->完成),前端由Vue2+js+Element UI升级为Vue3+ts+Element Plus,后端流程框架使用Flowable,项目参考了ruoyi-vue-pro(https://gitee.com/zhijiantianya/ruoyi-vue-pro)项目。
2.视频演示
OA系统自定义表单审批流转流程演示
3.表单
3.1表单的设计
依据业务需求,完全自定义表单,可以依据流程节点设置表单中每个属性的读写,实现原理是读取审批节点的编码,依据编码控制每个属性的读写。
<el-tree-select:disabled="!required"v-model="form.deptCode":data="deptList":props="defaultPropsForData"check-strictlynode-key="id"placeholder="请选择集团成员单位"/>/*** 根据不同参数控制表单属性的可读可写* @param pattern*/
const changeRequiredByPattern = (pattern: string) => {if (pattern === 'create') {//说明增加required.value = true} else if (pattern === 'update' || pattern === 'starter') {//修改required.value = truegetBid()} else {//只读required.value = falsegetBid()}
}
3.2.表单与流程关联
通过设计流程时定义的流程编码,在创建表单的后端服务调用中,实现表单与流程的绑定。
/*** 投标对应的流程定义 KEY*/public static final String PROCESS_KEY = "bidApproval";//发起BPM流程Map<String, Object> processInstanceVariables = new HashMap<>();processInstanceVariables.put("deptId", bidDO.getDeptId());processInstanceVariables.put("bidMoney", bidDO.getBidMoney());String processInstanceId = processInstanceApi.createProcessInstance(getLoginUserId(),new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(PROCESS_KEY).setVariables(processInstanceVariables).setBusinessKey(bidDO.getProjectName()));
3.3.表单与前端流程实例的关联
在通过待办打开需要审批的表单时,不同的流程实例如何对应不同的表单,并在页面展示具体的表单数据了?答案是通过定义流程时填写的表单组件名称,利用Vue的<component>元组件来实现。
<componentref="formDetailRef"v-if="processInstance.id !== undefined":is="processInstance.processDefinition.formComponentName":processInstanceId="processInstance.id":pattern="runningTasks.length > 0 ? runningTasks[0].definitionKey : 'readOnly'"@success="getDetail"/>
3.4.表单的保存
表单的保存分为提交时的保存与不提交的保存。不提交的保存用于修改数据但流程不需要提交到下一节点审批的情况,方便保存数据进入待办里面进行后续的修改。提交的时,流程会自动调用保存接口,先进行业务数据的保存,然后再进行流程的提交。
<el-button color="#626aef" @click="handleSave"><Icon icon="ep:coin" />保存</el-button><el-button type="success" @click="handleApproval(item)"><Icon icon="ep:select" />提交</el-button>/** 处理保存表单的操作 只更新表单数据 不提交流程任务 */
const formDetailRef = ref()
const handleSave = () => {formDetailRef.value.submitForm()
}/** 处理审批通过的操作 */
const approvalRef = ref()
const handleApproval = async (item) => {approvalRef.value.open(item)
}
@Override@Transactional(rollbackFor = Exception.class)public Long updateBid(BidUpdateReqVO updateReqVO) {BidDO bidDO = BidConvert.INSTANCE.convert3(updateReqVO);//checkDeptIsMateTendererDept(deptRespDTO, tendererDeptRespDTO);bidDO.setDeptId(findDeptByCode(updateReqVO.getDeptCode()).getId());bidDO.setTendererId(findDeptByName(updateReqVO.getTendererName()).getId());fileApi.deleteFile(bidMapper.selectById(bidDO.getId()).getFiles(), bidDO.getFiles());bidMapper.updateById(bidDO);return bidDO.getId();}@Override@Transactional(rollbackFor = Exception.class)public void approveProcessTask(BidUpdateReqVO bidUpdateReqVO, BpmProcessTaskApprovalDTO bpmProcessTaskApprovalDTO) {//需要根据流程中不同的节点 更改对应的表单信息 比如 需要在流程最后一个节点点击提交时 更改流程状态为完成BidDO bidDO = BidConvert.INSTANCE.convert3(bidUpdateReqVO);bidDO.setDeptId(findDeptByCode(bidUpdateReqVO.getDeptCode()).getId());bidDO.setTendererId(findDeptByName(bidUpdateReqVO.getTendererName()).getId());updateFlowInfoByProcessInstanceState(bidDO);fileApi.deleteFile(bidMapper.selectById(bidDO.getId()).getFiles(), bidDO.getFiles());bidMapper.updateById(bidDO);bpmProcessTaskApi.approvalTask(bpmProcessTaskApprovalDTO);}
4.流程的审批
流程的审批按照设计流程的审批节点依次进行流转,不支持夸环节提交,支持流程的自由回退。提交当前审批任务时,进行下一节点的人员选择,在流程设计时,每个节点审批的人员的选择逻辑已经确定,也支持自由选择组织中的所有人员。
4.1.流程审批人员的设置
流程审批节点的人员设置主要思路为给定一个角色,让审批人员提交任务时,从角色中选择一个人员,这样可以缩小选择的范围。如果表单有对应的部门属性,可以设置审批人员是某个角色中且部门与表单部门属性相同的人员。
// 选择角色中的人员
private Set<Long> calculateTaskCandidateUsersByRole(BpmTaskAssignRuleDO rule) {return permissionApi.getUserRoleIdListByRoleIds(rule.getOptions());}//依据角色选择表单部门中的所属人员
private Set<Long> calculateTaskCandidateUsersByRolePerson(Map<String, Object> variables, BpmTaskAssignRuleDO rule) {Long deptId = (Long) variables.get("deptId");//获取流程实例变量的部门//从角色中获取属于该部门的人员Set<Long> userIdsByRoleId = permissionApi.getUserRoleIdListByRoleIds(rule.getOptions());Set<Long> userIdsByDeptId = userApi.getUsersByDeptId(deptId);return new HashSet<>(CollUtil.intersection(userIdsByDeptId, userIdsByRoleId));}
4.2.流程的流转
流程的流转其实没有什么好说的,就是按照流程设计的审批节点依次往下走,遇到网关时,根据前期设计好的条件读取对应的属性跳转到不同审批支线。本例中,会根据投标金额是否大于500万做判断,走不同的分支,而500万的属性,在创建流程时就已经传入了流程实例的变量中。
流程的回退,流程的回退依据流程的节点图,不管流程流转了多少圈,回退只允许回退当前审批节点的前面节点。
public Set<BpmDoneUserTaskNodeRespVO> getDoneUserTaskNodes(String taskId) {Set<BpmDoneUserTaskNodeRespVO> resultList = new HashSet<>();//获取流程实例idTask task = getTask(taskId);// 校验流程实例存在ProcessInstance instance = processInstanceService.getProcessInstance(task.getProcessInstanceId());//获取历史任务实例 条件为 流程实例 未完成 按照任务开始时间降序排列List<HistoricTaskInstance> historicTaskInstances = historyService.createHistoricTaskInstanceQuery().processInstanceId(instance.getProcessInstanceId()).finished().orderByHistoricTaskInstanceEndTime().desc().list();//需要做一个筛选,只能选择当前任务节点之前的节点进行回退// 1. 获取流程模型实例 BpmnModelBpmnModel bpmnModel = bpmProcessDefinitionService.getBpmnModel(task.getProcessDefinitionId());// 2. 通过任务节点id,来获取当前节点信息FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());// 3.获取下一个节点(或者多个节点的)信息,需要去重,因为并行的节点之前的节点会找多遍Set<FlowElement> flowElements = new HashSet<>();// 4.获取流程实例的变量Map<String, Object> variables = taskService.getVariables(taskId);getBeforeNodes(flowElement, flowElements, variables);if (flowElements.isEmpty()){//说明处于第一个节点,此时不能回退throw exception(TASK_ROLLBACK_FORBIDDEN);}//找交集for(FlowElement f: flowElements) {HistoricTaskInstance h = historicTaskInstances.stream().filter( hi -> hi.getTaskDefinitionKey().equals(f.getId())).findFirst().orElse(null);if (h == null){//没有获取到最新的的节点审批信息,是不正常的情况throw exception(TASK_ROLLBACK_APPROVED_INFO_NULL);}//获取 审批人员的编号Long assignee = Long.valueOf(h.getAssignee());//获取人员信息UserRespDTO userRespDTO = adminUserApi.getUser(assignee);//获取部门信息DeptRespDTO deptRespDTO = deptApi.getDept(userRespDTO.getDeptId());resultList.add(BpmTaskConvert.INSTANCE.convertBpmDoneUserTaskNodeRespVO(h, userRespDTO, deptRespDTO));}return resultList;}
5.流程审批详情
流程审批详情包括审批记录与流程图的展示。
5.1.审批记录详情
审批记录根据审批的先后顺序展示数据,状态根据提交回退的不同使用不同颜色的标签显示。
<el-table v-loading="loading" :data="tasks" border><el-table-column align="center" prop="name" label="审批环节" width="200" /><el-table-column align="center" prop="assigneeUser.nickname" label="审批人" width="180" /><el-table-columnlabel="任务开始时间"align="center"prop="createTime"width="180":formatter="dateFormatter"/><el-table-columnlabel="任务结束时间"align="center"prop="endTime"width="180":formatter="dateFormatter"/><el-table-column label="任务耗时" align="center" prop="durationInMillis" width="150"><template #default="scope"><span>{{ formatPast2(scope.row.durationInMillis) }}</span></template></el-table-column><el-table-column label="审批结果" align="center" prop="result" width="150"><template #default="scope"><el-tag :type="getTimelineItemType(scope.row)">{{ getDictLabel(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, scope.row.result) }}</el-tag></template></el-table-column><el-table-column label="审批意见" align="center" prop="reason" width="250" /></el-table>
public List<BpmTaskRespVO> getTaskListByProcessInstanceId(String processInstanceId) {// 获得任务列表List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery().processInstanceId(processInstanceId).orderByHistoricTaskInstanceStartTime().desc() // 创建时间倒序.list();if (CollUtil.isEmpty(tasks)) {return Collections.emptyList();}// 获得 TaskExtDO MapList<BpmTaskExtDO> bpmTaskExtDOs = taskExtMapper.selectListByTaskIds(convertSet(tasks, HistoricTaskInstance::getId));Map<String, BpmTaskExtDO> bpmTaskExtDOMap = convertMap(bpmTaskExtDOs, BpmTaskExtDO::getTaskId);// 获得 ProcessInstance MapHistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(processInstanceId);// 获得 User MapSet<Long> userIds = convertSet(tasks, task -> NumberUtils.parseLong(task.getAssignee()));userIds.add(NumberUtils.parseLong(processInstance.getStartUserId()));Map<Long, UserRespDTO> userMap = adminUserApi.getUserMap(userIds);// 获得 Dept MapMap<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), UserRespDTO::getDeptId));// 拼接数据return BpmTaskConvert.INSTANCE.convertList3(tasks, bpmTaskExtDOMap, processInstance, userMap, deptMap);}
5.2.流程图显示
流程图显示比较复杂,可以查看对应的代码,主要就是使用了bpmn-js库,根据后端的数据,进行不同的展示,核心代码个人理解是这一块。
const highlightDiagram = async () => {const activityList = activityLists.valueif (activityList.length === 0) {return}// 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现// 再次基础上,增加不同审批结果的颜色等等let canvas = bpmnModeler.get('canvas')let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务// debuggerbpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => {let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动if (!activity) {return}if (n.$type === 'bpmn:UserTask') {// 用户任务// 处理用户任务的高亮const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskIdif (!task) {return}// 高亮任务canvas.addMarker(n.id, getResultCss(task.result))// 如果非通过,就不走后面的线条了if (task.result !== 2) {return}// 处理 outgoing 出线const outgoing = getActivityOutgoing(activity)outgoing?.forEach((nn: any) => {// debuggerlet targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)// 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置if (targetActivity) {canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo')} else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {// TODO 芋艿:这个流程,暂时没走到过canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo')canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo')} else if (nn.targetRef.$type === 'bpmn:EndEvent') {// TODO 芋艿:这个流程,暂时没走到过if (!todoActivity && endActivity.key === n.id) {canvas.addMarker(nn.id, 'highlight')canvas.addMarker(nn.targetRef.id, 'highlight')}if (!activity.endTime) {canvas.addMarker(nn.id, 'highlight-todo')canvas.addMarker(nn.targetRef.id, 'highlight-todo')}}})} else if (n.$type === 'bpmn:ExclusiveGateway') {// 排它网关// 设置【bpmn:ExclusiveGateway】排它网关的高亮canvas.addMarker(n.id, getActivityHighlightCss(activity))// 查找需要高亮的连线let matchNN: any = undefinedlet matchActivity: any = undefinedconst outgoing = getActivityOutgoing(activity)outgoing.forEach((nn: any) => {let targetActivity = activityList.find((m: any) => m.key === nn.id)if (!targetActivity) {return}// 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径:// 1. 一个是 UserTask => EndEvent// 2. 一个是 EndEvent// 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。// 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~if (!matchActivity || matchActivity.type === 'endEvent') {matchNN = nnmatchActivity = targetActivity}})if (matchNN && matchActivity) {canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity))}} else if (n.$type === 'bpmn:ParallelGateway') {// 并行网关// 设置【bpmn:ParallelGateway】并行网关的高亮canvas.addMarker(n.id, getActivityHighlightCss(activity))const outgoing = getActivityOutgoing(activity)outgoing.forEach((nn: any) => {// 获得连线是否有指向目标。如果有,则进行高亮const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)if (targetActivity) {canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线// 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity))}})} else if (n.$type === 'bpmn:StartEvent') {// 开始节点 流程只要发起 开始节点就是完成状态let targetActivity = activityList.find((m) => m.key === n.id)if (targetActivity) {canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)}// 开始节点const outgoing = getActivityOutgoing(activity)outgoing.forEach((nn) => {// outgoing 例如说【bpmn:SequenceFlow】连线// 获得连线是否有指向目标。如果有,则进行高亮let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)if (targetActivity) {canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)}})} else if (n.$type === 'bpmn:EndEvent') {// 结束节点if (!processInstance.value || processInstance.value.result === 1) {return}canvas.addMarker(n.id, getResultCss(processInstance.value.result))} else if (n.$type === 'bpmn:ServiceTask') {//服务任务if (activity.startTime > 0 && activity.endTime === 0) {//进入执行,标识进行色canvas.addMarker(n.id, getResultCss(1))}if (activity.endTime > 0) {// 执行完成,节点标识完成色, 所有outgoing标识完成色。canvas.addMarker(n.id, getResultCss(2))const outgoing = getActivityOutgoing(activity)outgoing?.forEach((out) => {canvas.addMarker(out.id, getResultCss(2))})}}})
}
6.写在最后
本文简单的介绍了一个OA办公系统表单审批的全过程,行文比较粗糙,代码只展示了很少的一部分,如果有兴趣一起研究讨论的,欢迎留言批评指教。
相关文章:

一个完整的流程表单流转
1.写在前面 一个完整的流程表单审批(起表单-->各环节审批-->回退-->重新审批-->完成),前端由Vue2jsElement UI升级为Vue3tsElement Plus,后端流程框架使用Flowable,项目参考了ruoyi-vue-pro(https://gite…...

2024杭州国际智慧城市,人工智能,安防展览会(杭州智博会)
在智能化浪潮的冲击下,我们的生活与环境正在经历一场深刻的变革。这是一场前所未有的技术革命,它以前所未有的速度和广度,改变着我们的生活方式、工作方式、思维方式和社会结构。在这场变革中,有的人选择激流勇进,拥抱…...

编程笔记 html5cssjs 031 HTML视频
编程笔记 html5&css&js 031 HTML视频 一、<video>: 视频元素二、属性三、事件四、嵌入视频页面五、练习小结 视频应用广泛,当前的互联网应用中,视频越来越重要,比如抖音、快手、腾讯视频等应用。 一、<video>: 视频元素 …...

SpringBoot外部配置文件
✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: 循序渐进学SpringBoot ✨特色专栏: MySQL学习 🥭本文内容:SpringBoot外部配置文件 📚个人知识库: Leo知识库,欢迎大家访问 1.前言☕…...

99个Python脚本实用实例
题目:有四个数字:1、2、3、4,能组成多少个互不相同且无重复数字的三位数?各是多少? #!/usr/bin/python# -*- coding: UTF-8 -*-for i in range(1,5): for j in range(1,5): for k in range(1,5): …...

HarmonyOS 工程目录介绍
工程目录 AppScope:存放应用全局所需要的资源文件 base element:文件夹主要存放公共的字符串、布局文件等资源media:存放全局公共的多媒体资源文件app.json5:应用的全局的配置文件,用于存放应用公共的配置信息 {"…...

门店管理系统驱动智慧零售升级
在当今数字化经济的大潮中,实体门店正在经历一场由内而外的深度变革。门店管理系统以其高效、便捷和全面的功能特性,为实体店提供了高效的运营解决方案。 门店管理系统拜托了传统零售业对本地化软件的依赖,它将复杂的信息技术转化为易于获取…...

Iterator迭代器操作集合元素时,不能用集合删除元素
在使用Iterator迭代器对集合中的元素进行迭代时,如果调用了集合对象的remove()方法删除元素或者调用add()方法添加元素之后,继续使用迭代器遍历元素,会出现异常(java.util.ConcurrentModificationException)。 import java.util.ArrayList; …...

Spring Boot是什么-特点介绍
什么是SpringBoot Spring Boot是由Pivotal团队提供的全新框架,其中“Boot”的意思就是“引导”,Spring Boot 并不是对 Spring 功能上的增强,而是提供了一种快速开发 Spring应用的方式。 Spring Boot 特点 嵌入的 Tomcat,无需部署…...

相机成像之图像传感器与ISP【四】
文章目录 1、图像传感器基础1.1 基础原理——光电效应1.2 基础的图像传感器设计1.3 衡量传感器效率的一个关键指标:光量子效率(QE)1.4 感光单元的响应1.5 像素的满阱容量1.6 像素尺寸和填充比例1.7 微透镜的作用1.8 光学低通滤波器简介1.9 传…...

新手入门Java 方法带参,方法重载及面向对象和面向过程的区别介绍
第二章 方法带参 课前回顾 1.描述类和对象的关系 类是一组对象的共有特征和行为的描述。对象是类的其中一个具体的成员。 2.如何创建对象 类名 对象名 new 类名();3.如何定义和调用方法 public void 方法名(){}对象名.方法名();4.成员变量和局部变量的区别 成员变量有初…...

使用Sqoop将Hive数据导出到TiDB
关系型数据库与大数据平台之间的数据传输之前写过一些 使用Sqoop将数据在HDFS与MySQL互导 使用Sqoop将SQL Server视图中数据导入Hive 使用DataX将Hive与MySQL中的表互导 使用Sqoop将Hive数据导出到TiDB虽然没写过,但网上一堆写的,那为什么我要专门写一下…...

互联网上门洗衣洗鞋工厂系统搭建;
随着移动互联网的普及,人们越来越依赖手机应用程序来解决生活中的各种问题。通过手机预约服务、购买商品、获取信息已经成为一种生活习惯。因此,开发一款上门洗鞋小程序,可以满足消费者对于方便、快捷、专业的洗鞋服务的需求,同时…...

Redis面试题12
Redis 的主从复制是什么? Redis 的主从复制是一种数据备份和高可用性机制,通过将一个 Redis 服务器的数据复制到其他 Redis 从服务器上来实现数据的冗余备份和读写分离。 主从复制的工作原理如下: 配置主服务器并开启主从复制功能。从服务器…...

el-tree多个树进行节点同步联动(完整版)
2024.1.11今天我学习了如何对多个el-tree树进行相同节点的联动效果,如图: 这边有两棵树,我们发现第一个树和第二个树之间会有重复的指标,当我们选中第一个树的指标,我们希望第二个树如果也有重复的指标也能进行勾选上&…...

python两个字典合并,两个list合并
1.两个字典: a{‘a’:1,‘b’:2,‘c’:3} b {‘aa’:11,‘bb’:22,‘cc’:33} 合并1:dict(a, **b) 结果:{‘a’: 1,‘aa’: 11,‘c’: 3,‘b’: 2,‘bb’: 22,‘cc’: 33} 合并2:dict(a.items()b.items()) 结果:{‘…...

搜维尔科技:【简报】元宇宙数字人赛道,《全息影像技术应用》!
期待着看展的主角来到今天要参观的全息影像展,平时就喜欢看展的她对于所谓的全息影像非常好奇,于是她带着期待的心情进入展内。进入展内的主角看到的是与之前完全不同的画展,每幅画看起来就像真的一样,充满好奇的她在展览的各处游…...

SparkSQL和Hive语法差异
SparkSQL和Hive语法差异 1、仅支持Hive SparkSQL关联条件on不支持函数rand()创建零时表时,Spark不支持直接赋值nullSpark无法读取字段类型为void的表SparkSQL中如果表达式没有指定别名,SparkSQL会将整个表达式作为别名,如果表达式中包含特殊…...

XCODE IOS 静态链接库替换升级
XCODE 版本15.2. 一个很久需求没更新的IOS 应用,近来有新需求要开发。 拉下代码运行,出现了个BAD_ACCESS错误。出错的位置位于一个调用的第三方的.a静态库内部。因为调用代码并没有修改,很容易想到可能XCODE相关升级,导致的问题。…...

API设计:从基础到优秀实践
在这次深入探讨中,我们将深入了解API设计,从基础知识开始,逐步进阶到定义出色API的最佳实践。 作为开发者,你可能对许多这些概念很熟悉,但我将提供详细的解释,以加深你的理解。 API设计:电子商…...

路由的安装顺序
安装前端路由的顺序通常如下: 安装前端框架:选择并安装适合你的项目的前端框架,如React、Vue或Angular等。 创建路由配置文件:在项目根目录下创建一个路由配置文件,比如router.js或routes.js等,用于定义路…...

华为OD机试真题-围棋的气--Java-OD统一考试(C卷)
题目描述: 围棋棋盘由纵横各19条线垂直相交组成,棋盘上一共19x19=361个交点,对弈双方一方执白棋,一方执黑棋,落子时只能将棋子置于交点上。 “气”是围棋中很重要的一个概念,某个棋子有几口气,是指其上下左右方向四个相邻的交叉点中,有几个交叉点没有棋子,由此可知: …...

CANFD数据记录仪在新能源汽车复杂路测下的应用
CANFD数据记录仪在新能源汽车复杂路测下的应用 汽车制造商在生产预批量阶段的耐久性测试中,为了检测潜在故障,必须让车辆在严酷的路况和环境下接受测试。为确保能回溯故障发生的现场情况,我们需要对测试数据精准记录与储存。这些数据是新车型优化迭代的关键,也是确保产品质量的…...

java: 5-6 break
文章目录 1. break1.1 介绍1.2 语法和流程图1.3 入门练习1.4 细节说明1.5 练习 【老韩视频p137-】 1. break 看个需求:随机生成 1-100 的一个数,直到生成了 97 这个数,看看你一共用了几次? 【思路分析:循环,但是循环的次数不知道…...

如何使用Imagewheel搭建一个简单的的私人图床无公网ip也能访问
文章目录 1.前言2. Imagewheel网站搭建2.1. Imagewheel下载和安装2.2. Imagewheel网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar临时数据隧道3.2.Cpolar稳定隧道(云端设置)3.3.Cpolar稳定隧道(本地设置) 4.公网访问测…...

响应式编程Reactor API大全(上)
Reactor 是一个基于响应式编程的库,主要用于构建异步和事件驱动的应用程序。Reactor 提供了丰富的 API,包括创建、转换、过滤、组合等操作符,用于处理异步数据流。以下是一些 Reactor 的主要 API 示例: pom依赖 <dependencyMan…...

vue3自定义指令
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。 页面内创建自定义指令 下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦: <script setu…...

ECharts 多季度连续显示到一个图中。
效果图 二.相关option 以下option可以复制到 echarts的编辑器 进行查看修改 const site test1; const site2 test2;const qtrlyOption function (data: any, titleText: string): any {//获取最大值 。最大最小值的目的是:使左右里边的所有bar使用同一个指标let …...

【Microsoft Copilot】手机端发布 ——GPT-4, DALL-E3 免费用
Microsoft Copilot 关于Microsoft CopilotMicrosoft Copilot 的特点1. 可以在手机端使用:2. 可以免费使用GPT-4。3. 可以无限制地使用GPT-4。4. 可以使用DALL-E3生成图片。5. 搜索功能6. 图像识别 Microsoft Copilot的缺点和注意事项1. 非常容易报错2. 不支持长篇聊…...

[蓝桥杯 2013 省 AB] 错误票据
题目背景 某涉密单位下发了某种票据,并要在年终全部收回。 题目描述 每张票据有唯一的 ID 号,全年所有票据的 ID 号是连续的,但 ID 的开始数码是随机选定的。因为工作人员疏忽,在录入 ID 号的时候发生了一处错误,造…...