Commit 2835c9d1 by puhui999

Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev

parents f7477c32 4754685d
...@@ -69,11 +69,11 @@ ...@@ -69,11 +69,11 @@
支持 Spring Boot、Spring Cloud 两种架构: 支持 Spring Boot、Spring Cloud 两种架构:
① Spring Boot 单体架构:<https://github.com/YunaiV/ruoyi-vue-pro> ① Spring Boot 单体架构:<https://doc.iocoder.cn>
![架构图](/.image/common/ruoyi-vue-pro-architecture.png) ![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
② Spring Cloud 微服务架构:<https://github.com/YunaiV/yudao-cloud> ② Spring Cloud 微服务架构:<https://cloud.iocoder.cn>
![架构图](/.image/common/yudao-cloud-architecture.png) ![架构图](/.image/common/yudao-cloud-architecture.png)
...@@ -121,17 +121,21 @@ ...@@ -121,17 +121,21 @@
### 工作流程 ### 工作流程
| | 功能 | 描述 | | | 功能 | 描述 |
|-----|-------|----------------------------------------| |----|-------|-----------------------------------------|
| 🚀 | 流程模型 | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 | | 🚀 | 流程模型 | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器 |
| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 | | 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 | | 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 | | 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作 | | 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 | | 🚀 | 已办任务 | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息 |
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 | | 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
![功能图](/.image/common/bpm-feature.png) ![功能图](/.image/common/bpm-feature.png)
| BPMN 设计器 | 钉钉/飞书设计器 |
|------------------------------|--------------------------------|
| ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
### 支付系统 ### 支付系统
| | 功能 | 描述 | | | 功能 | 描述 |
......
...@@ -9,11 +9,11 @@ ...@@ -9,11 +9,11 @@
"dev": "vite --mode env.local", "dev": "vite --mode env.local",
"dev-server": "vite --mode dev", "dev-server": "vite --mode dev",
"ts:check": "vue-tsc --noEmit", "ts:check": "vue-tsc --noEmit",
"build:local": "node ./node_modules/vite/bin/vite.js build", "build:local": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build",
"build:dev": "node ./node_modules/vite/bin/vite.js build --mode dev", "build:dev": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode dev",
"build:test": "node ./node_modules/vite/bin/vite.js build --mode test", "build:test": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node ./node_modules/vite/bin/vite.js build --mode stage", "build:stage": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod": "node ./node_modules/vite/bin/vite.js build --mode prod", "build:prod": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode prod",
"serve:dev": "vite preview --mode dev", "serve:dev": "vite preview --mode dev",
"serve:prod": "vite preview --mode prod", "serve:prod": "vite preview --mode prod",
"preview": "pnpm build:local && vite preview", "preview": "pnpm build:local && vite preview",
...@@ -64,13 +64,14 @@ ...@@ -64,13 +64,14 @@
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.12.0", "qs": "^6.12.0",
"sortablejs": "^1.15.3",
"steady-xml": "^0.1.0", "steady-xml": "^0.1.0",
"url": "^0.11.3", "url": "^0.11.3",
"video.js": "^7.21.5", "video.js": "^7.21.5",
"vue": "3.5.12", "vue": "3.5.12",
"vue-dompurify-html": "^4.1.4", "vue-dompurify-html": "^4.1.4",
"vue-i18n": "9.10.2", "vue-i18n": "9.10.2",
"vue-router": "^4.3.0", "vue-router": "4.4.5",
"vue-types": "^5.1.1", "vue-types": "^5.1.1",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1", "web-storage-cache": "^1.1.1",
...@@ -95,7 +96,7 @@ ...@@ -95,7 +96,7 @@
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"bpmn-js": "8.9.0", "bpmn-js": "8.10.0",
"bpmn-js-properties-panel": "0.46.0", "bpmn-js-properties-panel": "0.46.0",
"consola": "^3.2.3", "consola": "^3.2.3",
"eslint": "^8.57.0", "eslint": "^8.57.0",
...@@ -143,6 +144,7 @@ ...@@ -143,6 +144,7 @@
"url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues" "url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues"
}, },
"homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3", "homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3",
"web-types": "./web-types.json",
"engines": { "engines": {
"node": ">= 16.0.0", "node": ">= 16.0.0",
"pnpm": ">=8.6.0" "pnpm": ">=8.6.0"
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import request from '@/config/axios'
export const getActivityList = async (params) => {
return await request.get({
url: '/bpm/activity/list',
params
})
}
...@@ -36,6 +36,16 @@ export const CategoryApi = { ...@@ -36,6 +36,16 @@ export const CategoryApi = {
return await request.put({ url: `/bpm/category/update`, data }) return await request.put({ url: `/bpm/category/update`, data })
}, },
// 批量修改流程分类的排序
updateCategorySortBatch: async (ids: number[]) => {
return await request.put({
url: `/bpm/category/update-sort-batch`,
params: {
ids: ids.join(',')
}
})
},
// 删除流程分类 // 删除流程分类
deleteCategory: async (id: number) => { deleteCategory: async (id: number) => {
return await request.delete({ url: `/bpm/category/delete?id=` + id }) return await request.delete({ url: `/bpm/category/delete?id=` + id })
......
...@@ -26,8 +26,8 @@ export type ModelVO = { ...@@ -26,8 +26,8 @@ export type ModelVO = {
bpmnXml: string bpmnXml: string
} }
export const getModelPage = async (params) => { export const getModelList = async (name: string | undefined) => {
return await request.get({ url: '/bpm/model/page', params }) return await request.get({ url: '/bpm/model/list', params: { name } })
} }
export const getModel = async (id: string) => { export const getModel = async (id: string) => {
...@@ -38,6 +38,16 @@ export const updateModel = async (data: ModelVO) => { ...@@ -38,6 +38,16 @@ export const updateModel = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update', data: data }) return await request.put({ url: '/bpm/model/update', data: data })
} }
// 批量修改流程分类的排序
export const updateModelSortBatch = async (ids: number[]) => {
return await request.put({
url: `/bpm/model/update-sort-batch`,
params: {
ids: ids.join(',')
}
})
}
export const updateModelBpmn = async (data: ModelVO) => { export const updateModelBpmn = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update-bpmn', data: data }) return await request.put({ url: '/bpm/model/update-bpmn', data: data })
} }
......
import request from '@/config/axios' import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model' import { ProcessDefinitionVO } from '@/api/bpm/model'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts' import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = { export type Task = {
id: string id: string
name: string name: string
...@@ -24,30 +24,30 @@ export type ProcessInstanceVO = { ...@@ -24,30 +24,30 @@ export type ProcessInstanceVO = {
// 用户信息 // 用户信息
export type User = { export type User = {
id: number, id: number
nickname: string, nickname: string
avatar: string avatar: string
} }
// 审批任务信息 // 审批任务信息
export type ApprovalTaskInfo = { export type ApprovalTaskInfo = {
id: number, id: number
ownerUser: User, ownerUser: User
assigneeUser: User, assigneeUser: User
status: number, status: number
reason: string reason: string
} }
// 审批节点信息 // 审批节点信息
export type ApprovalNodeInfo = { export type ApprovalNodeInfo = {
id : number id: number
name: string name: string
nodeType: NodeType nodeType: NodeType
candidateStrategy?: CandidateStrategy
status: number status: number
startTime?: Date startTime?: Date
endTime?: Date endTime?: Date
candidateUserList?: User[] candidateUsers?: User[]
tasks: ApprovalTaskInfo[] tasks: ApprovalTaskInfo[]
} }
...@@ -88,12 +88,16 @@ export const getProcessInstanceCopyPage = async (params: any) => { ...@@ -88,12 +88,16 @@ export const getProcessInstanceCopyPage = async (params: any) => {
} }
// 获取审批详情 // 获取审批详情
export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => { export const getApprovalDetail = async (params: any) => {
const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params })
return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
} }
// 获取表单字段权限 // 获取表单字段权限
export const getFormFieldsPermission = async (params: any) => { export const getFormFieldsPermission = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params }) return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
} }
// 获取流程实例的 BPMN 模型视图
export const getProcessInstanceBpmnModelView = async (id: string) => {
return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id })
}
...@@ -36,18 +36,9 @@ export enum TaskStatusEnum { ...@@ -36,18 +36,9 @@ export enum TaskStatusEnum {
*/ */
RETURN = 5, RETURN = 5,
/** /**
* 委派中
*/
DELEGATE = 6,
/**
* 审批通过中 * 审批通过中
*/ */
APPROVING = 7, APPROVING = 7
}
export type TaskVO = {
id: number
} }
export const getTaskTodoPage = async (params: any) => { export const getTaskTodoPage = async (params: any) => {
...@@ -76,12 +67,12 @@ export const getTaskListByProcessInstanceId = async (processInstanceId: string) ...@@ -76,12 +67,12 @@ export const getTaskListByProcessInstanceId = async (processInstanceId: string)
}) })
} }
// 获取所有可回退的节点 // 获取所有可退回的节点
export const getTaskListByReturn = async (id: string) => { export const getTaskListByReturn = async (id: string) => {
return await request.get({ url: '/bpm/task/list-by-return', params: { id } }) return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
} }
// 回退 // 退回
export const returnTask = async (data: any) => { export const returnTask = async (data: any) => {
return await request.put({ url: '/bpm/task/return', data }) return await request.put({ url: '/bpm/task/return', data })
} }
...@@ -106,6 +97,16 @@ export const signDeleteTask = async (data: any) => { ...@@ -106,6 +97,16 @@ export const signDeleteTask = async (data: any) => {
return await request.delete({ url: '/bpm/task/delete-sign', data }) return await request.delete({ url: '/bpm/task/delete-sign', data })
} }
// 抄送
export const copyTask = async (data: any) => {
return await request.put({ url: '/bpm/task/copy', data })
}
// 获取我的待办任务
export const myTodoTask = async (processInstanceId: string) => {
return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId })
}
// 获取减签任务列表 // 获取减签任务列表
export const getChildrenTaskList = async (id: string) => { export const getChildrenTaskList = async (id: string) => {
return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id }) return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })
......
...@@ -27,7 +27,7 @@ export const authorize = ( ...@@ -27,7 +27,7 @@ export const authorize = (
return request.post({ return request.post({
url: '/system/oauth2/authorize', url: '/system/oauth2/authorize',
headers: { headers: {
'Content-type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
}, },
params: { params: {
response_type: responseType, response_type: responseType,
......
<svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg>
\ No newline at end of file
<svg t="1729561718271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8640" width="200" height="200"><path d="M908.5952 920.4224H164.7616a31.0784 31.0784 0 0 1-30.976-30.976c0-17.0496 13.9264-30.976 30.976-30.976h743.8336c17.0496 0 31.0272 13.9264 31.0272 30.976a31.0784 31.0784 0 0 1-31.0272 30.976z m0-123.9552H164.7616a31.0784 31.0784 0 0 1-30.976-30.976v-154.9824c0-51.1488 41.8304-92.9792 92.9792-92.9792h198.3488c-6.1952-37.1712-24.7808-72.8064-51.1488-103.8336a216.576 216.576 0 0 1-54.2208-144.128c0-58.88 23.2448-114.688 66.6112-156.4672C429.7728 71.168 485.5296 51.0976 545.9968 52.6848c111.5648 4.608 206.08 100.6592 207.616 212.2752 1.536 55.808-20.1216 110.0288-57.344 151.8592-26.3168 27.904-41.8304 61.952-48.0256 100.7104h198.3488c51.2 0 93.0304 41.8304 93.0304 92.9792v154.9824a31.0784 31.0784 0 0 1-31.0272 30.976z m-712.8064-61.952H877.568v-124.0064a31.0784 31.0784 0 0 0-31.0272-30.976h-232.448a31.0784 31.0784 0 0 1-30.976-31.0272c0-65.024 23.2448-127.0784 66.6624-173.568 27.8528-29.3888 41.8304-68.1472 41.8304-108.4416-1.536-80.5888-68.1984-148.7872-148.7872-151.8592a150.528 150.528 0 0 0-113.152 43.3664 153.6 153.6 0 0 0-48.0256 111.616c0 37.1712 13.9776 74.3424 38.7584 102.2464 44.9536 51.1488 69.7344 113.152 69.7344 176.64a31.0784 31.0784 0 0 1-30.976 31.0272h-232.448a31.0784 31.0784 0 0 0-30.976 30.976v123.9552z" fill="#fff" p-id="8641"></path></svg>
\ No newline at end of file
<svg t="1729178183592" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4332" width="200" height="200"><path d="M784.074702 99.196443l10.927871 18.473304-21.302843-2.56935-14.180213 16.066571-4.130475-21.042655-19.676671-8.521137 18.733492-10.440019 2.016452-21.335366 15.708814 14.603017 20.945085-4.683373-9.041512 19.449008zM1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512z" fill="#8a8a8a" p-id="4333"></path><path d="M1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512zM571.924408 100.009528l-17.400031-12.488994 20.52228-6.211974 6.504685-20.457234 12.261331 17.595172 21.432936-0.09757-12.944323 17.074798 6.732349 20.359663-20.262093-7.02506-17.269938 12.716659 0.422804-21.46546zM991.444053 784.43246l-21.172749 3.480006 10.114785-18.928632-9.822074-19.026203 21.107702 3.772717 15.090868-15.253486 2.927109 21.237796 19.156296 9.626933-19.318914 9.366746-3.219819 21.205273-14.863204-15.48115zM428.008258 156.795426l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.936097 19.936859-7.870669-5.88674 20.619851 13.692361 16.521899-21.432936 0.813086-11.513292 18.083024-7.382817-20.132zM854.260251 896.475655l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.96862 19.936859-7.903192-5.854217 20.619851 13.659838 16.554423-21.432936 0.780562-11.513292 18.115547-7.382817-20.164523zM562.460092 923.665237l10.895347 18.440782-21.302843-2.569351-14.180212 16.099095-4.130475-21.042655-19.676672-8.521137 18.733493-10.440019 2.016452-21.36789 15.708814 14.603018 20.945085-4.683373-9.008989 19.48153zM242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988z" fill="#8a8a8a" p-id="4334"></path><path d="M242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988zM700.814737 943.959854l-17.400032-12.521518 20.522281-6.211974 6.504685-20.42471 12.26133 17.595172 21.432937-0.130094-12.944323 17.107321 6.732349 20.359663-20.262093-7.025059-17.269938 12.684135 0.422804-21.432936zM303.541115 278.823313l-21.140226 3.480006 10.114785-18.928633-9.854597-19.058726 21.107702 3.772717 15.090868-15.220962 2.927109 21.237796 19.156296 9.626933-19.28639 9.366746-3.252342 21.172749-14.863205-15.448626z" fill="#8a8a8a" p-id="4335"></path><path d="M407.648595 90.642782a486.713038 486.713038 0 0 1 504.568397 11.578339l25.010513-14.407877A512.081309 512.081309 0 0 0 139.850723 547.401747l24.977989-14.407877a486.778085 486.778085 0 0 1 242.819883-442.351088zM893.28836 933.422265a486.810608 486.810608 0 0 1-504.568398-11.610863l-25.010513 14.407877a512.081309 512.081309 0 0 0 797.5394-459.621026l-24.97799 14.505447a486.843132 486.843132 0 0 1-242.982499 442.318565z" fill="#8a8a8a" p-id="4336"></path><path d="M814.061299 795.880705a326.665269 326.665269 0 0 1-258.170939 29.563792l-29.791456 17.172368a353.236906 353.236906 0 0 0 472.793013-272.448721l-29.693886 17.172367a326.762839 326.762839 0 0 1-155.136732 208.540194zM486.875655 228.119295a326.795363 326.795363 0 0 1 258.170939-29.563792l29.791456-17.172368a353.236906 353.236906 0 0 0-472.793013 272.448721l29.82398-17.172367a326.762839 326.762839 0 0 1 155.006638-208.540194zM1288.350389 374.73489a53.923837 53.923837 0 0 1-14.34283 12.001143L229.420232 988.712085A53.793743 53.793743 0 0 1 156.112434 968.937843l-148.924757-258.235985a53.76122 53.76122 0 0 1 19.741718-73.437891L1071.516722 35.352962A53.826266 53.826266 0 0 1 1144.82452 55.062157l148.827187 258.268508a53.793743 53.793743 0 0 1-5.398888 61.404225zM32.19819 665.754486a28.360426 28.360426 0 0 0-5.626553 10.73273 28.067715 28.067715 0 0 0 2.699444 21.432936L178.195839 956.188661a28.165285 28.165285 0 0 0 38.442687 10.342449l1044.587328-601.976052a28.132762 28.132762 0 0 0 10.440019-38.442687l-148.924758-258.268509a28.197808 28.197808 0 0 0-38.442687-10.342449L39.711101 659.444942a28.230332 28.230332 0 0 0-7.512911 6.309544z" fill="#8a8a8a" p-id="4337"></path><path d="M498.941845 597.390249l-138.322121 79.877529 38.637827 66.933207q8.000762 13.854979 21.595554 5.98431l114.254788-65.957504a21.172749 21.172749 0 0 0 9.952167-11.123011q2.634397-9.757027-16.91218-47.321582l18.440781-4.130474q20.489757 43.22363 18.148071 56.167953a36.166047 36.166047 0 0 1-16.261712 19.514054l-123.068636 71.031158q-25.17313 14.603017-40.394092-11.77348L317.103383 639.020232l16.066571-9.269176 18.570875 32.133143 122.027886-70.47826-33.596697-58.249452-150.160648 86.707448-9.041511-15.611243 166.454883-96.106718zM691.903319 563.663459c-3.935334 3.837764-9.757027 9.399269-17.497602 16.619469l23.319295 40.394093-15.611244 9.008988-21.237795-36.816516q-31.027346 27.709957-64.754137 54.314118l-12.814229-13.39965 9.171605-7.382818 9.236653-7.122629-79.714912-138.126982-17.627696 10.179832-8.781324-15.155915L601.683341 414.836271l6.960013 12.06619 86.34969-49.858408 8.488614 14.733111q28.197808 65.82741 30.506972 123.39387a274.660314 274.660314 0 0 0 69.339939 27.612387l-3.642623 18.440781a322.177037 322.177037 0 0 1-65.534699-26.40902 220.899095 220.899095 0 0 1-15.38358 72.819946l-18.14807-6.179451a215.272542 215.272542 0 0 0 15.448626-77.340702 312.940384 312.940384 0 0 1-89.374369-86.739971l-8.748801 5.138701-7.2202-12.488995-17.172368 9.919644 71.876767 124.499667q10.570113-10.017215 17.465079-16.61947z m-134.32174-56.948515l40.166428-23.189202-19.969382-34.702493-40.166429 23.189201z m28.067714 48.785135l40.166429-23.189201-19.514055-33.921931-40.166428 23.189201z m48.557472-8.813847l-40.166428 23.189201 21.888264 37.922312q13.334604-10.92787 35.775766-30.767159z m7.2202-117.832365A289.848753 289.848753 0 0 0 715.515325 503.365031a330.437986 330.437986 0 0 0-26.441544-101.92841zM812.760362 400.460918l-4.813467 17.95293a280.482007 280.482007 0 0 0-56.167953-12.781706l5.073654-17.530125a291.637542 291.637542 0 0 1 55.907766 12.358901z m24.360045 28.78323a925.063745 925.063745 0 0 1 10.017214 101.895887l-18.440781 2.016452a812.792886 812.792886 0 0 0-8.878895-101.375512z m-45.923075-86.25212l-4.813467 18.017977a290.922026 290.922026 0 0 0-58.542163-11.513292l5.073655-17.497602a308.972527 308.972527 0 0 1 58.281975 10.992917z m48.459902-17.562649l-9.334223 13.724885A298.792695 298.792695 0 0 0 783.814515 315.477211l9.757027-14.180212a437.635191 437.635191 0 0 1 46.085692 24.13238zM834.355916 269.944418l16.521899-9.529363 35.157821 60.916373 48.199714-27.840051L1003.282579 413.047483q12.716659 22.115928-8.228426 34.214642l-26.018739 15.058345-13.237034-13.009369 25.238177-13.952549c6.992536-4.065428 8.45609-9.561887 4.423186-16.554423l-12.716659-22.018358-80.527997 46.475973L919.762427 491.1037l-16.066572 9.269176-81.926505-141.899698 47.744387-27.579864z m107.750103 73.763125l-14.830682-25.660981-80.56052 46.508496 14.830681 25.726028z m-72.592282 60.330952l14.700587 25.433317 80.560521-46.508496-14.700587-25.433318z m45.532793-166.064603a222.720407 222.720407 0 0 1-2.406733 56.13543l-16.456853 0.878132a242.722312 242.722312 0 0 0 2.081499-55.647578z" fill="#8a8a8a" p-id="4338"></path></svg>
\ No newline at end of file
<svg t="1729585232424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1602" width="200" height="200"><path d="M925.5 898.9H804.9c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V572.2c0-19-15.4-34.4-34.5-34.4H529.2V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H443.1c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V537.8H219.1c-19 0-34.5 15.4-34.5 34.4V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H98.5c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4H133V555c0-38 30.9-68.8 68.9-68.8h275.7V297.1h-34.5c-19 0-34.5-15.4-34.5-34.4V159.5c0-19 15.4-34.4 34.5-34.4h120.6c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4h-34.5v189.2h292.9c38.1 0 68.9 30.8 68.9 68.8v172h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 18.8-15.4 34.2-34.5 34.2z m0 0" p-id="1603" fill="#fff"></path></svg>
\ No newline at end of file
<svg t="1729649333541" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1644" width="200" height="200"><path d="M647.888 893.84L491.904 571.52l393.888-393.888-237.904 716.208zM872.32 123.232L459.872 535.68 134.96 380.88 872.32 123.232z m90.72-68.32a23.968 23.968 0 0 0-24.784-5.568L64.08 354.816a23.984 23.984 0 0 0-2.4 44.32l381.392 181.728 187.36 387.088a24.048 24.048 0 0 0 23.152 13.504 24.032 24.032 0 0 0 21.232-16.4L968.96 79.552c2.88-8.672 0.592-18.24-5.92-24.64z" fill="#fff" p-id="1645"></path></svg>
\ No newline at end of file
<svg t="1730189225011" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2651" id="mx_n_1730189225011" width="200" height="200"><path d="M793.889347 200.380242c27.648573 20.615681 42.196018 32.710677 63.781037 56.119312 25.313864 27.453234 43.242957 48.52047 64.502857 86.507991 44.537416 79.580127 53.527718 136.949077 53.517684 212.063821 0 64.933675-15.452562 130.459388-40.138263 187.311893-22.076044 50.841799-61.545336 104.359483-101.886297 138.933914-45.506755 39.001681-81.214423 60.462941-137.605337 81.826531-55.699867 21.102023-114.070267 28.641326-181.379458 27.791064-68.274516-0.862973-129.364283-11.040029-180.533878-31.80489-46.159002-18.731189-98.338744-46.827973-141.596418-87.541551-43.946046-41.361142-70.369064-75.958317-93.88139-127.198155-26.157437-57.004361-40.094111-129.065922-39.680686-191.781288 0-36.980719 4.033895-70.902234 12.252873-105.241856 8.532726-35.651474 20.069131-69.572989 38.13135-102.35257 18.856956-34.221214 36.754607-62.067803 58.869452-88.973149 23.248751-28.285434 39.2104-46.417894 64.295476-63.475987 18.297696-12.442861 36.879036-9.295353 47.199252-2.306612 4.403836 2.982273 8.919391 6.577992 12.933218 12.933217 9.572307 15.156208-0.334486 29.769212-6.69038 38.465836-7.148625 9.781026-23.130343 26.023643-38.738775 43.218205-38.192895 42.075603-55.133918 65.965228-74.986303 106.965794-30.772668 63.552249-37.495827 115.718611-38.131349 166.573791-0.668971 53.517684 9.995096 99.647251 27.427813 140.483919 33.916163 80.572211 94.807915 144.44289 175.270414 178.615938 41.108271 17.845472 113.812713 37.319888 181.960793 38.13135 56.193568 0.668971 125.919751-11.321666 166.574459-28.096784 45.935566-18.954626 97.223569-56.862539 127.10383-94.324918 23.013273-28.852721 52.179742-70.910931 64.413884-105.694749 14.863868-42.260239 24.806784-87.661297 24.559934-132.458943 0-54.414105-11.53373-108.417461-36.918505-156.856317-20.16747-38.483228-46.480777-74.607665-84.66899-108.048189-13.377414-11.714352-23.822728-20.067124-38.808348-31.619586-10.191774-7.857065-36.059546-25.027545-28.923632-47.326356 4.970455-15.53217 18.303717-25.294464 31.887843-27.205046 19.456354-2.736092 28.565733 2.427027 43.705885 12.041479l6.179955 4.322891zM510.755379 531.65738c-8.696624-0.668971-10.034566-0.446204-20.738102-6.689711-11.031333-6.434832-17.839451-21.183637-16.514219-35.175166V92.220334c0-18.178619 0.386665-22.815926 8.988295-31.685813 5.351768-5.519011 10.963097-11.381873 26.08987-11.539751 16.055305-0.167243 21.407073 3.846584 27.929542 9.700081 9.70677 8.711341 10.703537 17.56049 10.377078 33.525483v397.5715c-0.509756 15.273947 0.326458 22.967114-11.380535 33.502739-3.884046 3.495374-8.027653 7.693167-20.96087 8.362138l-3.791059 0.000669z m4.453341 0.573308" p-id="2652" fill="#ffffff"></path></svg>
\ No newline at end of file
<svg t="1729585239190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1755" width="200" height="200"><path d="M901.489435 536.822664v-0.931601l-1.001722-198.240726c-0.100172-19.162936-9.21584-37.474409-25.043042-50.246361-14.024104-11.349507-32.265456-17.60025-51.348255-17.610268l-618.062295-0.18031c-19.142902 0-37.424323 6.280795-51.478478 17.690405-15.827203 12.842072-24.902802 31.2437-24.892785 50.486775v196.798247A114.987635 114.987635 0 1 0 195.295664 536.922836V338.782282c1.15198-1.252152 4.808264-3.596181 10.768509-3.596181l276.725622 0.090155v199.753326a114.987635 114.987635 0 1 0 65.612772 1.412428V335.326342l275.693849 0.080138c6.01033 0 9.626546 2.344029 10.768508 3.596181l1.001722 195.70637a114.987635 114.987635 0 1 0 65.592737 2.113633zM214.979496 645.910158a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m354.689623 0a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m295.507904 56.437001a56.437001 56.437001 0 1 1 56.437001-56.437001 56.507122 56.507122 0 0 1-56.457035 56.437001z" p-id="1756" fill='#fff'></path></svg>
\ No newline at end of file
<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FAFAFA" d="M0 0h22v22H0z"/><circle fill="#919BAE" cx="1" cy="1" r="1"/></g></svg>
\ No newline at end of file
<svg t="1729561814171" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1359" width="200" height="200"><path d="M674.496 603.456c120.256 0 218.176 90.752 221.44 203.84l0.064 5.888v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352a21.568 21.568 0 0 1-22.144-20.928v-125.888c0-67.712-56.512-123.264-128-125.76l-4.928-0.064H349.568c-71.488 0-130.176 53.504-132.864 121.152l-0.064 4.672v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352A21.568 21.568 0 0 1 128 939.072v-125.888c0-113.92 95.872-206.528 215.36-209.664l6.208-0.064h324.928zM497.216 128c122.368 0 221.568 93.888 221.568 209.728s-99.2 209.792-221.568 209.792c-122.304 0-221.44-93.952-221.44-209.728C275.712 221.952 374.848 128 497.152 128z m0 83.904c-73.408 0-132.864 56.32-132.864 125.888 0 69.504 59.52 125.824 132.864 125.824 73.408 0 132.928-56.32 132.928-125.824 0-69.504-59.52-125.888-132.928-125.888z" fill="#fff" p-id="1360"></path></svg>
\ No newline at end of file
...@@ -185,7 +185,6 @@ export const useApiSelect = (option: ApiSelectProps) => { ...@@ -185,7 +185,6 @@ export const useApiSelect = (option: ApiSelectProps) => {
</el-select> </el-select>
) )
} }
debugger
return ( return (
<el-select <el-select
class="w-1/1" class="w-1/1"
......
...@@ -16,3 +16,46 @@ export const localeProps = (t, prefix, rules) => { ...@@ -16,3 +16,46 @@ export const localeProps = (t, prefix, rules) => {
return rule return rule
}) })
} }
/**
* 解析表单组件的 field, title 等字段(递归,如果组件包含子组件)
*
* @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule
* @param fields 解析后表单组件字段
* @param parentTitle 如果是子表单,子表单的标题,默认为空
*/
export const parseFormFields = (
rule: Record<string, any>,
fields: Array<Record<string, any>> = [],
parentTitle: string = ''
) => {
const { type, field, $required, title: tempTitle, children } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
let required = false
if ($required) {
required = true
}
fields.push({
field,
title,
type,
required
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFields(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseFormFields(rule, fields)
})
}
}
<template> <template>
<div class="node-handler-wrapper"> <div class="node-handler-wrapper">
<div class="node-handler" v-if="props.showAdd"> <div class="node-handler">
<el-popover <el-popover
trigger="hover" trigger="hover"
v-model:visible="popoverShow" v-model:visible="popoverShow"
placement="right-start" placement="right-start"
width="auto" width="auto"
v-if="!readonly"
> >
<div class="handler-item-wrapper"> <div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)"> <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
...@@ -27,11 +28,17 @@ ...@@ -27,11 +28,17 @@
<div class="handler-item-text">条件分支</div> <div class="handler-item-text">条件分支</div>
</div> </div>
<div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)"> <div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
<div class="handler-item-icon condition"> <div class="handler-item-icon parallel">
<span class="iconfont icon-size icon-parallel"></span> <span class="iconfont icon-size icon-parallel"></span>
</div> </div>
<div class="handler-item-text">并行分支</div> <div class="handler-item-text">并行分支</div>
</div> </div>
<div class="handler-item" @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)">
<div class="handler-item-icon inclusive">
<span class="iconfont icon-size icon-inclusive"></span>
</div>
<div class="handler-item-text">包容分支</div>
</div>
</div> </div>
<template #reference> <template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div> <div class="add-icon"><Icon icon="ep:plus" /></div>
...@@ -56,23 +63,36 @@ import { generateUUID } from '@/utils' ...@@ -56,23 +63,36 @@ import { generateUUID } from '@/utils'
defineOptions({ defineOptions({
name: 'NodeHandler' name: 'NodeHandler'
}) })
const popoverShow = ref(false)
const message = useMessage() // 消息弹窗
const popoverShow = ref(false)
const props = defineProps({ const props = defineProps({
childNode: { childNode: {
type: Object as () => SimpleFlowNode, type: Object as () => SimpleFlowNode,
default: null default: null
}, },
showAdd: { currentNode: {
// 是否显示添加节点 type: Object as () => SimpleFlowNode,
type: Boolean, required: true
default: true
} }
}) })
const emits = defineEmits(['update:childNode']) const emits = defineEmits(['update:childNode'])
const readonly = inject<Boolean>('readonly') // 是否只读
const addNode = (type: number) => { const addNode = (type: number) => {
// 校验:条件分支、包容分支后面,不允许直接添加并行分支
if (
type === NodeType.PARALLEL_BRANCH_NODE &&
[NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes(
props.currentNode?.type
)
) {
message.error('条件分支、包容分支后面,不允许直接添加并行分支')
return
}
popoverShow.value = false popoverShow.value = false
if (type === NodeType.USER_TASK_NODE) { if (type === NodeType.USER_TASK_NODE) {
const id = 'Activity_' + generateUUID() const id = 'Activity_' + generateUUID()
...@@ -122,12 +142,11 @@ const addNode = (type: number) => { ...@@ -122,12 +142,11 @@ const addNode = (type: number) => {
childNode: undefined, childNode: undefined,
conditionType: 1, conditionType: 1,
defaultFlow: false defaultFlow: false
}, },
{ {
id: 'Flow_' + generateUUID(), id: 'Flow_' + generateUUID(),
name: '其它情况', name: '其它情况',
showText: '其它情况进入此流程', showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE, type: NodeType.CONDITION_NODE,
childNode: undefined, childNode: undefined,
conditionType: undefined, conditionType: undefined,
...@@ -162,6 +181,33 @@ const addNode = (type: number) => { ...@@ -162,6 +181,33 @@ const addNode = (type: number) => {
} }
emits('update:childNode', data) emits('update:childNode', data)
} }
if (type === NodeType.INCLUSIVE_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '包容分支',
type: NodeType.INCLUSIVE_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '包容条件1',
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
defaultFlow: false
},
{
id: 'Flow_' + generateUUID(),
name: '其它情况',
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
childNode: undefined,
defaultFlow: true
}
]
}
emits('update:childNode', data)
}
} }
</script> </script>
......
...@@ -31,6 +31,13 @@ ...@@ -31,6 +31,13 @@
@update:model-value="handleModelValueUpdate" @update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode" @find:parent-node="findFromParentNode"
/> />
<!-- 包容分支节点 -->
<InclusiveNode
v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 递归显示孩子节点 --> <!-- 递归显示孩子节点 -->
<ProcessNodeTree <ProcessNodeTree
v-if="currentNode && currentNode.childNode" v-if="currentNode && currentNode.childNode"
...@@ -40,7 +47,10 @@ ...@@ -40,7 +47,10 @@
/> />
<!-- 结束节点 --> <!-- 结束节点 -->
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" /> <EndEventNode
v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE"
:flow-node="currentNode"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import StartUserNode from './nodes/StartUserNode.vue' import StartUserNode from './nodes/StartUserNode.vue'
...@@ -49,6 +59,7 @@ import UserTaskNode from './nodes/UserTaskNode.vue' ...@@ -49,6 +59,7 @@ import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue' import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue' import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue' import ParallelNode from './nodes/ParallelNode.vue'
import InclusiveNode from './nodes/InclusiveNode.vue'
import { SimpleFlowNode, NodeType } from './consts' import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node' import { useWatchNode } from './node'
defineOptions({ defineOptions({
......
<template> <template>
<div class="simple-flow-canvas" v-loading="loading"> <div v-loading="loading" class="overflow-auto">
<div class="simple-flow-container"> <SimpleProcessModel
<div class="top-area-container"> v-if="processNodeTree"
<div class="top-actions"> :flow-node="processNodeTree"
<div class="canvas-control"> :readonly="false"
<span class="control-scale-group"> @save="saveSimpleFlowModel"
<span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span> />
<span class="control-scale-label">{{ scaleValue }}%</span>
<span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
</span>
</div>
<el-button type="primary" @click="saveSimpleFlowModel">保存</el-button>
<!-- <el-button type="primary">全局设置</el-button> -->
</div>
</div>
<div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
</div>
</div>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false"> <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
<div class="mb-2">以下节点内容不完善,请修改后保存</div> <div class="mb-2">以下节点内容不完善,请修改后保存</div>
<div <div
...@@ -35,7 +23,7 @@ ...@@ -35,7 +23,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue' import SimpleProcessModel from './SimpleProcessModel.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple' import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts' import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model' import { getModel } from '@/api/bpm/model'
...@@ -50,14 +38,16 @@ import * as UserGroupApi from '@/api/bpm/userGroup' ...@@ -50,14 +38,16 @@ import * as UserGroupApi from '@/api/bpm/userGroup'
defineOptions({ defineOptions({
name: 'SimpleProcessDesigner' name: 'SimpleProcessDesigner'
}) })
const router = useRouter() // 路由 const emits = defineEmits(['success']) // 保存成功事件
const props = defineProps({ const props = defineProps({
modelId: { modelId: {
type: String, type: String,
required: true required: true
} }
}) })
const loading = ref(true)
const loading = ref(false)
const formFields = ref<string[]>([]) const formFields = ref<string[]>([])
const formType = ref(20) const formType = ref(20)
const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
...@@ -79,29 +69,27 @@ const message = useMessage() // 国际化 ...@@ -79,29 +69,27 @@ const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>() const processNodeTree = ref<SimpleFlowNode | undefined>()
const errorDialogVisible = ref(false) const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = [] let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => { const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
if (!props.modelId) { if (!simpleModelNode) {
message.error('缺少模型 modelId 编号') message.error('模型数据为空')
return
}
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return return
} }
try {
loading.value = true
const data = { const data = {
id: props.modelId, id: props.modelId,
simpleModel: processNodeTree.value simpleModel: simpleModelNode
} }
const result = await updateBpmSimpleModel(data) const result = await updateBpmSimpleModel(data)
if (result) { if (result) {
message.success('修改成功') message.success('修改成功')
close() emits('success')
} else { } else {
message.alert('修改失败') message.alert('修改失败')
} }
} finally {
loading.value = false
}
} }
// 校验节点设置。 暂时以 showText 为空 未节点错误配置 // 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
...@@ -111,58 +99,37 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo ...@@ -111,58 +99,37 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
return return
} }
if (type == NodeType.START_USER_NODE) { if (type == NodeType.START_USER_NODE) {
// 发起人节点暂时不用校验,直接校验孩子节点
validateNode(node.childNode, errorNodes) validateNode(node.childNode, errorNodes)
} }
if (type === NodeType.USER_TASK_NODE) { if (
if (!showText) { type === NodeType.USER_TASK_NODE ||
errorNodes.push(node) type === NodeType.COPY_TASK_NODE ||
} type === NodeType.CONDITION_NODE
validateNode(node.childNode, errorNodes) ) {
}
if (type === NodeType.COPY_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.CONDITION_NODE) {
if (!showText) { if (!showText) {
errorNodes.push(node) errorNodes.push(node)
} }
validateNode(node.childNode, errorNodes) validateNode(node.childNode, errorNodes)
} }
if (type == NodeType.CONDITION_BRANCH_NODE) { if (
type == NodeType.CONDITION_BRANCH_NODE ||
type == NodeType.PARALLEL_BRANCH_NODE ||
type == NodeType.INCLUSIVE_BRANCH_NODE
) {
// 分支节点
// 1. 先校验各个分支
conditionNodes?.forEach((item) => { conditionNodes?.forEach((item) => {
validateNode(item, errorNodes) validateNode(item, errorNodes)
}) })
// 2. 校验孩子节点
validateNode(node.childNode, errorNodes) validateNode(node.childNode, errorNodes)
} }
} }
} }
const close = () => {
router.push({ path: '/bpm/manager/model' })
}
let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50
// 放大
const zoomOut = () => {
if (scaleValue.value == MAX_SCALE_VALUE) {
return
}
scaleValue.value += 10
}
// 缩小
const zoomIn = () => {
if (scaleValue.value == MIN_SCALE_VALUE) {
return
}
scaleValue.value -= 10
}
onMounted(async () => { onMounted(async () => {
try { try {
loading.value = true loading.value = true
...@@ -188,7 +155,7 @@ onMounted(async () => { ...@@ -188,7 +155,7 @@ onMounted(async () => {
// 获取用户组列表 // 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList() userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// 获取 SIMPLE 设计器模型 //获取 SIMPLE 设计器模型
const result = await getBpmSimpleModel(props.modelId) const result = await getBpmSimpleModel(props.modelId)
if (result) { if (result) {
processNodeTree.value = result processNodeTree.value = result
......
<template>
<div class="simple-process-model-container position-relative">
<div class="position-absolute top-0px right-0px bg-#fff">
<el-row type="flex" justify="end">
<el-button-group key="scale-control" size="default">
<el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
<el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
<el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
<el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
</el-button-group>
<el-button
v-if="!readonly"
size="default"
class="ml-4px"
type="primary"
:icon="Select"
@click="saveSimpleFlowModel"
>保存模型</el-button
>
</el-row>
</div>
<div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
</div>
</div>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
<div class="mb-2">以下节点内容不完善,请修改后保存</div>
<div
class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
v-for="(item, index) in errorNodes"
:key="index"
>
{{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
</div>
<template #footer>
<el-button type="primary" @click="errorDialogVisible = false">知道了</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
import { useWatchNode } from './node'
import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
defineOptions({
name: 'SimpleProcessModel'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
},
readonly: {
type: Boolean,
required: false,
default: true
}
})
const emits = defineEmits<{
'save': [node: SimpleFlowNode | undefined]
}>()
const processNodeTree = useWatchNode(props)
provide('readonly', props.readonly)
let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50
// 放大
const zoomIn = () => {
if (scaleValue.value == MAX_SCALE_VALUE) {
return
}
scaleValue.value += 10
}
// 缩小
const zoomOut = () => {
if (scaleValue.value == MIN_SCALE_VALUE) {
return
}
scaleValue.value -= 10
}
const processReZoom = () => {
scaleValue.value = 100
}
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
emits('save', processNodeTree.value)
}
// 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) {
const { type, showText, conditionNodes } = node
if (type == NodeType.END_EVENT_NODE) {
return
}
if (type == NodeType.START_USER_NODE) {
// 发起人节点暂时不用校验,直接校验孩子节点
validateNode(node.childNode, errorNodes)
}
if (
type === NodeType.USER_TASK_NODE ||
type === NodeType.COPY_TASK_NODE ||
type === NodeType.CONDITION_NODE
) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (
type == NodeType.CONDITION_BRANCH_NODE ||
type == NodeType.PARALLEL_BRANCH_NODE ||
type == NodeType.INCLUSIVE_BRANCH_NODE
) {
// 分支节点
// 1. 先校验各个分支
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
})
// 2. 校验孩子节点
validateNode(node.childNode, errorNodes)
}
}
}
</script>
<style lang="scss" scoped></style>
<template>
<SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
</template>
<script setup lang="ts">
import { useWatchNode } from './node'
import { SimpleFlowNode } from './consts'
defineOptions({
name: 'SimpleProcessViewer'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
},
// 流程任务
tasks: {
type: Array,
default: () => [] as any[]
},
// 流程实例
processInstance: {
type: Object,
default: () => undefined
}
})
const approveTasks = ref<any[]>(props.tasks)
const currentProcessInstance = ref(props.processInstance)
const simpleModel = useWatchNode(props)
watch(
() => props.tasks,
(newValue) => {
approveTasks.value = newValue
}
)
watch(
() => props.processInstance,
(newValue) => {
currentProcessInstance.value = newValue
}
)
provide('tasks', approveTasks)
provide('processInstance', currentProcessInstance)
</script>
p
// @ts-ignore // @ts-ignore
import { DictDataVO } from '@/api/system/dict/types' import { DictDataVO } from '@/api/system/dict/types'
import { TaskStatusEnum } from '@/api/bpm/task'
/** /**
* 节点类型 * 节点类型
*/ */
...@@ -79,7 +79,7 @@ export interface SimpleFlowNode { ...@@ -79,7 +79,7 @@ export interface SimpleFlowNode {
// 审批按钮设置 // 审批按钮设置
buttonsSetting?: any[] buttonsSetting?: any[]
// 表单权限 // 表单权限
fieldsPermission?: Array<Record<string, string>> fieldsPermission?: Array<Record<string, any>>
// 审批任务超时处理 // 审批任务超时处理
timeoutHandler?: TimeoutHandler timeoutHandler?: TimeoutHandler
// 审批任务拒绝处理 // 审批任务拒绝处理
...@@ -96,7 +96,8 @@ export interface SimpleFlowNode { ...@@ -96,7 +96,8 @@ export interface SimpleFlowNode {
conditionGroups?: ConditionGroup conditionGroups?: ConditionGroup
// 是否默认的条件 // 是否默认的条件
defaultFlow?: boolean defaultFlow?: boolean
// 活动的状态,用于前端节点状态展示
activityStatus?: TaskStatusEnum
} }
// 候选人策略枚举 ( 用于审批节点。抄送节点 ) // 候选人策略枚举 ( 用于审批节点。抄送节点 )
export enum CandidateStrategy { export enum CandidateStrategy {
...@@ -145,6 +146,14 @@ export enum CandidateStrategy { ...@@ -145,6 +146,14 @@ export enum CandidateStrategy {
*/ */
USER_GROUP = 40, USER_GROUP = 40,
/** /**
* 表单内用户字段
*/
FORM_USER = 50,
/**
* 表单内部门负责人
*/
FORM_DEPT_LEADER = 51,
/**
* 流程表达式 * 流程表达式
*/ */
EXPRESSION = 60 EXPRESSION = 60
...@@ -178,7 +187,7 @@ export enum ApproveMethodType { ...@@ -178,7 +187,7 @@ export enum ApproveMethodType {
export type RejectHandler = { export type RejectHandler = {
// 审批拒绝类型 // 审批拒绝类型
type: RejectHandlerType type: RejectHandlerType
// 回退节点 Id // 退回节点 Id
returnNodeId?: string returnNodeId?: string
} }
...@@ -360,9 +369,13 @@ export enum OperationButtonType { ...@@ -360,9 +369,13 @@ export enum OperationButtonType {
*/ */
ADD_SIGN = 5, ADD_SIGN = 5,
/** /**
* 回退 * 退回
*/
RETURN = 6,
/**
* 抄送
*/ */
RETURN = 6 COPY = 7
} }
/** /**
...@@ -419,6 +432,8 @@ export const CANDIDATE_STRATEGY: DictDataVO[] = [ ...@@ -419,6 +432,8 @@ export const CANDIDATE_STRATEGY: DictDataVO[] = [
{ label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER }, { label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
{ label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER }, { label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
{ label: '用户组', value: CandidateStrategy.USER_GROUP }, { label: '用户组', value: CandidateStrategy.USER_GROUP },
{ label: '表单内用户字段', value: CandidateStrategy.FORM_USER },
{ label: '表单内部门负责人', value: CandidateStrategy.FORM_DEPT_LEADER },
{ label: '流程表达式', value: CandidateStrategy.EXPRESSION } { label: '流程表达式', value: CandidateStrategy.EXPRESSION }
] ]
// 审批节点 的审批类型 // 审批节点 的审批类型
...@@ -503,16 +518,17 @@ OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝') ...@@ -503,16 +518,17 @@ OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝')
OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办') OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办')
OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派') OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派')
OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签') OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签')
OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '回退') OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退回')
OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送')
// 默认的按钮权限设置 // 默认的按钮权限设置
export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [ export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.APPROVE, displayName: '通过', enable: true }, { id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
{ id: OperationButtonType.REJECT, displayName: '拒绝', enable: true }, { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false }, { id: OperationButtonType.TRANSFER, displayName: '转办', enable: true },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false }, { id: OperationButtonType.DELEGATE, displayName: '委派', enable: true },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false }, { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true },
{ id: OperationButtonType.RETURN, displayName: '回退', enable: false } { id: OperationButtonType.RETURN, displayName: '退回', enable: true }
] ]
// 发起人的按钮权限。暂时定死,不可以编辑 // 发起人的按钮权限。暂时定死,不可以编辑
...@@ -522,7 +538,7 @@ export const START_USER_BUTTON_SETTING: ButtonSetting[] = [ ...@@ -522,7 +538,7 @@ export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false }, { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false }, { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false }, { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
{ id: OperationButtonType.RETURN, displayName: '回退', enable: false } { id: OperationButtonType.RETURN, displayName: '退回', enable: false }
] ]
export const MULTI_LEVEL_DEPT: DictDataVO = [ export const MULTI_LEVEL_DEPT: DictDataVO = [
...@@ -542,3 +558,13 @@ export const MULTI_LEVEL_DEPT: DictDataVO = [ ...@@ -542,3 +558,13 @@ export const MULTI_LEVEL_DEPT: DictDataVO = [
{ label: '第 14 级部门', value: 14 }, { label: '第 14 级部门', value: 14 },
{ label: '第 15 级部门', value: 15 } { label: '第 15 级部门', value: 15 }
] ]
/**
* 流程实例的变量枚举
*/
export enum ProcessVariableEnum {
/**
* 发起用户 ID
*/
START_USER_ID = 'PROCESS_START_USER_ID'
}
import SimpleProcessDesigner from './SimpleProcessDesigner.vue' import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
import SimpleProcessViewer from './SimpleProcessViewer.vue'
import '../theme/simple-process-designer.scss' import '../theme/simple-process-designer.scss'
export { SimpleProcessDesigner } export { SimpleProcessDesigner, SimpleProcessViewer}
\ No newline at end of file
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { TaskStatusEnum } from '@/api/bpm/task'
import * as RoleApi from '@/api/system/role' import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept' import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post' import * as PostApi from '@/api/system/post'
...@@ -13,8 +14,10 @@ import { ...@@ -13,8 +14,10 @@ import {
NODE_DEFAULT_NAME, NODE_DEFAULT_NAME,
AssignStartUserHandlerType, AssignStartUserHandlerType,
AssignEmptyHandlerType, AssignEmptyHandlerType,
FieldPermissionType FieldPermissionType,
ProcessVariableEnum
} from './consts' } from './consts'
import { parseFormFields } from '@/components/FormCreate/src/utils/index'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
const node = ref<SimpleFlowNode>(props.flowNode) const node = ref<SimpleFlowNode>(props.flowNode)
watch( watch(
...@@ -26,12 +29,30 @@ export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlo ...@@ -26,12 +29,30 @@ export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlo
return node return node
} }
// 解析 formCreate 所有表单字段, 并返回
const parseFormCreateFields = (formFields?: string[]) => {
const result: Array<Record<string, any>> = []
if (formFields) {
formFields.forEach((fieldStr: string) => {
parseFormFields(JSON.parse(fieldStr), result)
})
}
// 固定添加发起人 ID 字段
result.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
type: 'UserSelect',
required: true
})
return result
}
/** /**
* @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点 * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
*/ */
export function useFormFieldsPermission(defaultPermission: FieldPermissionType) { export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
// 字段权限配置. 需要有 field, title, permissioin 属性 // 字段权限配置. 需要有 field, title, permissioin 属性
const fieldsPermissionConfig = ref<Array<Record<string, string>>>([]) const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
const formType = inject<Ref<number>>('formType') // 表单类型 const formType = inject<Ref<number>>('formType') // 表单类型
...@@ -44,49 +65,26 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType) ...@@ -44,49 +65,26 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
} }
// 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读 // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
const getDefaultFieldsPermission = (formFields?: string[]) => { const getDefaultFieldsPermission = (formFields?: string[]) => {
const defaultFieldsPermission: Array<Record<string, string>> = [] let defaultFieldsPermission: Array<Record<string, any>> = []
if (formFields) { if (formFields) {
formFields.forEach((fieldStr: string) => { defaultFieldsPermission = parseFormCreateFields(formFields).map((item) => {
parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission) return {
}) field: item.field,
} title: item.title,
return defaultFieldsPermission
}
// 解析字段。赋给默认权限
const parseFieldsSetDefaultPermission = (
rule: Record<string, any>,
fieldsPermission: Array<Record<string, string>>,
parentTitle: string = ''
) => {
const { /**type,*/ field, title: tempTitle, children } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
fieldsPermission.push({
field,
title,
permission: defaultPermission permission: defaultPermission
}) }
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFieldsSetDefaultPermission(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseFieldsSetDefaultPermission(rule, fieldsPermission)
}) })
} }
return defaultFieldsPermission
} }
// 获取表单的所有字段,作为下拉框选项
const formFieldOptions = parseFormCreateFields(unref(formFields))
return { return {
formType, formType,
fieldsPermissionConfig, fieldsPermissionConfig,
formFieldOptions,
getNodeConfigFormFields getNodeConfigFormFields
} }
} }
...@@ -94,50 +92,8 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType) ...@@ -94,50 +92,8 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
* @description 获取表单的字段 * @description 获取表单的字段
*/ */
export function useFormFields() { export function useFormFields() {
// 解析后的表单字段
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段 const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const parseFormFields = () => { return parseFormCreateFields(unref(formFields))
const parsedFormFields: Array<Record<string, string>> = []
if (formFields) {
formFields.value.forEach((fieldStr: string) => {
parseField(JSON.parse(fieldStr), parsedFormFields)
})
}
return parsedFormFields
}
// 解析字段。
const parseField = (
rule: Record<string, any>,
parsedFormFields: Array<Record<string, string>>,
parentTitle: string = ''
) => {
const { field, title: tempTitle, children, type } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
parsedFormFields.push({
field,
title,
type
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFieldsSetDefaultPermission(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseField(rule, parsedFormFields)
})
}
}
return parseFormFields()
} }
export type UserTaskFormType = { export type UserTaskFormType = {
...@@ -151,6 +107,8 @@ export type UserTaskFormType = { ...@@ -151,6 +107,8 @@ export type UserTaskFormType = {
userGroups?: number[] // 用户组 userGroups?: number[] // 用户组
postIds?: number[] // 岗位 postIds?: number[] // 岗位
expression?: string // 流程表达式 expression?: string // 流程表达式
formUser?: string // 表单内用户字段
formDept?: string // 表单内部门字段
approveRatio?: number approveRatio?: number
rejectHandlerType?: RejectHandlerType rejectHandlerType?: RejectHandlerType
returnNodeId?: string returnNodeId?: string
...@@ -173,6 +131,8 @@ export type CopyTaskFormType = { ...@@ -173,6 +131,8 @@ export type CopyTaskFormType = {
userIds?: number[] // 用户 userIds?: number[] // 用户
userGroups?: number[] // 用户组 userGroups?: number[] // 用户组
postIds?: number[] // 岗位 postIds?: number[] // 岗位
formUser?: string // 表单内用户字段
formDept?: string // 表单内部门字段
expression?: string // 流程表达式 expression?: string // 流程表达式
} }
...@@ -186,6 +146,7 @@ export function useNodeForm(nodeType: NodeType) { ...@@ -186,6 +146,7 @@ export function useNodeForm(nodeType: NodeType) {
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表 const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表 const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
const deptTreeOptions = inject('deptTree') // 部门树 const deptTreeOptions = inject('deptTree') // 部门树
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const configForm = ref<UserTaskFormType | CopyTaskFormType>() const configForm = ref<UserTaskFormType | CopyTaskFormType>()
if (nodeType === NodeType.USER_TASK_NODE) { if (nodeType === NodeType.USER_TASK_NODE) {
configForm.value = { configForm.value = {
...@@ -281,6 +242,18 @@ export function useNodeForm(nodeType: NodeType) { ...@@ -281,6 +242,18 @@ export function useNodeForm(nodeType: NodeType) {
} }
} }
// 表单内用户字段
if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) {
const formFieldOptions = parseFormCreateFields(unref(formFields))
const item = formFieldOptions.find((item) => item.field === configForm.value?.formUser)
showText = `表单用户:${item?.title}`
}
// 表单内部门负责人
if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER) {
showText = `表单内部门负责人`
}
// 发起人自选 // 发起人自选
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) { if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
showText = `发起人自选` showText = `发起人自选`
...@@ -327,6 +300,9 @@ export function useNodeForm(nodeType: NodeType) { ...@@ -327,6 +300,9 @@ export function useNodeForm(nodeType: NodeType) {
case CandidateStrategy.USER_GROUP: case CandidateStrategy.USER_GROUP:
candidateParam = configForm.value.userGroups!.join(',') candidateParam = configForm.value.userGroups!.join(',')
break break
case CandidateStrategy.FORM_USER:
candidateParam = configForm.value.formUser!
break
case CandidateStrategy.EXPRESSION: case CandidateStrategy.EXPRESSION:
candidateParam = configForm.value.expression! candidateParam = configForm.value.expression!
break break
...@@ -346,6 +322,13 @@ export function useNodeForm(nodeType: NodeType) { ...@@ -346,6 +322,13 @@ export function useNodeForm(nodeType: NodeType) {
candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '') candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
break break
} }
// 表单内部门的负责人
case CandidateStrategy.FORM_DEPT_LEADER: {
// 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级
const deptFieldOnForm = configForm.value.formDept!
candidateParam = deptFieldOnForm.concat('|' + configForm.value.deptLevel + '')
break
}
default: default:
break break
} }
...@@ -375,6 +358,9 @@ export function useNodeForm(nodeType: NodeType) { ...@@ -375,6 +358,9 @@ export function useNodeForm(nodeType: NodeType) {
case CandidateStrategy.USER_GROUP: case CandidateStrategy.USER_GROUP:
configForm.value.userGroups = candidateParam.split(',').map((item) => +item) configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
break break
case CandidateStrategy.FORM_USER:
configForm.value.formUser = candidateParam
break
case CandidateStrategy.EXPRESSION: case CandidateStrategy.EXPRESSION:
configForm.value.expression = candidateParam configForm.value.expression = candidateParam
break break
...@@ -395,6 +381,14 @@ export function useNodeForm(nodeType: NodeType) { ...@@ -395,6 +381,14 @@ export function useNodeForm(nodeType: NodeType) {
configForm.value.deptLevel = +paramArray[1] configForm.value.deptLevel = +paramArray[1]
break break
} }
// 表单内的部门负责人
case CandidateStrategy.FORM_DEPT_LEADER: {
// 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级
const paramArray = candidateParam.split('|')
configForm.value.formDept = paramArray[0]
configForm.value.deptLevel = +paramArray[1]
break
}
default: default:
break break
} }
...@@ -476,3 +470,26 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) { ...@@ -476,3 +470,26 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
blurEvent blurEvent
} }
} }
/**
* @description 根据节点任务状态,获取节点任务状态样式
*/
export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): string {
if (!taskStatus) {
return ''
}
if (taskStatus === TaskStatusEnum.APPROVE) {
return 'status-pass'
}
if (taskStatus === TaskStatusEnum.RUNNING) {
return 'status-running'
}
if (taskStatus === TaskStatusEnum.REJECT) {
return 'status-reject'
}
if (taskStatus === TaskStatusEnum.CANCEL) {
return 'status-cancel'
}
return ''
}
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
</div> </div>
</template> </template>
<div> <div>
<div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">其它条件不满足进入此分支(该分支不可编辑和删除)</div> <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div>
<div v-else> <div v-else>
<el-form <el-form
ref="formRef" ref="formRef"
......
...@@ -60,7 +60,8 @@ ...@@ -60,7 +60,8 @@
<el-form-item <el-form-item
v-if=" v-if="
configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER || configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
" "
label="指定部门" label="指定部门"
prop="deptIds" prop="deptIds"
...@@ -122,7 +123,57 @@ ...@@ -122,7 +123,57 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
label="表单内用户字段"
prop="formUser"
>
<el-select v-model="configForm.formUser" clearable style="width: 100%">
<el-option
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
label="表单内部门字段"
prop="formDept"
>
<el-select v-model="configForm.formDept" clearable style="width: 100%">
<el-option
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy ==
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item <el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION" v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式" label="流程表达式"
...@@ -201,7 +252,8 @@ import { ...@@ -201,7 +252,8 @@ import {
CandidateStrategy, CandidateStrategy,
NodeType, NodeType,
CANDIDATE_STRATEGY, CANDIDATE_STRATEGY,
FieldPermissionType FieldPermissionType,
MULTI_LEVEL_DEPT
} from '../consts' } from '../consts'
import { import {
useWatchNode, useWatchNode,
...@@ -221,6 +273,15 @@ const props = defineProps({ ...@@ -221,6 +273,15 @@ const props = defineProps({
required: true required: true
} }
}) })
const deptLevelLabel = computed(() => {
let label = '部门负责人来源'
if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
label = label + '(指定部门向上)'
} else {
label = label + '(发起人部门向上)'
}
return label
})
// 抽屉配置 // 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer() const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点 // 当前节点
...@@ -230,9 +291,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_ ...@@ -230,9 +291,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_
// 激活的 Tab 标签页 // 激活的 Tab 标签页
const activeTabName = ref('user') const activeTabName = ref('user')
// 表单字段权限配置 // 表单字段权限配置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
FieldPermissionType.READ useFormFieldsPermission(FieldPermissionType.READ)
) // 表单内用户字段选项, 必须是必填和用户选择器
const userFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'UserSelect')
})
// 表单内部门字段选项, 必须是必填和部门选择器
const deptFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'DeptSelect')
})
// 抄送人表单配置 // 抄送人表单配置
const formRef = ref() // 表单 Ref const formRef = ref() // 表单 Ref
// 表单校验规则 // 表单校验规则
...@@ -243,6 +311,8 @@ const formRules = reactive({ ...@@ -243,6 +311,8 @@ const formRules = reactive({
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }], deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }], userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }], postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }] expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }]
}) })
...@@ -260,11 +330,7 @@ const { ...@@ -260,11 +330,7 @@ const {
const configForm = tempConfigForm as Ref<CopyTaskFormType> const configForm = tempConfigForm as Ref<CopyTaskFormType>
// 抄送人策略, 去掉发起人自选 和 发起人自己 // 抄送人策略, 去掉发起人自选 和 发起人自己
const copyUserStrategies = computed(() => { const copyUserStrategies = computed(() => {
return CANDIDATE_STRATEGY.filter( return CANDIDATE_STRATEGY.filter((item) => item.value !== CandidateStrategy.START_USER)
(item) =>
item.value !== CandidateStrategy.START_USER_SELECT &&
item.value !== CandidateStrategy.START_USER
)
}) })
// 改变抄送人设置策略 // 改变抄送人设置策略
const changeCandidateStrategy = () => { const changeCandidateStrategy = () => {
...@@ -274,6 +340,7 @@ const changeCandidateStrategy = () => { ...@@ -274,6 +340,7 @@ const changeCandidateStrategy = () => {
configForm.value.postIds = [] configForm.value.postIds = []
configForm.value.userGroups = [] configForm.value.userGroups = []
configForm.value.deptLevel = 1 configForm.value.deptLevel = 1
configForm.value.formUser = ''
} }
// 保存配置 // 保存配置
const saveConfig = async () => { const saveConfig = async () => {
......
...@@ -119,7 +119,6 @@ const saveConfig = async () => { ...@@ -119,7 +119,6 @@ const saveConfig = async () => {
currentNode.value.fieldsPermission = fieldsPermissionConfig.value currentNode.value.fieldsPermission = fieldsPermissionConfig.value
// 设置发起人的按钮权限 // 设置发起人的按钮权限
currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
settingVisible.value = false settingVisible.value = false
return true return true
} }
......
...@@ -56,7 +56,6 @@ ...@@ -56,7 +56,6 @@
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.ROLE" v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
label="指定角色" label="指定角色"
...@@ -95,25 +94,6 @@ ...@@ -95,25 +94,6 @@
/> />
</el-form-item> </el-form-item>
<el-form-item <el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.POST" v-if="configForm.candidateStrategy == CandidateStrategy.POST"
label="指定岗位" label="指定岗位"
prop="postIds" prop="postIds"
...@@ -134,13 +114,7 @@ ...@@ -134,13 +114,7 @@
prop="userIds" prop="userIds"
span="24" span="24"
> >
<el-select <el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
v-model="configForm.userIds"
clearable
multiple
style="width: 100%"
@change="changedCandidateUsers"
>
<el-option <el-option
v-for="item in userOptions" v-for="item in userOptions"
:key="item.id" :key="item.id"
...@@ -163,6 +137,57 @@ ...@@ -163,6 +137,57 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
label="表单内用户字段"
prop="formUser"
>
<el-select v-model="configForm.formUser" clearable style="width: 100%">
<el-option
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
label="表单内部门字段"
prop="formDept"
>
<el-select v-model="configForm.formDept" clearable style="width: 100%">
<el-option
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy ==
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- TODO @jason:后续要支持选择已经存好的表达式 --> <!-- TODO @jason:后续要支持选择已经存好的表达式 -->
<el-form-item <el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION" v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
...@@ -184,14 +209,7 @@ ...@@ -184,14 +209,7 @@
:key="index" :key="index"
class="flex items-center" class="flex items-center"
> >
<el-radio <el-radio :value="item.value" :label="item.value">
:value="item.value"
:label="item.value"
:disabled="
item.value !== ApproveMethodType.RANDOM_SELECT_ONE_APPROVE &&
notAllowedMultiApprovers
"
>
{{ item.label }} {{ item.label }}
</el-radio> </el-radio>
<el-form-item prop="approveRatio"> <el-form-item prop="approveRatio">
...@@ -481,6 +499,8 @@ const deptLevelLabel = computed(() => { ...@@ -481,6 +499,8 @@ const deptLevelLabel = computed(() => {
let label = '部门负责人来源' let label = '部门负责人来源'
if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
label = label + '(指定部门向上)' label = label + '(指定部门向上)'
} else if (configForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
label = label + '(表单内部门向上)'
} else { } else {
label = label + '(发起人部门向上)' label = label + '(发起人部门向上)'
} }
...@@ -495,9 +515,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_ ...@@ -495,9 +515,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_
// 激活的 Tab 标签页 // 激活的 Tab 标签页
const activeTabName = ref('user') const activeTabName = ref('user')
// 表单字段权限设置 // 表单字段权限设置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
FieldPermissionType.READ useFormFieldsPermission(FieldPermissionType.READ)
) // 表单内用户字段选项, 必须是必填和用户选择器
const userFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'UserSelect')
})
// 表单内部门字段选项, 必须是必填和部门选择器
const deptFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'DeptSelect')
})
// 操作按钮设置 // 操作按钮设置
const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } = const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting() useButtonsSetting()
...@@ -511,6 +538,8 @@ const formRules = reactive({ ...@@ -511,6 +538,8 @@ const formRules = reactive({
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }], roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }], deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }], userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }], postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }], expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }],
approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }], approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
...@@ -537,8 +566,7 @@ const { ...@@ -537,8 +566,7 @@ const {
getShowText getShowText
} = useNodeForm(NodeType.USER_TASK_NODE) } = useNodeForm(NodeType.USER_TASK_NODE)
const configForm = tempConfigForm as Ref<UserTaskFormType> const configForm = tempConfigForm as Ref<UserTaskFormType>
// 不允许多人审批
const notAllowedMultiApprovers = ref(false)
// 改变审批人设置策略 // 改变审批人设置策略
const changeCandidateStrategy = () => { const changeCandidateStrategy = () => {
configForm.value.userIds = [] configForm.value.userIds = []
...@@ -547,30 +575,11 @@ const changeCandidateStrategy = () => { ...@@ -547,30 +575,11 @@ const changeCandidateStrategy = () => {
configForm.value.postIds = [] configForm.value.postIds = []
configForm.value.userGroups = [] configForm.value.userGroups = []
configForm.value.deptLevel = 1 configForm.value.deptLevel = 1
configForm.value.formUser = ''
configForm.value.formDept = ''
configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
if (
configForm.value.candidateStrategy === CandidateStrategy.START_USER ||
configForm.value.candidateStrategy === CandidateStrategy.USER
) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
}
// 改变审批候选人
const changedCandidateUsers = () => {
if (
configForm.value.userIds &&
configForm.value.userIds?.length <= 1 &&
configForm.value.candidateStrategy === CandidateStrategy.USER
) {
configForm.value.approveMethod = ApproveMethodType.RANDOM_SELECT_ONE_APPROVE
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
} }
// 审批方式改变 // 审批方式改变
const approveMethodChanged = () => { const approveMethodChanged = () => {
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
...@@ -579,7 +588,7 @@ const approveMethodChanged = () => { ...@@ -579,7 +588,7 @@ const approveMethodChanged = () => {
} }
formRef.value.clearValidate('approveRatio') formRef.value.clearValidate('approveRatio')
} }
// 审批拒绝 可回退的节点 // 审批拒绝 可退回的节点
const returnTaskList = ref<SimpleFlowNode[]>([]) const returnTaskList = ref<SimpleFlowNode[]>([])
// 审批人超时未处理设置 // 审批人超时未处理设置
const { const {
...@@ -666,11 +675,6 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => { ...@@ -666,11 +675,6 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
configForm.value.candidateStrategy = node.candidateStrategy! configForm.value.candidateStrategy = node.candidateStrategy!
// 解析候选人参数 // 解析候选人参数
parseCandidateParam(node.candidateStrategy!, node?.candidateParam) parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
if (configForm.value.userIds && configForm.value.userIds.length > 1) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
// 2.2 设置审批方式 // 2.2 设置审批方式
configForm.value.approveMethod = node.approveMethod! configForm.value.approveMethod = node.approveMethod!
if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) { if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {
......
<template> <template>
<div class="node-wrapper"> <div class="node-wrapper">
<div class="node-container"> <div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }"> <div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container"> <div class="node-title-container">
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div> <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input <input
v-if="showInput" v-if="!readonly && showInput"
type="text" type="text"
class="editable-title-input" class="editable-title-input"
@blur="blurEvent()" @blur="blurEvent()"
...@@ -24,9 +30,9 @@ ...@@ -24,9 +30,9 @@
<div class="node-text" v-else> <div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }} {{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
</div> </div>
<Icon icon="ep:arrow-right-bold" /> <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div> </div>
<div class="node-toolbar"> <div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon" <div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div> /></div>
...@@ -34,15 +40,23 @@ ...@@ -34,15 +40,23 @@
</div> </div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" /> <NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div> </div>
<CopyTaskNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" /> <CopyTaskNodeConfig
v-if="!readonly && currentNode"
ref="nodeSetting"
:flow-node="currentNode"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue' import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode } from '../node' import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue' import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
defineOptions({ defineOptions({
name: 'CopyTaskNode' name: 'CopyTaskNode'
...@@ -57,7 +71,8 @@ const props = defineProps({ ...@@ -57,7 +71,8 @@ const props = defineProps({
const emits = defineEmits<{ const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined] 'update:flowNode': [node: SimpleFlowNode | undefined]
}>() }>()
// 是否只读
const readonly = inject<Boolean>('readonly')
// 监控节点的变化 // 监控节点的变化
const currentNode = useWatchNode(props) const currentNode = useWatchNode(props)
// 节点名称编辑 // 节点名称编辑
...@@ -66,6 +81,9 @@ const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType. ...@@ -66,6 +81,9 @@ const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.
const nodeSetting = ref() const nodeSetting = ref()
// 打开节点配置 // 打开节点配置
const openNodeConfig = () => { const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value) nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer() nodeSetting.value.openDrawer()
} }
......
<template> <template>
<div class="end-node-wrapper"> <div class="end-node-wrapper">
<div class="end-node-box"> <div class="end-node-box cursor-pointer" :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" @click="nodeClick">
<span class="node-fixed-name" title="结束">结束</span> <span class="node-fixed-name" title="结束">结束</span>
</div> </div>
</div> </div>
<el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body>
<el-row>
<el-table
:data="processInstanceInfos"
size="small"
border
header-cell-class-name="table-header-gray"
>
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column
label="发起人"
prop="assigneeUser.nickname"
min-width="100"
align="center"
/>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SimpleFlowNode } from '../consts'
import { useWatchNode, useTaskStatusClass } from '../node'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({ defineOptions({
name: 'EndEventNode' name: 'EndEventNode'
}) })
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
// 监控节点变化
const currentNode = useWatchNode(props)
// 是否只读
const readonly = inject<Boolean>('readonly')
const processInstance = inject<Ref<any>>('processInstance')
// 审批信息的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const processInstanceInfos = ref<any[]>([]) // 流程的审批信息
const nodeClick = () => {
if (readonly) {
if(processInstance && processInstance.value){
processInstanceInfos.value = [
{
assigneeUser: processInstance.value.startUser,
createTime: processInstance.value.startTime,
endTime: processInstance.value.endTime,
status: processInstance.value.status,
durationInMillis: processInstance.value.durationInMillis
}
]
dialogVisible.value = true
}
}
}
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
<template> <template>
<div class="branch-node-wrapper"> <div class="branch-node-wrapper">
<div class="branch-node-container"> <div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加条件</div> <div
v-if="readonly"
class="branch-node-readonly"
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
>
<span class="iconfont icon-exclusive icon-size condition"></span>
</div>
<el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain
>添加条件</el-button
>
<div <div
class="branch-node-item" class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes" v-for="(item, index) in currentNode.conditionNodes"
...@@ -17,9 +27,15 @@ ...@@ -17,9 +27,15 @@
</template> </template>
<div class="node-wrapper"> <div class="node-wrapper">
<div class="node-container"> <div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !item.showText }"> <div
class="node-box"
:class="[
{ 'node-config-error': !item.showText },
`${useTaskStatusClass(item.activityStatus)}`
]"
>
<div class="branch-node-title-container"> <div class="branch-node-title-container">
<div v-if="showInputs[index]"> <div v-if="!readonly && showInputs[index]">
<input <input
type="text" type="text"
class="input-max-width editable-title-input" class="input-max-width editable-title-input"
...@@ -39,7 +55,10 @@ ...@@ -39,7 +55,10 @@
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }} {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div> </div>
</div> </div>
<div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length"> <div
class="node-toolbar"
v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
>
<div class="toolbar-icon"> <div class="toolbar-icon">
<Icon <Icon
color="#0089ff" color="#0089ff"
...@@ -65,7 +84,7 @@ ...@@ -65,7 +84,7 @@
<Icon icon="ep:arrow-right" /> <Icon icon="ep:arrow-right" />
</div> </div>
</div> </div>
<NodeHandler v-model:child-node="item.childNode" /> <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
</div> </div>
</div> </div>
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" /> <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
...@@ -78,7 +97,11 @@ ...@@ -78,7 +97,11 @@
/> />
</div> </div>
</div> </div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" /> <NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div> </div>
</template> </template>
...@@ -87,6 +110,7 @@ import NodeHandler from '../NodeHandler.vue' ...@@ -87,6 +110,7 @@ import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue' import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { getDefaultConditionNodeName } from '../utils' import { getDefaultConditionNodeName } from '../utils'
import { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils' import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue' import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any const { proxy } = getCurrentInstance() as any
...@@ -94,10 +118,6 @@ defineOptions({ ...@@ -94,10 +118,6 @@ defineOptions({
name: 'ExclusiveNode' name: 'ExclusiveNode'
}) })
const props = defineProps({ const props = defineProps({
// parentNode : {
// type: Object as () => SimpleFlowNode,
// required: true
// },
flowNode: { flowNode: {
type: Object as () => SimpleFlowNode, type: Object as () => SimpleFlowNode,
required: true required: true
...@@ -113,10 +133,9 @@ const emits = defineEmits<{ ...@@ -113,10 +133,9 @@ const emits = defineEmits<{
nodeType: number nodeType: number
] ]
}>() }>()
// 是否只读
const readonly = inject<Boolean>('readonly')
const currentNode = ref<SimpleFlowNode>(props.flowNode) const currentNode = ref<SimpleFlowNode>(props.flowNode)
// const conditionNodes = computed(() => currentNode.value.conditionNodes);
watch( watch(
() => props.flowNode, () => props.flowNode,
(newValue) => { (newValue) => {
...@@ -139,6 +158,9 @@ const clickEvent = (index: number) => { ...@@ -139,6 +158,9 @@ const clickEvent = (index: number) => {
} }
const conditionNodeConfig = (nodeId: string) => { const conditionNodeConfig = (nodeId: string) => {
if (readonly) {
return
}
const conditionNode = proxy.$refs[nodeId][0] const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open() conditionNode.open()
} }
...@@ -193,7 +215,7 @@ const recursiveFindParentNode = ( ...@@ -193,7 +215,7 @@ const recursiveFindParentNode = (
node: SimpleFlowNode, node: SimpleFlowNode,
nodeType: number nodeType: number
) => { ) => {
if (!node || node.type === NodeType.START_EVENT_NODE) { if (!node || node.type === NodeType.START_USER_NODE) {
return return
} }
if (node.type === nodeType) { if (node.type === nodeType) {
......
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div
v-if="readonly"
class="branch-node-readonly"
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
>
<span class="iconfont icon-inclusive icon-size inclusive"></span>
</div>
<el-button v-else class="branch-node-add" color="#345da2" @click="addCondition" plain
>添加条件</el-button
>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
:key="index"
>
<template v-if="index == 0">
<div class="branch-line-first-top"> </div>
<div class="branch-line-first-bottom"></div>
</template>
<template v-if="index + 1 == currentNode.conditionNodes?.length">
<div class="branch-line-last-top"></div>
<div class="branch-line-last-bottom"></div>
</template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !item.showText },
`${useTaskStatusClass(item.activityStatus)}`
]"
>
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
</div>
<div class="branch-node-content" @click="conditionNodeConfig(item.id)">
<div class="branch-node-text" :title="item.showText" v-if="item.showText">
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div
class="node-toolbar"
v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
>
<div class="toolbar-icon">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteCondition(index)"
/>
</div>
</div>
<div
class="branch-node-move move-node-left"
v-if="!readonly && index != 0 && index + 1 !== currentNode.conditionNodes?.length"
@click="moveNode(index, -1)"
>
<Icon icon="ep:arrow-left" />
</div>
<div
class="branch-node-move move-node-right"
v-if="
!readonly &&
currentNode.conditionNodes &&
index < currentNode.conditionNodes.length - 2
"
@click="moveNode(index, 1)"
>
<Icon icon="ep:arrow-right" />
</div>
</div>
<NodeHandler v-model:child-node="item.childNode" :current-node="item" />
</div>
</div>
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
<!-- 递归显示子节点 -->
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
</div>
</div>
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useTaskStatusClass } from '../node'
import { getDefaultInclusiveConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'InclusiveNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
const currentNode = ref<SimpleFlowNode>(props.flowNode)
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const showInputs = ref<boolean[]>([])
// 失去焦点
const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.defaultFlow)
}
// 点击条件名称
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
if (readonly) {
return
}
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
// 新增条件
const addCondition = () => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
const len = conditionNodes.length
let lastIndex = len - 1
const conditionData: SimpleFlowNode = {
id: 'Flow_' + generateUUID(),
name: '包容条件' + len,
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
}
// 删除条件
const deleteCondition = (index: number) => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
conditionNodes.splice(index, 1)
if (conditionNodes.length == 1) {
const childNode = currentNode.value.childNode
// 更新此节点为后续孩子节点
emits('update:modelValue', childNode)
}
}
}
// 移动节点
const moveNode = (index: number, to: number) => {
// -1 :向左 1: 向右
if (currentNode.value.conditionNodes) {
currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
index + to,
1,
currentNode.value.conditionNodes[index]
)[0]
}
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_USER_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.INCLUSIVE_BRANCH_NODE) 继续查找
emits('find:parentNode', nodeList, nodeType)
}
</script>
<style lang="scss" scoped></style>
<template> <template>
<div class="branch-node-wrapper"> <div class="branch-node-wrapper">
<div class="branch-node-container"> <div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加分支</div> <div
v-if="readonly"
class="branch-node-readonly"
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
>
<span class="iconfont icon-parallel icon-size parallel"></span>
</div>
<el-button v-else class="branch-node-add" color="#626aef" @click="addCondition" plain
>添加分支</el-button
>
<div <div
class="branch-node-item" class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes" v-for="(item, index) in currentNode.conditionNodes"
...@@ -17,7 +26,7 @@ ...@@ -17,7 +26,7 @@
</template> </template>
<div class="node-wrapper"> <div class="node-wrapper">
<div class="node-container"> <div class="node-container">
<div class="node-box"> <div class="node-box" :class="`${useTaskStatusClass(item.activityStatus)}`">
<div class="branch-node-title-container"> <div class="branch-node-title-container">
<div v-if="showInputs[index]"> <div v-if="showInputs[index]">
<input <input
...@@ -39,7 +48,7 @@ ...@@ -39,7 +48,7 @@
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }} {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div> </div>
</div> </div>
<div class="node-toolbar"> <div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"> <div class="toolbar-icon">
<Icon <Icon
color="#0089ff" color="#0089ff"
...@@ -49,20 +58,8 @@ ...@@ -49,20 +58,8 @@
/> />
</div> </div>
</div> </div>
<!-- <div
class="branch-node-move move-node-left"
v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
<Icon icon="ep:arrow-left" />
</div> -->
<!-- <div
class="branch-node-move move-node-right"
v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
@click="moveNode(index, 1)">
<Icon icon="ep:arrow-right" />
</div> -->
</div> </div>
<NodeHandler v-model:child-node="item.childNode" /> <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
</div> </div>
</div> </div>
<!-- 递归显示子节点 --> <!-- 递归显示子节点 -->
...@@ -74,7 +71,11 @@ ...@@ -74,7 +71,11 @@
/> />
</div> </div>
</div> </div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" /> <NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div> </div>
</template> </template>
...@@ -82,8 +83,8 @@ ...@@ -82,8 +83,8 @@
import NodeHandler from '../NodeHandler.vue' import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue' import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils' import { generateUUID } from '@/utils'
const { proxy } = getCurrentInstance() as any const { proxy } = getCurrentInstance() as any
defineOptions({ defineOptions({
name: 'ParallelNode' name: 'ParallelNode'
...@@ -106,6 +107,8 @@ const emits = defineEmits<{ ...@@ -106,6 +107,8 @@ const emits = defineEmits<{
}>() }>()
const currentNode = ref<SimpleFlowNode>(props.flowNode) const currentNode = ref<SimpleFlowNode>(props.flowNode)
// 是否只读
const readonly = inject<Boolean>('readonly')
watch( watch(
() => props.flowNode, () => props.flowNode,
...@@ -169,7 +172,7 @@ const recursiveFindParentNode = ( ...@@ -169,7 +172,7 @@ const recursiveFindParentNode = (
node: SimpleFlowNode, node: SimpleFlowNode,
nodeType: number nodeType: number
) => { ) => {
if (!node || node.type === NodeType.START_EVENT_NODE) { if (!node || node.type === NodeType.START_USER_NODE) {
return return
} }
if (node.type === nodeType) { if (node.type === nodeType) {
......
<template> <template>
<div class="node-wrapper"> <div class="node-wrapper">
<div class="node-container"> <div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }"> <div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container"> <div class="node-title-container">
<div class="node-title-icon start-user" <div class="node-title-icon start-user"
><span class="iconfont icon-start-user"></span ><span class="iconfont icon-start-user"></span
...@@ -19,27 +25,88 @@ ...@@ -19,27 +25,88 @@
{{ currentNode.name }} {{ currentNode.name }}
</div> </div>
</div> </div>
<div class="node-content" @click="openNodeConfig"> <div class="node-content" @click="nodeClick">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }} {{ currentNode.showText }}
</div> </div>
<div class="node-text" v-else> <div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }} {{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
</div> </div>
<Icon icon="ep:arrow-right-bold" /> <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
</div> </div>
</div> </div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" /> <NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div> </div>
</div> </div>
<StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" /> <StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
<!-- 审批记录 -->
<el-dialog
:title="dialogTitle || '审批记录'"
v-model="dialogVisible"
width="1000px"
append-to-body
>
<el-row>
<el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column label="审批人" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import NodeHandler from '../NodeHandler.vue' import NodeHandler from '../NodeHandler.vue'
import { useWatchNode, useNodeName2 } from '../node' import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts' import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue' import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({ defineOptions({
name: 'StartEventNode' name: 'StartEventNode'
}) })
...@@ -49,6 +116,8 @@ const props = defineProps({ ...@@ -49,6 +116,8 @@ const props = defineProps({
default: () => null default: () => null
} }
}) })
const readonly = inject<Boolean>('readonly') // 是否只读
const tasks = inject<Ref<any[]>>('tasks')
// 定义事件,更新父组件。 // 定义事件,更新父组件。
const emits = defineEmits<{ const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined] 'update:modelValue': [node: SimpleFlowNode | undefined]
...@@ -59,11 +128,27 @@ const currentNode = useWatchNode(props) ...@@ -59,11 +128,27 @@ const currentNode = useWatchNode(props)
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE) const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref() const nodeSetting = ref()
// 打开节点配置 //
const openNodeConfig = () => { const nodeClick = () => {
// 把当前节点传递给配置组件 if (readonly) {
// 只读模式,弹窗显示任务信息
if (tasks && tasks.value) {
dialogTitle.value = currentNode.value.name
selectTasks.value = tasks.value.filter(
(item: any) => item?.taskDefinitionKey === currentNode.value.id
)
dialogVisible.value = true
}
} else {
// 编辑模式,打开节点配置、把当前节点传递给配置组件
nodeSetting.value.showStartUserNodeConfig(currentNode.value) nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer() nodeSetting.value.openDrawer()
}
} }
// 任务的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
<template> <template>
<div class="node-wrapper"> <div class="node-wrapper">
<div class="node-container"> <div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }"> <div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container"> <div class="node-title-container">
<div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div> <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
<input <input
v-if="showInput" v-if="!readonly && showInput"
type="text" type="text"
class="editable-title-input" class="editable-title-input"
@blur="blurEvent()" @blur="blurEvent()"
...@@ -17,23 +23,27 @@ ...@@ -17,23 +23,27 @@
{{ currentNode.name }} {{ currentNode.name }}
</div> </div>
</div> </div>
<div class="node-content" @click="openNodeConfig"> <div class="node-content" @click="nodeClick">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }} {{ currentNode.showText }}
</div> </div>
<div class="node-text" v-else> <div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }} {{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
</div> </div>
<Icon icon="ep:arrow-right-bold" /> <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
</div> </div>
<div class="node-toolbar"> <div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon" <div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div> /></div>
</div> </div>
</div> </div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" /> <NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div> </div>
</div> </div>
<UserTaskNodeConfig <UserTaskNodeConfig
...@@ -42,12 +52,69 @@ ...@@ -42,12 +52,69 @@
:flow-node="currentNode" :flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes" @find:return-task-nodes="findReturnTaskNodes"
/> />
<!-- 审批记录 -->
<el-dialog
:title="dialogTitle || '审批记录'"
v-model="dialogVisible"
width="1000px"
append-to-body
>
<el-row>
<el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column label="审批人" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useWatchNode, useNodeName2 } from '../node' import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import NodeHandler from '../NodeHandler.vue' import NodeHandler from '../NodeHandler.vue'
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue' import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({ defineOptions({
name: 'UserTaskNode' name: 'UserTaskNode'
}) })
...@@ -61,22 +128,36 @@ const emits = defineEmits<{ ...@@ -61,22 +128,36 @@ const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined] 'update:flowNode': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType] 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
}>() }>()
// 是否只读
const readonly = inject<Boolean>('readonly')
const tasks = inject<Ref<any[]>>('tasks')
// 监控节点变化 // 监控节点变化
const currentNode = useWatchNode(props) const currentNode = useWatchNode(props)
// 节点名称编辑 // 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE) const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref() const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => { const nodeClick = () => {
// 把当前节点传递给配置组件 if (readonly) {
if (tasks && tasks.value) {
dialogTitle.value = currentNode.value.name
// 只读模式,弹窗显示任务信息
selectTasks.value = tasks.value.filter(
(item: any) => item?.taskDefinitionKey === currentNode.value.id
)
dialogVisible.value = true
}
} else {
// 编辑模式,打开节点配置、把当前节点传递给配置组件
nodeSetting.value.showUserTaskNodeConfig(currentNode.value) nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer() nodeSetting.value.openDrawer()
}
} }
const deleteNode = () => { const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode) emits('update:flowNode', currentNode.value.childNode)
} }
// 查找可以驳回用户节点 // 查找可以驳回用户节点
const findReturnTaskNodes = ( const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[] // 匹配的节点 matchNodeList: SimpleFlowNode[] // 匹配的节点
...@@ -84,5 +165,10 @@ const findReturnTaskNodes = ( ...@@ -84,5 +165,10 @@ const findReturnTaskNodes = (
// 从父节点查找 // 从父节点查找
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE) emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
} }
// 任务的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
...@@ -8,6 +8,14 @@ export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean ...@@ -8,6 +8,14 @@ export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean
return '条件' + (index + 1) return '条件' + (index + 1)
} }
// 获取包容分支条件节点默认的名称
export const getDefaultInclusiveConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
if (defaultFlow) {
return '其它情况'
}
return '包容条件' + (index + 1)
}
export const convertTimeUnit = (strTimeUnit: string) => { export const convertTimeUnit = (strTimeUnit: string) => {
if (strTimeUnit === 'M') { if (strTimeUnit === 'M') {
return TimeUnitType.MINUTE return TimeUnitType.MINUTE
......
.simple-flow-canvas { // 配置节点头部
position: absolute; .config-header {
inset: 0;
z-index: 1;
overflow: auto;
background-color: #fafafa;
user-select: none;
.simple-flow-container {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
.top-area-container { .node-name {
position: sticky;
inset: 0;
display: flex; display: flex;
width: 100%; height: 24px;
height: 42px; line-height: 24px;
z-index: 1; font-size: 16px;
// padding: 4px 0; cursor: pointer;
background-color: #fff;
justify-content: flex-end;
align-items: center; align-items: center;
}
.top-actions { .divide-line {
width: 100%;
height: 1px;
margin-top: 16px;
background: #eee;
}
.config-editable-input {
height: 24px;
max-width: 510px;
font-size: 16px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
// 表单字段权限
.field-setting-pane {
display: flex; display: flex;
margin: 4px; flex-direction: column;
margin-right: 8px; font-size: 14px;
align-items: center;
.canvas-control { .field-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px; font-size: 16px;
font-weight: 700;
}
.control-scale-group { .field-permit-title {
display: inline-flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
margin-right: 8px; height: 45px;
padding-left: 12px;
line-height: 45px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
.control-scale-button { .first-title {
display: inline-flex; text-align: left !important;
width: 28px; }
height: 28px;
padding: 2px; .other-titles {
display: flex;
justify-content: space-between;
}
.setting-title-label {
display: inline-block;
width: 110px;
padding: 5px 0;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: center; text-align: center;
}
}
.field-setting-item {
align-items: center;
display: flex;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
.field-setting-item-label {
display: inline-block;
width: 110px;
min-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.field-setting-item-group {
display: flex;
justify-content: space-between;
.item-radio-wrap {
display: inline-block;
width: 110px;
text-align: center;
}
}
}
}
// 节点连线气泡卡片样式
.handler-item-wrapper {
display: flex;
cursor: pointer; cursor: pointer;
justify-content: center;
.handler-item {
display: flex;
flex-direction: column;
align-items: center; align-items: center;
} }
.control-scale-label { .handler-item-icon {
margin: 0 4px; width: 60px;
font-size: 14px; height: 60px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
user-select: none;
text-align: center;
&:hover {
background: #e2e2e2;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
.icon-size {
font-size: 25px;
line-height: 60px;
} }
} }
.approve {
color: #ff943e;
}
.copy {
color: #3296fa;
}
.condition {
color: #67c23a;
} }
.parallel {
color: #626aef;
} }
.inclusive {
color: #345da2;
} }
.scale-container { .handler-item-text {
margin-top: 4px;
width: 80px;
text-align: center;
font-size: 13px;
}
}
// Simple 流程模型样式
.simple-process-model-container {
height: 100%;
padding-top: 32px;
background-color: #fafafa;
.simple-process-model {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-top: 16px;
background-color: #fafafa;
transform-origin: 50% 0 0; transform-origin: 50% 0 0;
overflow: auto;
transform: scale(1); transform: scale(1);
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
// 节点容器 定义节点宽度 // 节点容器 定义节点宽度
.node-container { .node-container {
width: 200px; width: 200px;
...@@ -84,14 +197,40 @@ ...@@ -84,14 +197,40 @@
background-color: #fff; background-color: #fff;
flex-direction: column; flex-direction: column;
border: 2px solid transparent; border: 2px solid transparent;
// border-color: #0089ff;
border-radius: 8px; border-radius: 8px;
// border-color: #0089ff; box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%);
box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1); transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
&.status-pass {
background-color: #a9da90;
border-color: #67c23a;
}
&.status-pass:hover {
border-color: #67c23a;
}
&.status-running {
background-color: #e7f0fe;
border-color: #5a9cf8;
}
&.status-running:hover {
border-color: #5a9cf8;
}
&.status-reject {
background-color: #f6e5e5;
border-color: #e47470;
}
&.status-reject:hover {
border-color: #e47470;
}
&:hover { &:hover {
border-color: #0089ff; border-color: #0089ff;
.node-toolbar { .node-toolbar {
opacity: 1; opacity: 1;
} }
...@@ -116,9 +255,11 @@ ...@@ -116,9 +255,11 @@
&.user-task { &.user-task {
color: #ff943e; color: #ff943e;
} }
&.copy-task { &.copy-task {
color: #3296fa; color: #3296fa;
} }
&.start-user { &.start-user {
color: #676565; color: #676565;
} }
...@@ -126,13 +267,14 @@ ...@@ -126,13 +267,14 @@
.node-title { .node-title {
margin-left: 4px; margin-left: 4px;
overflow: hidden;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #1f1f1f;
line-height: 18px; line-height: 18px;
color: #1f1f1f;
text-overflow: ellipsis;
white-space: nowrap;
&:hover { &:hover {
border-bottom: 1px dashed #f60; border-bottom: 1px dashed #f60;
} }
...@@ -153,12 +295,13 @@ ...@@ -153,12 +295,13 @@
} }
.branch-title { .branch-title {
overflow: hidden;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #f60; color: #f60;
text-overflow: ellipsis;
white-space: nowrap;
&:hover { &:hover {
border-bottom: 1px dashed #000; border-bottom: 1px dashed #000;
} }
...@@ -166,7 +309,7 @@ ...@@ -166,7 +309,7 @@
.branch-priority { .branch-priority {
min-width: 50px; min-width: 50px;
font-size: 13px; font-size: 12px;
} }
} }
...@@ -179,7 +322,7 @@ ...@@ -179,7 +322,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
color: #111f2c; color: #111f2c;
background: rgba(0, 0, 0, 0.03); background: rgb(0 0 0 / 3%);
border-radius: 4px; border-radius: 4px;
.node-text { .node-text {
...@@ -198,7 +341,7 @@ ...@@ -198,7 +341,7 @@
.branch-node-content { .branch-node-content {
display: flex; display: flex;
min-height: 32px; min-height: 32px;
padding: 4px 8px; padding: 4px 0;
margin-top: 4px; margin-top: 4px;
line-height: 32px; line-height: 32px;
align-items: center; align-items: center;
...@@ -207,7 +350,7 @@ ...@@ -207,7 +350,7 @@
.branch-node-text { .branch-node-text {
overflow: hidden; overflow: hidden;
font-size: 14px; font-size: 12px;
line-height: 24px; line-height: 24px;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-all; word-break: break-all;
...@@ -218,11 +361,11 @@ ...@@ -218,11 +361,11 @@
// 节点操作 :删除 // 节点操作 :删除
.node-toolbar { .node-toolbar {
opacity: 0;
position: absolute; position: absolute;
top: -20px; top: -20px;
right: 0px; right: 0;
display: flex; display: flex;
opacity: 0;
.toolbar-icon { .toolbar-icon {
text-align: center; text-align: center;
...@@ -233,26 +376,26 @@ ...@@ -233,26 +376,26 @@
// 条件节点左右移动 // 条件节点左右移动
.branch-node-move { .branch-node-move {
position: absolute; position: absolute;
display: none;
width: 10px; width: 10px;
height: 100%;
cursor: pointer; cursor: pointer;
display: none;
align-items: center; align-items: center;
height: 100%;
justify-content: center; justify-content: center;
} }
.move-node-left { .move-node-left {
top: 0;
left: -2px; left: -2px;
top: 0px; background: rgb(126 134 142 / 8%);
background: rgba(126, 134, 142, 0.08);
border-top-left-radius: 8px;
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
border-top-left-radius: 8px;
} }
.move-node-right { .move-node-right {
top: 0;
right: -2px; right: -2px;
top: 0px; background: rgb(126 134 142 / 8%);
background: rgba(126, 134, 142, 0.08);
border-top-right-radius: 6px; border-top-right-radius: 6px;
border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;
} }
...@@ -281,14 +424,9 @@ ...@@ -281,14 +424,9 @@
&::before { &::before {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0;
left: 0;
// bottom: 5px;
bottom: 0px;
z-index: 0; z-index: 0;
width: 2px; width: 2px;
height: 100%; height: 100%;
// height: calc(100% - 5px);
margin: auto; margin: auto;
background-color: #dedede; background-color: #dedede;
content: ''; content: '';
...@@ -299,14 +437,14 @@ ...@@ -299,14 +437,14 @@
position: relative; position: relative;
top: -5px; top: -5px;
display: flex; display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
width: 25px; width: 25px;
height: 25px; height: 25px;
color: #fff; color: #fff;
cursor: pointer;
background-color: #0089ff; background-color: #0089ff;
border-radius: 50%; border-radius: 50%;
align-items: center;
justify-content: center;
&:hover { &:hover {
transform: scale(1.1); transform: scale(1.1);
...@@ -338,11 +476,11 @@ ...@@ -338,11 +476,11 @@
&::before { &::before {
position: absolute; position: absolute;
height: 100%; left: 50%;
width: 4px; width: 4px;
height: 100%;
background-color: #fafafa; background-color: #fafafa;
content: ''; content: '';
left: 50%;
transform: translate(-50%); transform: translate(-50%);
} }
...@@ -355,14 +493,49 @@ ...@@ -355,14 +493,49 @@
padding: 0 10px; padding: 0 10px;
font-size: 12px; font-size: 12px;
line-height: 36px; line-height: 36px;
color: #222;
cursor: pointer;
background: #fff;
border: 2px solid #dedede; border: 2px solid #dedede;
border-radius: 18px; border-radius: 18px;
transform: translateX(-50%); transform: translateX(-50%);
transform-origin: center center; transform-origin: center center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); }
.branch-node-readonly {
position: absolute;
top: -18px;
left: 50%;
z-index: 1;
display: flex;
width: 36px;
height: 36px;
background-color: #fff;
border: 2px solid #dedede;
border-radius: 50%;
transform: translateX(-50%);
align-items: center;
justify-content: center;
transform-origin: center center;
&.status-pass {
background-color: #e9f4e2;
border-color: #6bb63c;
}
&.status-pass:hover {
border-color: #6bb63c;
}
.icon-size {
font-size: 22px;
&.condition {
color: #67c23a;
}
&.parallel {
color: #626aef;
}
&.inclusive {
color: #345da2;
}
}
} }
.branch-node-item { .branch-node-item {
...@@ -458,10 +631,9 @@ ...@@ -458,10 +631,9 @@
padding: 3px 4px; padding: 3px 4px;
color: #212121; color: #212121;
cursor: pointer; cursor: pointer;
// background: #2c2c2c;
background: #fafafa; background: #fafafa;
border-radius: 30px; border-radius: 30px;
box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08); box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
box-sizing: border-box; box-sizing: border-box;
} }
} }
...@@ -473,198 +645,62 @@ ...@@ -473,198 +645,62 @@
.end-node-box { .end-node-box {
display: flex; display: flex;
justify-content: center;
align-items: center;
width: 80px; width: 80px;
height: 36px; height: 36px;
color: #212121; color: #212121;
// background: #6e6e6e; border: 2px solid #fafafa;
background: #fafafa;
border-radius: 30px; border-radius: 30px;
box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08); box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
box-sizing: border-box; box-sizing: border-box;
} justify-content: center;
}
// 可编辑的 title 输入框
.editable-title-input {
height: 20px;
max-width: 145px;
line-height: 20px;
font-size: 12px;
margin-left: 4px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
}
}
// 配置节点头部
.config-header {
display: flex;
flex-direction: column;
.node-name {
display: flex;
height: 24px;
line-height: 24px;
font-size: 16px;
cursor: pointer;
align-items: center; align-items: center;
}
.divide-line {
width: 100%;
height: 1px;
margin-top: 16px;
background: #eee;
}
.config-editable-input {
height: 24px;
max-width: 510px;
font-size: 16px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
// 表单字段权限
.field-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
.field-setting-desc { &.status-pass {
padding-right: 8px; background-color: #a9da90;
margin-bottom: 16px; border-color: #6bb63c;
font-size: 16px;
font-weight: 700;
} }
.field-permit-title { &.status-pass:hover {
display: flex; border-color: #6bb63c;
justify-content: space-between;
align-items: center;
height: 45px;
padding-left: 12px;
line-height: 45px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
.first-title {
text-align: left !important;
} }
.other-titles { &.status-reject {
display: flex; background-color: #f6e5e5;
justify-content: space-between; border-color: #e47470;
} }
.setting-title-label { &.status-reject:hover {
display: inline-block; border-color: #e47470;
width: 110px;
padding: 5px 0;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: center;
}
} }
.field-setting-item { &.status-cancel {
align-items: center; background-color: #eaeaeb;
display: flex; border-color: #919398;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
.field-setting-item-label {
display: inline-block;
width: 110px;
min-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
} }
.field-setting-item-group { &.status-cancel:hover {
display: flex; border-color: #919398;
justify-content: space-between;
.item-radio-wrap {
display: inline-block;
width: 110px;
text-align: center;
} }
} }
} }
}
// 节点连线气泡卡片样式
.handler-item-wrapper {
display: flex;
cursor: pointer;
.handler-item {
margin-right: 8px;
}
.handler-item-icon {
width: 80px;
height: 80px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
user-select: none;
text-align: center;
&:hover {
background: #e2e2e2;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
.icon-size { // 可编辑的 title 输入框
font-size: 35px; .editable-title-input {
line-height: 80px; height: 20px;
} max-width: 145px;
} margin-left: 4px;
font-size: 12px;
line-height: 20px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
.approve { &:focus {
color: #ff943e; border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
} }
.copy {
color: #3296fa;
} }
.condition {
color: #15bc83;
}
.handler-item-text {
margin-top: 4px;
width: 80px;
text-align: center;
} }
} }
......
<template>
<Dialog v-model="dialogVisible" title="人员选择" width="800">
<el-row class="gap2" v-loading="formLoading">
<el-col :span="6">
<ContentWrap class="h-1/1">
<el-tree
ref="treeRef"
:data="deptTree"
:expand-on-click-node="false"
:props="defaultProps"
default-expand-all
highlight-current
node-key="id"
@node-click="handleNodeClick"
/>
</ContentWrap>
</el-col>
<el-col :span="17">
<el-transfer
v-model="selectedUserIdList"
:titles="['未选', '已选']"
filterable
filter-placeholder="搜索成员"
:data="transferUserList"
:props="{ label: 'nickname', key: 'id' }"
/>
</el-col>
</el-row>
<template #footer>
<el-button
:disabled="formLoading || !selectedUserIdList?.length"
type="primary"
@click="submitForm"
>
确 定
</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { defaultProps, findTreeNode, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'UserSelectForm' })
const emit = defineEmits<{
confirm: [id: any, userList: any[]]
}>()
const { t } = useI18n() // 国际
const message = useMessage() // 消息弹窗
const deptTree = ref<Tree[]>([]) // 部门树形结构化
const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表
const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
const selectedUserIdList: any = ref([]) // 选中的用户列表
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const activityId = ref()
/** 计算属性:合并已选择的用户和当前部门过滤后的用户 */
const transferUserList = computed(() => {
// 1.1 获取所有已选择的用户
const selectedUsers = userList.value.filter((user: any) =>
selectedUserIdList.value.includes(user.id)
)
// 1.2 获取当前部门过滤后的未选择用户
const filteredUnselectedUsers = filteredUserList.value.filter(
(user: any) => !selectedUserIdList.value.includes(user.id)
)
// 2. 合并并去重
return [...selectedUsers, ...filteredUnselectedUsers]
})
/** 打开弹窗 */
const open = async (id: number, selectedList?: any[]) => {
activityId.value = id
resetForm()
// 加载部门、用户列表
deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = await UserApi.getSimpleUserList()
// 初始状态下,过滤列表等于所有用户列表
filteredUserList.value = [...userList.value]
selectedUserIdList.value = selectedList?.map((item: any) => item.id) || []
dialogVisible.value = true
}
/** 获取部门过滤后的用户列表 */
const getUserList = async (deptId?: number) => {
formLoading.value = true
try {
// @ts-ignore
// TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤
// TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList
const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
// 更新过滤后的用户列表
filteredUserList.value = data.list
} finally {
formLoading.value = false
}
}
/** 提交选择 */
const submitForm = async () => {
try {
message.success(t('common.updateSuccess'))
dialogVisible.value = false
// 从所有用户列表中筛选出已选择的用户
const emitUserList = userList.value.filter((user: any) =>
selectedUserIdList.value.includes(user.id)
)
// 发送操作成功的事件
emit('confirm', activityId.value, emitUserList)
} finally {
}
}
/** 重置表单 */
const resetForm = () => {
deptTree.value = []
userList.value = []
filteredUserList.value = []
selectedUserIdList.value = []
}
/** 处理部门被点击 */
const handleNodeClick = (row: { [key: string]: any }) => {
getUserList(row.id)
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
</script>
<style lang="scss" scoped>
:deep() {
.el-transfer {
display: flex;
}
.el-transfer__buttons {
display: flex !important;
flex-direction: column-reverse;
justify-content: center;
gap: 20px;
.el-transfer__button:nth-child(2) {
margin: 0;
}
}
}
</style>
<template> <template>
<div class="my-process-designer"> <div class="process-viewer">
<div class="my-process-designer__container"> <div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div>
<div class="my-process-designer__canvas" style="height: 760px" ref="bpmnCanvas"></div> <!-- 自定义箭头样式,用于已完成状态下流程连线箭头 -->
<defs ref="customDefs">
<marker
id="sequenceflow-end-white-success"
viewBox="0 0 20 20"
refX="11"
refY="10"
markerWidth="10"
markerHeight="10"
orient="auto"
>
<path
class="success-arrow"
d="M 1 5 L 11 10 L 1 15 Z"
style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
/>
</marker>
<marker
id="conditional-flow-marker-white-success"
viewBox="0 0 20 20"
refX="-1"
refY="10"
markerWidth="10"
markerHeight="10"
orient="auto"
>
<path
class="success-conditional"
d="M 0 10 L 8 6 L 16 10 L 8 14 Z"
style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
/>
</marker>
</defs>
<!-- 审批记录 -->
<el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px">
<el-row>
<el-table
:data="selectTasks"
size="small"
border
header-cell-class-name="table-header-gray"
>
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column
label="审批人"
min-width="100"
align="center"
v-if="selectActivityType === 'bpmn:UserTask'"
>
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column
label="发起人"
prop="assigneeUser.nickname"
min-width="100"
align="center"
v-else
/>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
align="center"
label="审批建议"
prop="reason"
min-width="120"
v-if="selectActivityType === 'bpmn:UserTask'"
/>
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
<!-- Zoom:放大、缩小 -->
<div style="position: absolute; top: 0; left: 0; width: 100%">
<el-row type="flex" justify="end">
<el-button-group key="scale-control" size="default">
<el-button
size="default"
:plain="true"
:disabled="defaultZoom <= 0.3"
:icon="ZoomOut"
@click="processZoomOut()"
/>
<el-button size="default" style="width: 90px">
{{ Math.floor(defaultZoom * 10 * 10) + '%' }}
</el-button>
<el-button
size="default"
:plain="true"
:disabled="defaultZoom >= 3.9"
:icon="ZoomIn"
@click="processZoomIn()"
/>
<el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
</el-button-group>
</el-row>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import '../theme/index.scss'
import BpmnViewer from 'bpmn-js/lib/Viewer' import BpmnViewer from 'bpmn-js/lib/Viewer'
import DefaultEmptyXML from './plugins/defaultEmpty' import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
import { formatDate } from '@/utils/formatTime' import { DICT_TYPE } from '@/utils/dict'
import { isEmpty } from '@/utils/is' import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { BpmProcessInstanceStatus } from '@/utils/constants'
defineOptions({ name: 'MyProcessViewer' })
const props = defineProps({ const props = defineProps({
value: { xml: {
// BPMN XML 字符串
type: String,
default: ''
},
prefix: {
// 使用哪个引擎
type: String, type: String,
default: 'camunda' required: true
}, },
activityData: { view: {
// 活动的数据。传递时,可高亮流程
type: Array,
default: () => []
},
processInstanceData: {
// 流程实例的数据。传递时,可展示流程发起人等信息
type: Object, type: Object,
default: () => {} require: true
},
taskData: {
// 任务实例的数据。传递时,可展示 UserTask 审核相关的信息
type: Array,
default: () => []
} }
}) })
provide('configGlobal', props) const processCanvas = ref()
const bpmnViewer = ref<BpmnViewer | null>(null)
const emit = defineEmits(['destroy']) const customDefs = ref()
const defaultZoom = ref(1) // 默认缩放比例
const isLoading = ref(false) // 是否加载中
let bpmnModeler const processInstance = ref<any>({}) // 流程实例
const tasks = ref([]) // 流程任务
const xml = ref('') const dialogVisible = ref(false) // 弹窗可见性
const activityLists = ref<any[]>([]) const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
const processInstance = ref<any>(undefined) const selectActivityType = ref<string | undefined>(undefined) // 选中 Task 的活动编号
const taskList = ref<any[]>([]) const selectTasks = ref<any[]>([]) // 选中的任务数组
const bpmnCanvas = ref()
// const element = ref()
const elementOverlayIds = ref<any>(null)
const overlays = ref<any>(null)
const initBpmnModeler = () => { /** Zoom:恢复 */
if (bpmnModeler) return const processReZoom = () => {
bpmnModeler = new BpmnViewer({ defaultZoom.value = 1
container: bpmnCanvas.value, bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto')
bpmnRenderer: {}
})
} }
/* 创建新的流程图 */ /** Zoom:放大 */
const createNewDiagram = async (xml) => { const processZoomIn = (zoomStep = 0.1) => {
// 将字符串转换成图显示出来 let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
let newId = `Process_${new Date().getTime()}` if (newZoom > 4) {
let newName = `业务流程_${new Date().getTime()}` throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
try {
let { warnings } = await bpmnModeler.importXML(xmlString)
if (warnings && warnings.length) {
warnings.forEach((warn) => console.warn(warn))
}
// 高亮流程图
await highlightDiagram()
const canvas = bpmnModeler.get('canvas')
canvas.zoom('fit-viewport', 'auto')
} catch (e) {
console.error(e)
// console.error(`[Process Designer Warn]: ${e?.message || e}`);
} }
defaultZoom.value = newZoom
bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
} }
/* 高亮流程图 */ /** Zoom:缩小 */
// TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html const processZoomOut = (zoomStep = 0.1) => {
const highlightDiagram = async () => { let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
const activityList = activityLists.value if (newZoom < 0.2) {
if (activityList.length === 0) { throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
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] // 获得最后一个任务
let findProcessTask = false //是否已经高亮了进行中的任务
//进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据
let removeTaskDefinitionKeyList = []
// debugger
bpmnModeler.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) // 找到活动对应的 taskId
if (!task) {
return
}
// 进行中的任务已经高亮过了,则不高亮后面的任务了
if (findProcessTask) {
removeTaskDefinitionKeyList.push(n.id)
return
}
// 高亮任务
canvas.addMarker(n.id, getResultCss(task.status))
//标记是否高亮了进行中任务
if (task.status === 1) {
findProcessTask = true
}
// 如果非通过,就不走后面的线条了
if (task.status !== 2) {
return
}
// 处理 outgoing 出线
const outgoing = getActivityOutgoing(activity)
outgoing?.forEach((nn: any) => {
// debugger
let 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) { defaultZoom.value = newZoom
canvas.addMarker(nn.id, 'highlight-todo') bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
canvas.addMarker(nn.targetRef.id, 'highlight-todo') }
/** 流程图预览清空 */
const clearViewer = () => {
if (processCanvas.value) {
processCanvas.value.innerHTML = ''
} }
if (bpmnViewer.value) {
bpmnViewer.value.destroy()
} }
}) bpmnViewer.value = null
} else if (n.$type === 'bpmn:ExclusiveGateway') { }
// 排它网关
// 设置【bpmn:ExclusiveGateway】排它网关的高亮 /** 添加自定义箭头 */
canvas.addMarker(n.id, getActivityHighlightCss(activity)) // TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!!
// 查找需要高亮的连线 const addCustomDefs = () => {
let matchNN: any = undefined if (!bpmnViewer.value) {
let matchActivity: any = undefined
n.outgoing?.forEach((nn: any) => {
let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
if (!targetActivity) {
return return
} }
// 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径: const canvas = bpmnViewer.value?.get('canvas')
// 1. 一个是 UserTask => EndEvent const svg = canvas?._svg
// 2. 一个是 EndEvent svg.appendChild(customDefs.value)
// 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。 }
// 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~
if (!matchActivity || matchActivity.type === 'endEvent') { /** 节点选中 */
matchNN = nn const onSelectElement = (element: any) => {
matchActivity = targetActivity // 清空原选中
} selectActivityType.value = undefined
}) dialogTitle.value = undefined
if (matchNN && matchActivity) { if (!element || !processInstance.value?.id) {
canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity))
}
} else if (n.$type === 'bpmn:ParallelGateway') {
// 并行网关
// 设置【bpmn:ParallelGateway】并行网关的高亮
canvas.addMarker(n.id, getActivityHighlightCss(activity))
n.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') {
// 开始节点
canvas.addMarker(n.id, 'highlight')
n.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.status === 1) {
return return
} }
canvas.addMarker(n.id, getResultCss(processInstance.value.status))
} else if (n.$type === 'bpmn:ServiceTask') { // UserTask 的情况
//服务任务 const activityType = element.type
if (activity.startTime > 0 && activity.endTime === 0) { selectActivityType.value = activityType
//进入执行,标识进行色 if (activityType === 'bpmn:UserTask') {
canvas.addMarker(n.id, getResultCss(1)) dialogTitle.value = element.businessObject ? element.businessObject.name : undefined
} selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id)
if (activity.endTime > 0) { dialogVisible.value = true
// 执行完成,节点标识完成色, 所有outgoing标识完成色。 } else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') {
canvas.addMarker(n.id, getResultCss(2)) dialogTitle.value = '审批信息'
const outgoing = getActivityOutgoing(activity) selectTasks.value = [
outgoing?.forEach((out) => { {
canvas.addMarker(out.id, getResultCss(2)) assigneeUser: processInstance.value.startUser,
}) createTime: processInstance.value.startTime,
} endTime: processInstance.value.endTime,
} else if (n.$type === 'bpmn:SequenceFlow') { status: processInstance.value.status,
let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id) durationInMillis: processInstance.value.durationInMillis
if (targetActivity) {
canvas.addMarker(n.id, getActivityHighlightCss(targetActivity))
}
} }
}) ]
if (!isEmpty(removeTaskDefinitionKeyList)) { dialogVisible.value = true
taskList.value = taskList.value.filter(
(item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
)
} }
} }
const getActivityHighlightCss = (activity) => { /** 初始化 BPMN 视图 */
return activity.endTime ? 'highlight' : 'highlight-todo' const importXML = async (xml: string) => {
} // 清空流程图
clearViewer()
const getResultCss = (status) => { // 初始化流程图
if (status === 1) { if (xml != null && xml !== '') {
// 审批中 try {
return 'highlight-todo' bpmnViewer.value = new BpmnViewer({
} else if (status === 2) { additionalModules: [MoveCanvasModule],
// 已通过 container: processCanvas.value
return 'highlight' })
} else if (status === 3) { // 增加点击事件
// 不通过 bpmnViewer.value.on('element.click', ({ element }) => {
return 'highlight-reject' onSelectElement(element)
} else if (status === 4) { })
// 已取消
return 'highlight-cancel'
} else if (status === 5) {
// 退回
return 'highlight-return'
} else if (status === 6) {
// 委派
return 'highlight-todo'
} else if (status === 7) {
// 审批通过中
return 'highlight-todo'
} else if (status === 0) {
// 待审批
return 'highlight-todo'
}
return ''
}
const getActivityOutgoing = (activity) => { // 初始化 BPMN 视图
// 如果有 outgoing,则直接使用它 isLoading.value = true
if (activity.outgoing && activity.outgoing.length > 0) { await bpmnViewer.value.importXML(xml)
return activity.outgoing // 自定义成功的箭头
} addCustomDefs()
// 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing } catch (e) {
const flowElements = bpmnModeler.getDefinitions().rootElements[0].flowElements clearViewer()
const outgoing: any[] = [] } finally {
flowElements.forEach((item: any) => { isLoading.value = false
if (item.$type !== 'bpmn:SequenceFlow') { // 高亮流程
return setProcessStatus(props.view)
} }
if (item.sourceRef.id === activity.key) {
outgoing.push(item)
} }
})
return outgoing
} }
const initModelListeners = () => {
const EventBus = bpmnModeler.get('eventBus') /** 高亮流程 */
// 注册需要的监听事件 const setProcessStatus = (view: any) => {
EventBus.on('element.hover', function (eventObj) { // 设置相关变量
let element = eventObj ? eventObj.element : null if (!view || !view.processInstance) {
elementHover(element)
})
EventBus.on('element.out', function (eventObj) {
let element = eventObj ? eventObj.element : null
elementOut(element)
})
}
// 流程图的元素被 hover
const elementHover = (element) => {
element.value = element
!elementOverlayIds.value && (elementOverlayIds.value = {})
!overlays.value && (overlays.value = bpmnModeler.get('overlays'))
// 展示信息
// console.log(activityLists.value, 'activityLists.value')
// console.log(element.value, 'element.value')
const activity = activityLists.value.find((m) => m.key === element.value.id)
// console.log(activity, 'activityactivityactivityactivity')
if (!activity) {
return return
} }
if (!elementOverlayIds.value[element.value.id] && element.value.type !== 'bpmn:Process') { processInstance.value = view.processInstance
let html = `<div class="element-overlays"> tasks.value = view.tasks
<p>Elemet id: ${element.value.id}</p> if (isLoading.value || !bpmnViewer.value) {
<p>Elemet type: ${element.value.type}</p>
</div>` // 默认值
if (element.value.type === 'bpmn:StartEvent' && processInstance.value) {
html = `<p>发起人:${processInstance.value.startUser.nickname}</p>
<p>部门:${processInstance.value.startUser.deptName}</p>
<p>创建时间:${formatDate(processInstance.value.createTime)}`
} else if (element.value.type === 'bpmn:UserTask') {
let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
if (!task) {
return return
} }
let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS) const {
let dataResult = '' unfinishedTaskActivityIds,
optionData.forEach((element) => { finishedTaskActivityIds,
if (element.value == task.status) { finishedSequenceFlowActivityIds,
dataResult = element.label rejectedTaskActivityIds
} = view
const canvas = bpmnViewer.value.get('canvas')
const elementRegistry = bpmnViewer.value.get('elementRegistry')
// 已完成节点
if (Array.isArray(finishedSequenceFlowActivityIds)) {
finishedSequenceFlowActivityIds.forEach((item: any) => {
if (item != null) {
canvas.addMarker(item, 'success')
const element = elementRegistry.get(item)
const conditionExpression = element.businessObject.conditionExpression
if (conditionExpression) {
canvas.addMarker(item, 'condition-expression')
} }
})
html = `<p>审批人:${task.assigneeUser.nickname}</p>
<p>部门:${task.assigneeUser.deptName}</p>
<p>结果:${dataResult}</p>
<p>创建时间:${formatDate(task.createTime)}</p>`
// html = `<p>审批人:${task.assigneeUser.nickname}</p>
// <p>部门:${task.assigneeUser.deptName}</p>
// <p>结果:${getIntDictOptions(
// DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
// task.status
// )}</p>
// <p>创建时间:${formatDate(task.createTime)}</p>`
if (task.endTime) {
html += `<p>结束时间:${formatDate(task.endTime)}</p>`
} }
if (task.reason) { })
html += `<p>审批建议:${task.reason}</p>`
} }
} else if (element.value.type === 'bpmn:ServiceTask' && processInstance.value) { if (Array.isArray(finishedTaskActivityIds)) {
if (activity.startTime > 0) { finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success'))
html = `<p>创建时间:${formatDate(activity.startTime)}</p>`
} }
if (activity.endTime > 0) {
html += `<p>结束时间:${formatDate(activity.endTime)}</p>` // 未完成节点
if (Array.isArray(unfinishedTaskActivityIds)) {
unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary'))
} }
console.log(html)
} else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) { // 被拒绝节点
let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS) if (Array.isArray(rejectedTaskActivityIds)) {
let dataResult = '' rejectedTaskActivityIds.forEach((item: any) => {
optionData.forEach((element) => { if (item != null) {
if (element.value == processInstance.value.status) { canvas.addMarker(item, 'danger')
dataResult = element.label
} }
}) })
html = `<p>结果:${dataResult}</p>`
// html = `<p>结果:${getIntDictOptions(
// DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
// processInstance.value.status
// )}</p>`
if (processInstance.value.endTime) {
html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
} }
// 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里
if (
[BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes(
processInstance.value.status
)
) {
const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent')
endNodes.forEach((item: any) => {
canvas.removeMarker(item.id, 'success')
if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) {
canvas.addMarker(item.id, 'cancel')
} else {
canvas.addMarker(item.id, 'danger')
} }
// console.log(html, 'html111111111111111')
elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, {
position: { left: 0, bottom: 0 },
html: `<div class="element-overlays">${html}</div>`
}) })
} }
} }
// 流程图的元素被 out watch(
const elementOut = (element) => { () => props.xml,
toRaw(overlays.value).remove({ element }) (newXml) => {
elementOverlayIds.value[element.id] = null importXML(newXml)
} },
{ immediate: true }
)
watch(
() => props.view,
(newView) => {
setProcessStatus(newView)
},
{ immediate: true }
)
/** mounted:初始化 */
onMounted(() => { onMounted(() => {
xml.value = props.value importXML(props.xml)
activityLists.value = props.activityData setProcessStatus(props.view)
// 初始化
initBpmnModeler()
createNewDiagram(xml.value)
// 初始模型的监听器
initModelListeners()
}) })
/** unmount:销毁 */
onBeforeUnmount(() => { onBeforeUnmount(() => {
// this.$once('hook:beforeDestroy', () => { clearViewer()
// })
if (bpmnModeler) bpmnModeler.destroy()
emit('destroy', bpmnModeler)
bpmnModeler = null
}) })
watch(
() => props.value,
(newValue) => {
xml.value = newValue
createNewDiagram(xml.value)
}
)
watch(
() => props.activityData,
(newActivityData) => {
activityLists.value = newActivityData
createNewDiagram(xml.value)
}
)
watch(
() => props.processInstanceData,
(newProcessInstanceData) => {
processInstance.value = newProcessInstanceData
createNewDiagram(xml.value)
}
)
watch(
() => props.taskData,
(newTaskListData) => {
taskList.value = newTaskListData
createNewDiagram(xml.value)
}
)
</script> </script>
<style lang="scss">
/** 处理中 */
.highlight-todo.djs-connection > .djs-visual > path {
stroke: #1890ff !important;
stroke-dasharray: 4px !important;
fill-opacity: 0.2 !important;
}
.highlight-todo.djs-shape .djs-visual > :nth-child(1) {
fill: #1890ff !important;
stroke: #1890ff !important;
stroke-dasharray: 4px !important;
fill-opacity: 0.2 !important;
}
:deep(.highlight-todo.djs-connection > .djs-visual > path) {
stroke: #1890ff !important;
stroke-dasharray: 4px !important;
fill-opacity: 0.2 !important;
marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
}
:deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
fill: #1890ff !important;
stroke: #1890ff !important;
stroke-dasharray: 4px !important;
fill-opacity: 0.2 !important;
}
/** 通过 */
.highlight.djs-shape .djs-visual > :nth-child(1) {
fill: green !important;
stroke: green !important;
fill-opacity: 0.2 !important;
}
.highlight.djs-shape .djs-visual > :nth-child(2) {
fill: green !important;
}
.highlight.djs-shape .djs-visual > path {
fill: green !important;
fill-opacity: 0.2 !important;
stroke: green !important;
}
.highlight.djs-connection > .djs-visual > path {
stroke: green !important;
}
.highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
fill: green !important; /* color elements as green */
}
:deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
fill: green !important;
stroke: green !important;
fill-opacity: 0.2 !important;
}
:deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
fill: green !important;
}
:deep(.highlight.djs-shape .djs-visual > path) {
fill: green !important;
fill-opacity: 0.2 !important;
stroke: green !important;
}
:deep(.highlight.djs-connection > .djs-visual > path) {
stroke: green !important;
}
.djs-element.highlight > .djs-visual > path {
stroke: green !important;
}
/** 不通过 */
.highlight-reject.djs-shape .djs-visual > :nth-child(1) {
fill: red !important;
stroke: red !important;
fill-opacity: 0.2 !important;
}
.highlight-reject.djs-shape .djs-visual > :nth-child(2) {
fill: red !important;
}
.highlight-reject.djs-shape .djs-visual > path {
fill: red !important;
fill-opacity: 0.2 !important;
stroke: red !important;
}
.highlight-reject.djs-connection > .djs-visual > path {
stroke: red !important;
marker-end: url(#sequenceflow-end-white-success) !important;
}
.highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
fill: red !important; /* color elements as green */
}
:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) {
fill: red !important;
stroke: red !important;
fill-opacity: 0.2 !important;
}
:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) {
fill: red !important;
}
:deep(.highlight-reject.djs-shape .djs-visual > path) {
fill: red !important;
fill-opacity: 0.2 !important;
stroke: red !important;
}
:deep(.highlight-reject.djs-connection > .djs-visual > path) {
stroke: red !important;
}
/** 已取消 */
.highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
fill: grey !important;
stroke: grey !important;
fill-opacity: 0.2 !important;
}
.highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
fill: grey !important;
}
.highlight-cancel.djs-shape .djs-visual > path {
fill: grey !important;
fill-opacity: 0.2 !important;
stroke: grey !important;
}
.highlight-cancel.djs-connection > .djs-visual > path {
stroke: grey !important;
}
.highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) {
fill: grey !important; /* color elements as green */
}
:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) {
fill: grey !important;
stroke: grey !important;
fill-opacity: 0.2 !important;
}
:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) {
fill: grey !important;
}
:deep(.highlight-cancel.djs-shape .djs-visual > path) {
fill: grey !important;
fill-opacity: 0.2 !important;
stroke: grey !important;
}
:deep(.highlight-cancel.djs-connection > .djs-visual > path) {
stroke: grey !important;
}
/** 回退 */
.highlight-return.djs-shape .djs-visual > :nth-child(1) {
fill: #e6a23c !important;
stroke: #e6a23c !important;
fill-opacity: 0.2 !important;
}
.highlight-return.djs-shape .djs-visual > :nth-child(2) {
fill: #e6a23c !important;
}
.highlight-return.djs-shape .djs-visual > path {
fill: #e6a23c !important;
fill-opacity: 0.2 !important;
stroke: #e6a23c !important;
}
.highlight-return.djs-connection > .djs-visual > path {
stroke: #e6a23c !important;
}
.highlight-return:not(.djs-connection) .djs-visual > :nth-child(1) {
fill: #e6a23c !important; /* color elements as green */
}
:deep(.highlight-return.djs-shape .djs-visual > :nth-child(1)) {
fill: #e6a23c !important;
stroke: #e6a23c !important;
fill-opacity: 0.2 !important;
}
:deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) {
fill: #e6a23c !important;
}
:deep(.highlight-return.djs-shape .djs-visual > path) {
fill: #e6a23c !important;
fill-opacity: 0.2 !important;
stroke: #e6a23c !important;
}
:deep(.highlight-return.djs-connection > .djs-visual > path) {
stroke: #e6a23c !important;
}
.element-overlays {
width: 200px;
padding: 8px;
color: #fafafa;
background: rgb(0 0 0 / 60%);
border-radius: 4px;
box-sizing: border-box;
}
</style>
...@@ -1211,6 +1211,76 @@ ...@@ -1211,6 +1211,76 @@
"isAttr": true "isAttr": true
} }
] ]
},
{
"name": "AssignStartUserHandlerType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "RejectHandlerType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "RejectReturnTaskId",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "String",
"isBody": true
}
]
},
{
"name": "AssignEmptyHandlerType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "AssignEmptyUserIds",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "String",
"isBody": true
}
]
} }
], ],
"emumerations": [] "emumerations": []
......
<template> <template>
<div class="process-panel__container" :style="{ width: `${width}px` }"> <div class="process-panel__container" :style="{ width: `${width}px`,maxHeight: '700px' }">
<el-collapse v-model="activeTab"> <el-collapse v-model="activeTab">
<el-collapse-item name="base"> <el-collapse-item name="base">
<!-- class="panel-tab__title" --> <!-- class="panel-tab__title" -->
...@@ -54,6 +54,10 @@ ...@@ -54,6 +54,10 @@
<template #title><Icon icon="ep:promotion" />其他</template> <template #title><Icon icon="ep:promotion" />其他</template>
<element-other-config :id="elementId" /> <element-other-config :id="elementId" />
</el-collapse-item> </el-collapse-item>
<el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig">
<template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template>
<element-custom-config :id="elementId" :type="elementType" />
</el-collapse-item>
</el-collapse> </el-collapse>
</div> </div>
</template> </template>
......
<!-- UserTask 自定义配置:
1. 审批人与提交人为同一人时
2. 审批人拒绝时
3. 审批人为空时
-->
<template>
<div class="panel-tab__content">
<el-divider content-position="left">审批人拒绝时</el-divider>
<el-form-item prop="rejectHandlerType">
<el-radio-group
v-model="rejectHandlerType"
:disabled="returnTaskList.length === 0"
@change="updateRejectHandlerType"
>
<div class="flex-col">
<div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
label="驳回节点"
prop="returnNodeId"
>
<el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
<el-option
v-for="item in returnTaskList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人为空时</el-divider>
<el-form-item prop="assignEmptyHandlerType">
<el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户"
prop="assignEmptyHandlerUserIds"
span="24"
>
<el-select
v-model="assignEmptyUserIds"
clearable
multiple
style="width: 100%"
@change="updateAssignEmptyUserIds"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人与提交人为同一人时</el-divider>
<el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</div>
</template>
<script lang="ts" setup>
import {
ASSIGN_START_USER_HANDLER_TYPES,
RejectHandlerType,
REJECT_HANDLER_TYPES,
ASSIGN_EMPTY_HANDLER_TYPES,
AssignEmptyHandlerType
} from '@/components/SimpleProcessDesignerV2/src/consts'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'ElementCustomConfig' })
const props = defineProps({
id: String,
type: String
})
const prefix = inject('prefix')
// 审批人与提交人为同一人时
const assignStartUserHandlerTypeEl = ref()
const assignStartUserHandlerType = ref()
// 审批人拒绝时
const rejectHandlerTypeEl = ref()
const rejectHandlerType = ref()
const returnNodeIdEl = ref()
const returnNodeId = ref()
const returnTaskList = ref([])
// 审批人为空时
const assignEmptyHandlerTypeEl = ref()
const assignEmptyHandlerType = ref()
const assignEmptyUserIdsEl = ref()
const assignEmptyUserIds = ref()
const elExtensionElements = ref()
const otherExtensions = ref()
const bpmnElement = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances
const resetCustomConfigList = () => {
bpmnElement.value = bpmnInstances().bpmnElement
// 获取可回退的列表
returnTaskList.value = findAllPredecessorsExcludingStart(
bpmnElement.value.id,
bpmnInstances().modeler
)
// 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
// 审批人与提交人为同一人时
assignStartUserHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
// 审批人拒绝时
rejectHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:RejectHandlerType`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
rejectHandlerType.value = rejectHandlerTypeEl.value.value
returnNodeIdEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:RejectReturnTaskId`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
returnNodeId.value = returnNodeIdEl.value.value
// 审批人为空时
assignEmptyHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
assignEmptyUserIdsEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value.split(',').map((item) => {
// 如果数字超出了最大安全整数范围,则将其作为字符串处理
let num = Number(item)
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
})
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex) =>
ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
ex.$type !== `${prefix}:RejectHandlerType` &&
ex.$type !== `${prefix}:RejectReturnTaskId` &&
ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
ex.$type !== `${prefix}:AssignEmptyUserIds`
) ?? []
// 更新元素扩展属性,避免后续报错
updateElementExtensions()
}
const updateAssignStartUserHandlerType = () => {
assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
updateElementExtensions()
}
const updateRejectHandlerType = () => {
rejectHandlerTypeEl.value.value = rejectHandlerType.value
returnNodeId.value = returnTaskList.value[0].id
returnNodeIdEl.value.value = returnNodeId.value
updateElementExtensions()
}
const updateReturnNodeId = () => {
returnNodeIdEl.value.value = returnNodeId.value
updateElementExtensions()
}
const updateAssignEmptyHandlerType = () => {
assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
updateElementExtensions()
}
const updateAssignEmptyUserIds = () => {
assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
updateElementExtensions()
}
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
assignStartUserHandlerTypeEl.value,
rejectHandlerTypeEl.value,
returnNodeIdEl.value,
assignEmptyHandlerTypeEl.value,
assignEmptyUserIdsEl.value
]
})
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions
})
}
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
resetCustomConfigList()
})
},
{ immediate: true }
)
function findAllPredecessorsExcludingStart(elementId, modeler) {
const elementRegistry = modeler.get('elementRegistry')
const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
const predecessors = new Set() // 使用 Set 来避免重复节点
// 检查是否是开始事件节点
function isStartEvent(element) {
return element.type === 'bpmn:StartEvent'
}
function findPredecessorsRecursively(element) {
// 获取与当前节点相连的所有连接
const incomingConnections = allConnections.filter((connection) => connection.target === element)
incomingConnections.forEach((connection) => {
const source = connection.source // 获取前置节点
// 只添加不是开始事件的前置节点
if (!isStartEvent(source)) {
predecessors.add(source.businessObject)
// 递归查找前置节点
findPredecessorsRecursively(source)
}
})
}
const targetElement = elementRegistry.get(elementId)
if (targetElement) {
findPredecessorsRecursively(targetElement)
}
return Array.from(predecessors) // 返回前置节点数组
}
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => {
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
})
</script>
...@@ -268,9 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances ...@@ -268,9 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances
const resetFormList = () => { const resetFormList = () => {
bpmnELement.value = bpmnInstances().bpmnElement bpmnELement.value = bpmnInstances().bpmnElement
formKey.value = bpmnELement.value.businessObject.formKey formKey.value = bpmnELement.value.businessObject.formKey
if (formKey.value?.length > 0) { // if (formKey.value?.length > 0) {
formKey.value = parseInt(formKey.value) // formKey.value = parseInt(formKey.value)
} // }
// 获取元素扩展属性 或者 创建扩展属性 // 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value = elExtensionElements.value =
bpmnELement.value.businessObject.get('extensionElements') || bpmnELement.value.businessObject.get('extensionElements') ||
......
...@@ -80,7 +80,7 @@ const resetAttributesList = () => { ...@@ -80,7 +80,7 @@ const resetAttributesList = () => {
otherExtensionList.value = [] // 其他扩展配置 otherExtensionList.value = [] // 其他扩展配置
bpmnElementProperties.value = bpmnElementProperties.value =
// bpmnElement.value.businessObject?.extensionElements?.filter((ex) => { // bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
bpmnElement.value.businessObject?.extensionElements?.values.filter((ex) => { bpmnElement.value.businessObject?.extensionElements?.values?.filter((ex) => {
if (ex.$type !== `${prefix}:Properties`) { if (ex.$type !== `${prefix}:Properties`) {
otherExtensionList.value.push(ex) otherExtensionList.value.push(ex)
} }
......
...@@ -5,7 +5,7 @@ $--color-danger: #ff4d4f; ...@@ -5,7 +5,7 @@ $--color-danger: #ff4d4f;
/* 改变 icon 字体路径变量,必需 */ /* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts'; $--font-path: '~element-ui/lib/theme-chalk/fonts';
@import '~element-ui/packages/theme-chalk/src/index'; @use '~element-ui/packages/theme-chalk/src/index';
.el-table td, .el-table td,
.el-table th { .el-table th {
......
@import './process-designer.scss'; @use './process-designer.scss';
@import './process-panel.scss'; @use './process-panel.scss';
$success-color: #4eb819;
$primary-color: #409EFF;
$danger-color: #F56C6C;
$cancel-color: #909399;
.process-viewer {
position: relative;
border: 1px solid #EFEFEF;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important;
.success-arrow {
fill: $success-color;
stroke: $success-color;
}
.success-conditional {
fill: white;
stroke: $success-color;
}
.success.djs-connection {
.djs-visual path {
stroke: $success-color!important;
//marker-end: url(#sequenceflow-end-white-success)!important;
}
}
.success.djs-connection.condition-expression {
.djs-visual path {
//marker-start: url(#conditional-flow-marker-white-success)!important;
}
}
.success.djs-shape {
.djs-visual rect {
stroke: $success-color!important;
fill: $success-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $success-color!important;
}
.djs-visual path:nth-child(2) {
stroke: $success-color!important;
fill: $success-color!important;
}
.djs-visual circle {
stroke: $success-color!important;
fill: $success-color!important;
fill-opacity: 0.15!important;
}
}
.primary.djs-shape {
.djs-visual rect {
stroke: $primary-color!important;
fill: $primary-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $primary-color!important;
}
.djs-visual circle {
stroke: $primary-color!important;
fill: $primary-color!important;
fill-opacity: 0.15!important;
}
}
.danger.djs-shape {
.djs-visual rect {
stroke: $danger-color!important;
fill: $danger-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $danger-color!important;
}
.djs-visual circle {
stroke: $danger-color!important;
fill: $danger-color!important;
fill-opacity: 0.15!important;
}
}
.cancel.djs-shape {
.djs-visual rect {
stroke: $cancel-color!important;
fill: $cancel-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $cancel-color!important;
}
.djs-visual circle {
stroke: $cancel-color!important;
fill: $cancel-color!important;
fill-opacity: 0.15!important;
}
}
}
.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette {
display: none;
}
@import 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; @use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
@import 'bpmn-js-token-simulation/assets/css/font-awesome.min.css'; @use 'bpmn-js-token-simulation/assets/css/font-awesome.min.css';
@import 'bpmn-js-token-simulation/assets/css/normalize.css'; @use 'bpmn-js-token-simulation/assets/css/normalize.css';
// 边框被 token-simulation 样式覆盖了 // 边框被 token-simulation 样式覆盖了
.djs-palette { .djs-palette {
......
...@@ -5,16 +5,12 @@ import { config } from './config' ...@@ -5,16 +5,12 @@ import { config } from './config'
const { default_headers } = config const { default_headers } = config
const request = (option: any) => { const request = (option: any) => {
const { url, method, params, data, headersType, responseType, ...config } = option const { headersType, headers, ...otherOption } = option
return service({ return service({
url: url, ...otherOption,
method,
params,
data,
...config,
responseType: responseType,
headers: { headers: {
'Content-Type': headersType || default_headers 'Content-Type': headersType || default_headers,
...headers
} }
}) })
} }
......
import axios, { import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
AxiosError,
AxiosInstance,
AxiosRequestHeaders,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import qs from 'qs' import qs from 'qs'
...@@ -37,7 +31,11 @@ const whiteList: string[] = ['/login', '/refresh-token'] ...@@ -37,7 +31,11 @@ const whiteList: string[] = ['/login', '/refresh-token']
const service: AxiosInstance = axios.create({ const service: AxiosInstance = axios.create({
baseURL: base_url, // api 的 base_url baseURL: base_url, // api 的 base_url
timeout: request_timeout, // 请求超时时间 timeout: request_timeout, // 请求超时时间
withCredentials: false // 禁用 Cookie 等信息 withCredentials: false, // 禁用 Cookie 等信息
// 自定义参数序列化函数
paramsSerializer: (params) => {
return qs.stringify(params, { allowDots: true })
}
}) })
// request拦截器 // request拦截器
...@@ -52,28 +50,26 @@ service.interceptors.request.use( ...@@ -52,28 +50,26 @@ service.interceptors.request.use(
} }
}) })
if (getAccessToken() && !isToken) { if (getAccessToken() && !isToken) {
;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token config.headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
} }
// 设置租户 // 设置租户
if (tenantEnable && tenantEnable === 'true') { if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId() const tenantId = getTenantId()
if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId if (tenantId) config.headers['tenant-id'] = tenantId
}
const method = config.method?.toUpperCase()
// 防止 GET 请求缓存
if (method === 'GET') {
config.headers['Cache-Control'] = 'no-cache'
config.headers['Pragma'] = 'no-cache'
}
// 自定义参数序列化函数
else if (method === 'POST') {
const contentType = config.headers['Content-Type'] || config.headers['content-type']
if (contentType === 'application/x-www-form-urlencoded') {
if (config.data && typeof config.data !== 'string') {
config.data = qs.stringify(config.data)
} }
const params = config.params || {}
const data = config.data || false
if (
config.method?.toUpperCase() === 'POST' &&
(config.headers as AxiosRequestHeaders)['Content-Type'] ===
'application/x-www-form-urlencoded'
) {
config.data = qs.stringify(data)
}
// get参数编码
if (config.method?.toUpperCase() === 'GET' && params) {
config.params = {}
const paramsStr = qs.stringify(params, { allowDots: true })
if (paramsStr) {
config.url = config.url + '?' + paramsStr
} }
} }
return config return config
......
...@@ -8,7 +8,8 @@ export function hasPermi(app: App<Element>) { ...@@ -8,7 +8,8 @@ export function hasPermi(app: App<Element>) {
const { wsCache } = useCache() const { wsCache } = useCache()
const { value } = binding const { value } = binding
const all_permission = '*:*:*' const all_permission = '*:*:*'
const permissions = wsCache.get(CACHE_KEY.USER).permissions const userInfo = wsCache.get(CACHE_KEY.USER)
const permissions = userInfo?.permissions || []
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value const permissionFlag = value
......
...@@ -7,8 +7,9 @@ export function hasRole(app: App<Element>) { ...@@ -7,8 +7,9 @@ export function hasRole(app: App<Element>) {
app.directive('hasRole', (el, binding) => { app.directive('hasRole', (el, binding) => {
const { wsCache } = useCache() const { wsCache } = useCache()
const { value } = binding const { value } = binding
const super_admin = 'admin' const super_admin = 'super_admin'
const roles = wsCache.get(CACHE_KEY.USER).roles const userInfo = wsCache.get(CACHE_KEY.USER)
const roles = userInfo?.roles || []
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {
const roleFlag = value const roleFlag = value
......
...@@ -92,7 +92,7 @@ export default defineComponent({ ...@@ -92,7 +92,7 @@ export default defineComponent({
$prefix-cls: #{$elNamespace}-breadcrumb; $prefix-cls: #{$elNamespace}-breadcrumb;
.#{$prefix-cls} { .#{$prefix-cls} {
:deep(&__item) { :deep(.#{$prefix-cls}__item) {
display: flex; display: flex;
.#{$prefix-cls}__inner { .#{$prefix-cls}__inner {
display: flex; display: flex;
...@@ -105,7 +105,7 @@ $prefix-cls: #{$elNamespace}-breadcrumb; ...@@ -105,7 +105,7 @@ $prefix-cls: #{$elNamespace}-breadcrumb;
} }
} }
:deep(&__item):not(:last-child) { :deep(.#{$prefix-cls}__item):not(:last-child) {
.#{$prefix-cls}__inner { .#{$prefix-cls}__inner {
color: var(--top-header-text-color); color: var(--top-header-text-color);
...@@ -115,7 +115,7 @@ $prefix-cls: #{$elNamespace}-breadcrumb; ...@@ -115,7 +115,7 @@ $prefix-cls: #{$elNamespace}-breadcrumb;
} }
} }
:deep(&__item):last-child { :deep(.#{$prefix-cls}__item):last-child {
.#{$prefix-cls}__inner { .#{$prefix-cls}__inner {
display: flex; display: flex;
align-items: center; align-items: center;
......
<script lang="ts" setup> <script lang="ts" setup>
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import * as NotifyMessageApi from '@/api/system/notify/message' import * as NotifyMessageApi from '@/api/system/notify/message'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'Message' }) defineOptions({ name: 'Message' })
const { push } = useRouter() const { push } = useRouter()
const userStore = useUserStoreWithOut()
const activeName = ref('notice') const activeName = ref('notice')
const unreadCount = ref(0) // 未读消息数量 const unreadCount = ref(0) // 未读消息数量
const list = ref<any[]>([]) // 消息列表 const list = ref<any[]>([]) // 消息列表
...@@ -37,7 +39,11 @@ onMounted(() => { ...@@ -37,7 +39,11 @@ onMounted(() => {
// 轮询刷新小红点 // 轮询刷新小红点
setInterval( setInterval(
() => { () => {
if (userStore.getIsSetUser) {
getUnreadCount() getUnreadCount()
} else {
unreadCount.value = 0
}
}, },
1000 * 60 * 2 1000 * 60 * 2
) )
......
...@@ -297,5 +297,6 @@ $prefix-cls: #{$namespace}-setting; ...@@ -297,5 +297,6 @@ $prefix-cls: #{$namespace}-setting;
.#{$prefix-cls} { .#{$prefix-cls} {
border-radius: 6px 0 0 6px; border-radius: 6px 0 0 6px;
z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/
} }
</style> </style>
...@@ -127,12 +127,8 @@ const toLastView = () => { ...@@ -127,12 +127,8 @@ const toLastView = () => {
const moveToCurrentTag = async () => { const moveToCurrentTag = async () => {
await nextTick() await nextTick()
for (const v of unref(visitedViews)) { for (const v of unref(visitedViews)) {
if (v.fullPath === unref(currentRoute).path) { if (v.fullPath === unref(currentRoute).fullPath) {
moveToTarget(v) moveToTarget(v)
if (v.fullPath !== unref(currentRoute).fullPath) {
tagsViewStore.updateVisitedView(unref(currentRoute))
}
break break
} }
} }
...@@ -207,7 +203,7 @@ const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => { ...@@ -207,7 +203,7 @@ const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
// 是否是当前tag // 是否是当前tag
const isActive = (route: RouteLocationNormalizedLoaded): boolean => { const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
return route.path === unref(currentRoute).path return route.fullPath === unref(currentRoute).fullPath
} }
// 所有右键菜单组件的元素 // 所有右键菜单组件的元素
...@@ -373,7 +369,10 @@ watch( ...@@ -373,7 +369,10 @@ watch(
:size="12" :size="12"
class="mr-5px" class="mr-5px"
/> />
{{ t(item?.meta?.title as string) }} {{
t(item?.meta?.title as string) +
(item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '')
}}
<Icon <Icon
:class="`${prefixCls}__item--close`" :class="`${prefixCls}__item--close`"
:size="12" :size="12"
......
...@@ -51,7 +51,10 @@ import { ...@@ -51,7 +51,10 @@ import {
ElMenu, ElMenu,
ElMenuItem, ElMenuItem,
ElFooter, ElFooter,
ElMessage ElMessage,
ElCollapse,
ElCollapseItem,
ElCard,
// ElFormItem, // ElFormItem,
// ElOption // ElOption
} from 'element-plus' } from 'element-plus'
...@@ -113,7 +116,10 @@ const components = [ ...@@ -113,7 +116,10 @@ const components = [
UserSelect, UserSelect,
DeptSelect, DeptSelect,
ApiSelect, ApiSelect,
Editor Editor,
ElCollapse,
ElCollapseItem,
ElCard,
] ]
// 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档 // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档
......
...@@ -267,9 +267,9 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -267,9 +267,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
} }
}, },
{ {
path: 'manager/simple/workflow/model/edit', path: 'manager/simple/model',
component: () => import('@/views/bpm/simpleWorkflow/index.vue'), component: () => import('@/views/bpm/simple/SimpleModelDesign.vue'),
name: 'SimpleWorkflowDesignEditor', name: 'SimpleModelDesign',
meta: { meta: {
noCache: true, noCache: true,
hidden: true, hidden: true,
...@@ -292,7 +292,6 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -292,7 +292,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
}, },
{ {
path: 'process-instance/detail', path: 'process-instance/detail',
// component: () => import('@/views/bpm/processInstance/detail/index_new.vue'), // TODO 芋艿:新审批界面,已适配 simple 模式,未来会适配 bpmn 模式
component: () => import('@/views/bpm/processInstance/detail/index.vue'), component: () => import('@/views/bpm/processInstance/detail/index.vue'),
name: 'BpmProcessInstanceDetail', name: 'BpmProcessInstanceDetail',
meta: { meta: {
......
...@@ -35,8 +35,9 @@ export const usePermissionStore = defineStore('permission', { ...@@ -35,8 +35,9 @@ export const usePermissionStore = defineStore('permission', {
return new Promise<void>(async (resolve) => { return new Promise<void>(async (resolve) => {
// 获得菜单列表,它在登录的时候,setUserInfoAction 方法中已经进行获取 // 获得菜单列表,它在登录的时候,setUserInfoAction 方法中已经进行获取
let res: AppCustomRouteRecordRaw[] = [] let res: AppCustomRouteRecordRaw[] = []
if (wsCache.get(CACHE_KEY.ROLE_ROUTERS)) { const roleRouters = wsCache.get(CACHE_KEY.ROLE_ROUTERS)
res = wsCache.get(CACHE_KEY.ROLE_ROUTERS) as AppCustomRouteRecordRaw[] if (roleRouters) {
res = roleRouters as AppCustomRouteRecordRaw[]
} }
const routerMap: AppRouteRecordRaw[] = generateRoute(res) const routerMap: AppRouteRecordRaw[] = generateRoute(res)
// 动态路由,404一定要放到最后面 // 动态路由,404一定要放到最后面
......
...@@ -31,13 +31,27 @@ export const useTagsViewStore = defineStore('tagsView', { ...@@ -31,13 +31,27 @@ export const useTagsViewStore = defineStore('tagsView', {
}, },
// 新增tag // 新增tag
addVisitedView(view: RouteLocationNormalizedLoaded) { addVisitedView(view: RouteLocationNormalizedLoaded) {
if (this.visitedViews.some((v) => v.path === view.path)) return if (this.visitedViews.some((v) => v.fullPath === view.fullPath)) return
if (view.meta?.noTagsView) return if (view.meta?.noTagsView) return
this.visitedViews.push( const visitedView = Object.assign({}, view, { title: view.meta?.title || 'no-name' })
Object.assign({}, view, {
title: view.meta?.title || 'no-name' if (visitedView.meta) {
const titleSuffixList: string[] = []
this.visitedViews.forEach((v) => {
if (v.path === visitedView.path && v.meta?.title === visitedView.meta?.title) {
titleSuffixList.push(v.meta?.titleSuffix || '1')
}
}) })
) if (titleSuffixList.length) {
let titleSuffix = 1
while (titleSuffixList.includes(`${titleSuffix}`)) {
titleSuffix += 1
}
visitedView.meta.titleSuffix = titleSuffix === 1 ? undefined : `${titleSuffix}`
}
}
this.visitedViews.push(visitedView)
}, },
// 新增缓存 // 新增缓存
addCachedView() { addCachedView() {
...@@ -63,7 +77,7 @@ export const useTagsViewStore = defineStore('tagsView', { ...@@ -63,7 +77,7 @@ export const useTagsViewStore = defineStore('tagsView', {
// 删除tag // 删除tag
delVisitedView(view: RouteLocationNormalizedLoaded) { delVisitedView(view: RouteLocationNormalizedLoaded) {
for (const [i, v] of this.visitedViews.entries()) { for (const [i, v] of this.visitedViews.entries()) {
if (v.path === view.path) { if (v.fullPath === view.fullPath) {
this.visitedViews.splice(i, 1) this.visitedViews.splice(i, 1)
break break
} }
...@@ -95,18 +109,18 @@ export const useTagsViewStore = defineStore('tagsView', { ...@@ -95,18 +109,18 @@ export const useTagsViewStore = defineStore('tagsView', {
// 删除其他tag // 删除其他tag
delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
this.visitedViews = this.visitedViews.filter((v) => { this.visitedViews = this.visitedViews.filter((v) => {
return v?.meta?.affix || v.path === view.path return v?.meta?.affix || v.fullPath === view.fullPath
}) })
}, },
// 删除左侧 // 删除左侧
delLeftViews(view: RouteLocationNormalizedLoaded) { delLeftViews(view: RouteLocationNormalizedLoaded) {
const index = findIndex<RouteLocationNormalizedLoaded>( const index = findIndex<RouteLocationNormalizedLoaded>(
this.visitedViews, this.visitedViews,
(v) => v.path === view.path (v) => v.fullPath === view.fullPath
) )
if (index > -1) { if (index > -1) {
this.visitedViews = this.visitedViews.filter((v, i) => { this.visitedViews = this.visitedViews.filter((v, i) => {
return v?.meta?.affix || v.path === view.path || i > index return v?.meta?.affix || v.fullPath === view.fullPath || i > index
}) })
this.addCachedView() this.addCachedView()
} }
...@@ -115,18 +129,18 @@ export const useTagsViewStore = defineStore('tagsView', { ...@@ -115,18 +129,18 @@ export const useTagsViewStore = defineStore('tagsView', {
delRightViews(view: RouteLocationNormalizedLoaded) { delRightViews(view: RouteLocationNormalizedLoaded) {
const index = findIndex<RouteLocationNormalizedLoaded>( const index = findIndex<RouteLocationNormalizedLoaded>(
this.visitedViews, this.visitedViews,
(v) => v.path === view.path (v) => v.fullPath === view.fullPath
) )
if (index > -1) { if (index > -1) {
this.visitedViews = this.visitedViews.filter((v, i) => { this.visitedViews = this.visitedViews.filter((v, i) => {
return v?.meta?.affix || v.path === view.path || i < index return v?.meta?.affix || v.fullPath === view.fullPath || i < index
}) })
this.addCachedView() this.addCachedView()
} }
}, },
updateVisitedView(view: RouteLocationNormalizedLoaded) { updateVisitedView(view: RouteLocationNormalizedLoaded) {
for (let v of this.visitedViews) { for (let v of this.visitedViews) {
if (v.path === view.path) { if (v.fullPath === view.fullPath) {
v = Object.assign(v, view) v = Object.assign(v, view)
break break
} }
......
@import './variables.scss'; @use './variables.scss' as *;
// 导出变量 // 导出变量
:export { :export {
namespace: $namespace; namespace: $namespace;
......
@import './var.css'; @use './var.css';
@import './FormCreate/index.scss'; @use './FormCreate/index.scss';
@import './theme.scss'; @use './theme.scss';
@import 'element-plus/theme-chalk/dark/css-vars.css'; @use 'element-plus/theme-chalk/dark/css-vars.css';
.reset-margin [class*='el-icon'] + span { .reset-margin [class*='el-icon'] + span {
margin-left: 2px !important; margin-left: 2px !important;
......
...@@ -10,7 +10,8 @@ const RefreshTokenKey = 'REFRESH_TOKEN' ...@@ -10,7 +10,8 @@ const RefreshTokenKey = 'REFRESH_TOKEN'
// 获取token // 获取token
export const getAccessToken = () => { export const getAccessToken = () => {
// 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错 // 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错
return wsCache.get(AccessTokenKey) ? wsCache.get(AccessTokenKey) : wsCache.get('ACCESS_TOKEN') const accessToken = wsCache.get(AccessTokenKey)
return accessToken ? accessToken : wsCache.get('ACCESS_TOKEN')
} }
// 刷新token // 刷新token
......
...@@ -449,3 +449,11 @@ export const BpmModelFormType = { ...@@ -449,3 +449,11 @@ export const BpmModelFormType = {
NORMAL: 10, // 流程表单 NORMAL: 10, // 流程表单
CUSTOM: 20 // 业务表单 CUSTOM: 20 // 业务表单
} }
export const BpmProcessInstanceStatus = {
NOT_START: -1, // 未开始
RUNNING: 1, // 审批中
APPROVE: 2, // 审批通过
REJECT: 3, // 审批不通过
CANCEL: 4 // 已取消
}
...@@ -44,6 +44,7 @@ export const setConfAndFields2 = ( ...@@ -44,6 +44,7 @@ export const setConfAndFields2 = (
value?: object value?: object
) => { ) => {
if (isRef(detailPreview)) { if (isRef(detailPreview)) {
// @ts-ignore
detailPreview = detailPreview.value detailPreview = detailPreview.value
} }
// @ts-ignore // @ts-ignore
......
...@@ -98,8 +98,9 @@ export const isServer = typeof window === 'undefined' ...@@ -98,8 +98,9 @@ export const isServer = typeof window === 'undefined'
export const isClient = !isServer export const isClient = !isServer
export const isUrl = (path: string): boolean => { export const isUrl = (path: string): boolean => {
// fix:修复hash路由无法跳转的问题
const reg = const reg =
/(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/ /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%#\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path) return reg.test(path)
} }
......
...@@ -12,8 +12,9 @@ export function checkPermi(value: string[]) { ...@@ -12,8 +12,9 @@ export function checkPermi(value: string[]) {
const { wsCache } = useCache() const { wsCache } = useCache()
const permissionDatas = value const permissionDatas = value
const all_permission = '*:*:*' const all_permission = '*:*:*'
const permissions = wsCache.get(CACHE_KEY.USER).permissions const userInfo = wsCache.get(CACHE_KEY.USER)
const hasPermission = permissions.some((permission) => { const permissions = userInfo?.permissions || []
const hasPermission = permissions.some((permission: string) => {
return all_permission === permission || permissionDatas.includes(permission) return all_permission === permission || permissionDatas.includes(permission)
}) })
return !!hasPermission return !!hasPermission
...@@ -32,9 +33,10 @@ export function checkRole(value: string[]) { ...@@ -32,9 +33,10 @@ export function checkRole(value: string[]) {
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {
const { wsCache } = useCache() const { wsCache } = useCache()
const permissionRoles = value const permissionRoles = value
const super_admin = 'admin' const super_admin = 'super_admin'
const roles = wsCache.get(CACHE_KEY.USER).roles const userInfo = wsCache.get(CACHE_KEY.USER)
const hasRole = roles.some((role) => { const roles = userInfo?.roles || []
const hasRole = roles.some((role: string) => {
return super_admin === role || permissionRoles.includes(role) return super_admin === role || permissionRoles.includes(role)
}) })
return !!hasRole return !!hasRole
......
...@@ -120,7 +120,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord ...@@ -120,7 +120,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
data.children = [childrenData] data.children = [childrenData]
} else { } else {
// 目录 // 目录
if (route.children) { if (route.children?.length) {
data.component = Layout data.component = Layout
data.redirect = getRedirect(route.path, route.children) data.redirect = getRedirect(route.path, route.children)
// 外链 // 外链
......
...@@ -42,6 +42,7 @@ ...@@ -42,6 +42,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { CategoryApi, CategoryVO } from '@/api/bpm/category' import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import { CommonStatusEnum } from '@/utils/constants'
/** BPM 流程分类 表单 */ /** BPM 流程分类 表单 */
defineOptions({ name: 'CategoryForm' }) defineOptions({ name: 'CategoryForm' })
...@@ -57,7 +58,7 @@ const formData = ref({ ...@@ -57,7 +58,7 @@ const formData = ref({
id: undefined, id: undefined,
name: undefined, name: undefined,
code: undefined, code: undefined,
status: undefined, status: CommonStatusEnum.ENABLE,
sort: undefined sort: undefined
}) })
const formRules = reactive({ const formRules = reactive({
...@@ -116,7 +117,7 @@ const resetForm = () => { ...@@ -116,7 +117,7 @@ const resetForm = () => {
id: undefined, id: undefined,
name: undefined, name: undefined,
code: undefined, code: undefined,
status: undefined, status: CommonStatusEnum.ENABLE,
sort: undefined sort: undefined
} }
formRef.value?.resetFields() formRef.value?.resetFields()
......
...@@ -70,13 +70,7 @@ ...@@ -70,13 +70,7 @@
<!-- 弹窗:流程模型图的预览 --> <!-- 弹窗:流程模型图的预览 -->
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800"> <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
<MyProcessViewer <MyProcessViewer style="height: 700px" key="designer" :xml="bpmnXml" />
key="designer"
v-model="bpmnXml"
:value="bpmnXml as any"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/>
</Dialog> </Dialog>
</template> </template>
...@@ -118,7 +112,7 @@ const formDetailPreview = ref({ ...@@ -118,7 +112,7 @@ const formDetailPreview = ref({
rule: [], rule: [],
option: {} option: {}
}) })
const handleFormDetail = async (row) => { const handleFormDetail = async (row: any) => {
if (row.formType == 10) { if (row.formType == 10) {
// 设置表单 // 设置表单
setConfAndFields2(formDetailPreview, row.formConf, row.formFields) setConfAndFields2(formDetailPreview, row.formConf, row.formFields)
...@@ -133,13 +127,13 @@ const handleFormDetail = async (row) => { ...@@ -133,13 +127,13 @@ const handleFormDetail = async (row) => {
/** 流程图的详情按钮操作 */ /** 流程图的详情按钮操作 */
const bpmnDetailVisible = ref(false) const bpmnDetailVisible = ref(false)
const bpmnXml = ref(null) const bpmnXml = ref('')
const bpmnControlForm = ref({ const handleBpmnDetail = async (row: any) => {
prefix: 'flowable' // 设置可见
}) bpmnXml.value = ''
const handleBpmnDetail = async (row) => {
bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
bpmnDetailVisible.value = true bpmnDetailVisible.value = true
// 加载 BPMN XML
bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
} }
/** 初始化 **/ /** 初始化 **/
......
...@@ -64,7 +64,11 @@ const designerConfig = ref({ ...@@ -64,7 +64,11 @@ const designerConfig = ref({
switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段 switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
autoActive: true, // 是否自动选中拖入的组件 autoActive: true, // 是否自动选中拖入的组件
useTemplate: false, // 是否生成vue2语法的模板组件 useTemplate: false, // 是否生成vue2语法的模板组件
formOptions: {}, // 定义表单配置默认值 formOptions: {
form: {
labelWidth: '100px' // 设置默认的 label 宽度为 100px
}
}, // 定义表单配置默认值
fieldReadonly: false, // 配置field是否可以编辑 fieldReadonly: false, // 配置field是否可以编辑
hiddenDragMenu: false, // 隐藏拖拽操作按钮 hiddenDragMenu: false, // 隐藏拖拽操作按钮
hiddenDragBtn: false, // 隐藏拖拽按钮 hiddenDragBtn: false, // 隐藏拖拽按钮
......
...@@ -143,8 +143,9 @@ const openForm = (id?: number) => { ...@@ -143,8 +143,9 @@ const openForm = (id?: number) => {
const toRouter: { name: string; query?: { id: number } } = { const toRouter: { name: string; query?: { id: number } } = {
name: 'BpmFormEditor' name: 'BpmFormEditor'
} }
console.log(typeof id)
// 表单新建的时候id传的是event需要排除 // 表单新建的时候id传的是event需要排除
if (typeof id === 'number') { if (typeof id === 'number' || typeof id === 'string') {
toRouter.query = { toRouter.query = {
id id
} }
......
<template>
<div class="flex items-center h-50px">
<!-- 头部:分类名 -->
<div class="flex items-center">
<el-tooltip content="拖动排序" v-if="isCategorySorting">
<Icon
:size="22"
icon="ic:round-drag-indicator"
class="ml-10px category-drag-icon cursor-move text-#8a909c"
/>
</el-tooltip>
<h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
<div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
</div>
<!-- 头部:操作 -->
<div class="flex-1 flex" v-if="!isCategorySorting">
<div
v-if="categoryInfo.modelList.length > 0"
class="ml-20px flex items-center"
:class="[
'transition-transform duration-300 cursor-pointer',
isExpand ? 'rotate-180' : 'rotate-0'
]"
@click="isExpand = !isExpand"
>
<Icon icon="ep:arrow-down-bold" color="#999" />
</div>
<div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
<template v-if="!isModelSorting">
<el-button
v-if="categoryInfo.modelList.length > 0"
link
type="info"
class="mr-20px"
@click.stop="handleModelSort"
>
<Icon icon="fa:sort-amount-desc" class="mr-5px" />
排序
</el-button>
<el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
<Icon icon="fa:plus" class="mr-5px" />
新建
</el-button>
<el-dropdown
@command="(command) => handleCategoryCommand(command, categoryInfo)"
placement="bottom"
>
<el-button link type="info">
<Icon icon="ep:setting" class="mr-5px" />
分类
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item>
<el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else>
<el-button @click.stop="handleModelSortCancel"> 取 消 </el-button>
<el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button>
</template>
</div>
</div>
</div>
<!-- 模型列表 -->
<el-collapse-transition>
<div v-show="isExpand">
<el-table
:class="categoryInfo.name"
ref="tableRef"
:header-cell-style="{ backgroundColor: isDark ? '' : '#edeff0', paddingLeft: '10px' }"
:cell-style="{ paddingLeft: '10px' }"
:row-style="{ height: '68px' }"
:data="modelList"
row-key="id"
>
<el-table-column label="流程名" prop="name" min-width="150">
<template #default="scope">
<div class="flex items-center">
<el-tooltip content="拖动排序" v-if="isModelSorting">
<Icon
icon="ic:round-drag-indicator"
class="drag-icon cursor-move text-#8a909c mr-10px"
/>
</el-tooltip>
<el-image :src="scope.row.icon" class="h-38px w-38px mr-10px rounded" />
{{ scope.row.name }}
</div>
</template>
</el-table-column>
<el-table-column label="可见范围" prop="startUserIds" min-width="100">
<template #default="scope">
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见
</el-text>
<el-text v-else-if="scope.row.startUsers.length == 1">
{{ scope.row.startUsers[0].nickname }}
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
>
{{ scope.row.startUsers[0].nickname }}{{ scope.row.startUsers.length }} 人可见
</el-tooltip>
</el-text>
</template>
</el-table-column>
<el-table-column label="表单信息" prop="formType" min-width="200">
<template #default="scope">
<el-button
v-if="scope.row.formType === BpmModelFormType.NORMAL"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formName }}</span>
</el-button>
<el-button
v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formCustomCreatePath }}</span>
</el-button>
<label v-else>暂无表单</label>
</template>
</el-table-column>
<el-table-column label="最后发布" prop="deploymentTime" min-width="250">
<template #default="scope">
<div class="flex items-center">
<span v-if="scope.row.processDefinition" class="w-150px">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
<el-tag v-if="scope.row.processDefinition">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
<el-tag
v-if="scope.row.processDefinition?.suspensionState === 2"
type="warning"
class="ml-10px"
>
已停用
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openModelForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
修改
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
设计
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
:disabled="!isManagerUser(scope.row)"
>
发布
</el-button>
<el-dropdown
class="!align-middle ml-5px"
@command="(command) => handleModelCommand(command, scope.row)"
v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
>
<el-button type="primary" link>更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleDefinitionList"
v-if="checkPermi(['bpm:process-definition:query'])"
>
历史
</el-dropdown-item>
<el-dropdown-item
command="handleChangeState"
v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
:disabled="!isManagerUser(scope.row)"
>
{{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleDelete"
v-if="checkPermi(['bpm:model:delete'])"
:disabled="!isManagerUser(scope.row)"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</div>
</el-collapse-transition>
<!-- 弹窗:重命名分类 -->
<Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
<template #title>
<div class="pl-10px font-bold text-18px"> 重命名分类 </div>
</template>
<div class="px-30px">
<el-input v-model="renameCategoryForm.name" />
</div>
<template #footer>
<div class="pr-25px pb-25px">
<el-button @click="renameCategoryVisible = false">取 消</el-button>
<el-button type="primary" @click="handleRenameConfirm">确 定</el-button>
</div>
</template>
</Dialog>
<!-- 表单弹窗:添加流程模型 -->
<ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" />
</template>
<script lang="ts" setup>
import ModelForm from './ModelForm.vue'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import Sortable from 'sortablejs'
import { propTypes } from '@/utils/propTypes'
import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import { setConfAndFields2 } from '@/utils/formCreate'
import { BpmModelFormType, BpmModelType } from '@/utils/constants'
import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user'
import { useAppStore } from '@/store/modules/app'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'BpmModel' })
const props = defineProps({
categoryInfo: propTypes.object.def([]), // 分类后的数据
isCategorySorting: propTypes.bool.def(false) // 是否分类在排序
})
const emit = defineEmits(['success'])
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const userStore = useUserStoreWithOut() // 用户信息缓存
const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式
const isModelSorting = ref(false) // 是否正处于排序状态
const originalData: any = ref([]) // 原始数据
const modelList: any = ref([]) // 模型列表
const isExpand = ref(false) // 是否处于展开状态
/** '更多'操作按钮 */
const handleModelCommand = (command: string, row: any) => {
switch (command) {
case 'handleDefinitionList':
handleDefinitionList(row)
break
case 'handleDelete':
handleDelete(row)
break
case 'handleChangeState':
handleChangeState(row)
break
default:
break
}
}
/** '分类'操作按钮 */
const handleCategoryCommand = async (command: string, row: any) => {
switch (command) {
case 'handleRename':
renameCategoryForm.value = await CategoryApi.getCategory(row.id)
renameCategoryVisible.value = true
break
case 'handleDeleteCategory':
await handleDeleteCategory()
break
default:
break
}
}
/** 删除按钮操作 */
const handleDelete = async (row: any) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ModelApi.deleteModel(row.id)
message.success(t('common.delSuccess'))
// 刷新列表
emit('success')
} catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row: any) => {
const state = row.processDefinition.suspensionState
const newState = state === 1 ? 2 : 1
try {
// 修改状态的二次确认
const id = row.id
debugger
const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
// 发起修改状态
await ModelApi.updateModelState(id, newState)
message.success(statusState + '成功')
// 刷新列表
emit('success')
} catch {}
}
/** 设计流程 */
const handleDesign = (row: any) => {
if (row.type == BpmModelType.BPMN) {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
} else {
push({
name: 'SimpleModelDesign',
query: {
modelId: row.id
}
})
}
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
try {
// 删除的二次确认
await message.confirm('是否部署该流程!!')
// 发起部署
await ModelApi.deployModel(row.id)
message.success(t('部署成功'))
// 刷新列表
emit('success')
} catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row: any) => {
push({
name: 'BpmProcessDefinition',
query: {
key: row.key
}
})
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
rule: [],
option: {}
})
const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
// 设置表单
const data = await FormApi.getForm(row.formId)
setConfAndFields2(formDetailPreview, data.conf, data.fields)
// 弹窗打开
formDetailVisible.value = true
} else {
await push({
path: row.formCustomCreatePath
})
}
}
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
const userId = userStore.getUser.id
return row.managerUserIds && row.managerUserIds.includes(userId)
}
/** 处理模型的排序 **/
const handleModelSort = () => {
// 保存初始数据
originalData.value = cloneDeep(props.categoryInfo.modelList)
isModelSorting.value = true
initSort()
}
/** 处理模型的排序提交 */
const handleModelSortSubmit = async () => {
// 保存排序
const ids = modelList.value.map((item: any) => item.id)
await ModelApi.updateModelSortBatch(ids)
// 刷新列表
isModelSorting.value = false
message.success('排序模型成功')
emit('success')
}
/** 处理模型的排序取消 */
const handleModelSortCancel = () => {
// 恢复初始数据
modelList.value = cloneDeep(originalData.value)
isModelSorting.value = false
}
/** 创建拖拽实例 */
const tableRef = ref()
const initSort = () => {
const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
Sortable.create(table, {
group: 'shared',
animation: 150,
draggable: '.el-table__row',
handle: '.drag-icon',
// 结束拖动事件
onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
if (oldDraggableIndex !== newDraggableIndex) {
modelList.value.splice(
newDraggableIndex,
0,
modelList.value.splice(oldDraggableIndex, 1)[0]
)
}
}
})
}
/** 更新 modelList 模型列表 */
const updateModeList = () => {
modelList.value = cloneDeep(props.categoryInfo.modelList)
if (props.categoryInfo.modelList.length > 0) {
isExpand.value = true
}
}
/** 重命名弹窗确定 */
const renameCategoryVisible = ref(false)
const renameCategoryForm = ref({
name: ''
})
const handleRenameConfirm = async () => {
if (renameCategoryForm.value?.name.length === 0) {
return message.warning('请输入名称')
}
// 发起修改
await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
message.success('重命名成功')
// 刷新列表
renameCategoryVisible.value = false
emit('success')
}
/** 删除分类 */
const handleDeleteCategory = async () => {
try {
if (props.categoryInfo.modelList.length > 0) {
return message.warning('该分类下仍有流程定义,不允许删除')
}
await message.confirm('确认删除分类吗?')
// 发起删除
await CategoryApi.deleteCategory(props.categoryInfo.id)
message.success(t('common.delSuccess'))
// 刷新列表
emit('success')
} catch {}
}
/** 添加流程模型弹窗 */
const modelFormRef = ref()
const openModelForm = (type: string, id?: number) => {
modelFormRef.value.open(type, id)
}
watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })
watch(
() => props.isCategorySorting,
(val) => {
if (val) isExpand.value = false
},
{ immediate: true }
)
</script>
<style lang="scss">
.rename-dialog.el-dialog {
padding: 0 !important;
.el-dialog__header {
border-bottom: none;
}
.el-dialog__footer {
border-top: none !important;
}
}
</style>
<style lang="scss" scoped>
:deep() {
.el-table__cell {
overflow: hidden;
border-bottom: none !important;
}
}
</style>
...@@ -155,6 +155,7 @@ ...@@ -155,6 +155,7 @@
</Dialog> </Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import * as ModelApi from '@/api/bpm/model' import * as ModelApi from '@/api/bpm/model'
...@@ -170,7 +171,9 @@ defineOptions({ name: 'ModelForm' }) ...@@ -170,7 +171,9 @@ defineOptions({ name: 'ModelForm' })
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const userStore = useUserStoreWithOut() // 用户信息缓存 const userStore = useUserStoreWithOut() // 用户信息缓存
const props = defineProps({
categoryId: propTypes.number
})
const dialogVisible = ref(false) // 弹窗的是否展示 const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题 const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
...@@ -232,6 +235,9 @@ const open = async (type: string, id?: string) => { ...@@ -232,6 +235,9 @@ const open = async (type: string, id?: string) => {
categoryList.value = await CategoryApi.getCategorySimpleList() categoryList.value = await CategoryApi.getCategorySimpleList()
// 查询用户列表 // 查询用户列表
userList.value = await UserApi.getSimpleUserList() userList.value = await UserApi.getSimpleUserList()
if (props.categoryId) {
formData.value.category = props.categoryId
}
} }
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
......
<template> <template>
<doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
<doc-alert
title="流程设计器(钉钉、飞书)"
url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
/>
<doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
<doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
<ContentWrap> <ContentWrap>
<div class="flex justify-between pl-20px items-center">
<h3 class="font-extrabold">流程模型</h3>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<el-form <el-form
class="-mb-15px" v-if="!isCategorySorting"
class="-mb-15px flex mr-10px"
:model="queryParams" :model="queryParams"
ref="queryFormRef" ref="queryFormRef"
:inline="true" :inline="true"
label-width="68px" label-width="68px"
@submit.prevent
> >
<el-form-item label="流程标识" prop="key"> <el-form-item prop="name" class="ml-auto">
<el-input
v-model="queryParams.key"
placeholder="请输入流程标识"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
placeholder="请输入流程名称" placeholder="搜索流程"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
class="!w-240px" class="!w-240px"
/>
</el-form-item>
<el-form-item label="流程分类" prop="category">
<el-select
v-model="queryParams.category"
placeholder="请选择流程分类"
clearable
class="!w-240px"
> >
<el-option <template #prefix>
v-for="category in categoryList" <Icon icon="ep:search" class="mx-10px" />
:key="category.code" </template>
:label="category.name" </el-input>
:value="category.code"
/>
</el-select>
</el-form-item> </el-form-item>
<!-- 右上角:新建模型、更多操作 -->
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon icon="ep:plus" class="mr-5px" /> 新建模型
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['bpm:model:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新建
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> <el-form-item>
</ContentWrap> <el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end">
<el-button class="w-30px" plain>
<!-- 列表 --> <Icon icon="ep:setting" />
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="流程名称" align="center" prop="name" min-width="200" />
<el-table-column label="流程图标" align="center" prop="icon" min-width="100">
<template #default="scope">
<el-image :src="scope.row.icon" class="h-32px w-32px" />
</template>
</el-table-column>
<el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
<template #default="scope">
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见
</el-text>
<el-text v-else-if="scope.row.startUsers.length == 1">
{{ scope.row.startUsers[0].nickname }}
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
>
{{ scope.row.startUsers[0].nickname }}{{ scope.row.startUsers.length }} 人可见
</el-tooltip>
</el-text>
</template>
</el-table-column>
<el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
<el-table-column label="表单信息" align="center" prop="formType" min-width="200">
<template #default="scope">
<el-button
v-if="scope.row.formType === 10"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formName }}</span>
</el-button>
<el-button
v-else-if="scope.row.formType === 20"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formCustomCreatePath }}</span>
</el-button>
<label v-else>暂无表单</label>
</template>
</el-table-column>
<el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
<template #default="scope">
<span v-if="scope.row.processDefinition">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
<el-tag v-if="scope.row.processDefinition" class="ml-10px">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
<el-tag
v-if="scope.row.processDefinition?.suspensionState === 2"
type="warning"
class="ml-10px"
>
已停用
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
修改
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
设计
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
:disabled="!isManagerUser(scope.row)"
>
发布
</el-button> </el-button>
<el-dropdown
class="!align-middle ml-5px"
@command="(command) => handleCommand(command, scope.row)"
v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
>
<el-button type="primary" link>更多</el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item <el-dropdown-item command="handleCategoryAdd">
command="handleDefinitionList" <Icon icon="ep:circle-plus" :size="13" class="mr-5px" />
v-if="checkPermi(['bpm:process-definition:query'])" 新建分类
>
历史
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item command="handleCategorySort">
command="handleChangeState" <Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" />
v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition" 分类排序
:disabled="!isManagerUser(scope.row)"
>
{{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleDelete"
v-if="checkPermi(['bpm:model:delete'])"
:disabled="!isManagerUser(scope.row)"
>
删除
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</template> </el-form-item>
</el-table-column> </el-form>
</el-table> <div class="mr-20px" v-else>
<!-- 分页 --> <el-button @click="handleCategorySortCancel"> 取 消 </el-button>
<Pagination <el-button type="primary" @click="handleCategorySortSubmit"> 保存排序 </el-button>
:total="total" </div>
v-model:page="queryParams.pageNo" </div>
v-model:limit="queryParams.pageSize"
@pagination="getList" <el-divider />
<!-- 按照分类,展示其所属的模型列表 -->
<div class="px-15px">
<draggable
:disabled="!isCategorySorting"
v-model="categoryGroup"
item-key="id"
:animation="400"
>
<template #item="{ element }">
<ContentWrap
class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
v-loading="loading"
:body-style="{ padding: 0 }"
:key="element.id"
>
<CategoryDraggableModel
:isCategorySorting="isCategorySorting"
:categoryInfo="element"
@success="getList"
/> />
</ContentWrap> </ContentWrap>
</template>
</draggable>
</div>
</ContentWrap>
<!-- 表单弹窗:添加/修改流程 --> <!-- 表单弹窗:添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" /> <ModelForm ref="formRef" @success="getList" />
<!-- 表单弹窗:添加分类 -->
<CategoryForm ref="categoryFormRef" @success="getList" />
<!-- 弹窗:表单详情 --> <!-- 弹窗:表单详情 -->
<Dialog title="表单详情" v-model="formDetailVisible" width="800"> <Dialog title="表单详情" v-model="formDetailVisible" width="800">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
...@@ -218,187 +96,126 @@ ...@@ -218,187 +96,126 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { formatDate } from '@/utils/formatTime' import draggable from 'vuedraggable'
import { CategoryApi } from '@/api/bpm/category'
import * as ModelApi from '@/api/bpm/model' import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import ModelForm from './ModelForm.vue' import ModelForm from './ModelForm.vue'
import { setConfAndFields2 } from '@/utils/formCreate' import CategoryForm from '../category/CategoryForm.vue'
import { CategoryApi } from '@/api/bpm/category' import { cloneDeep } from 'lodash-es'
import { BpmModelType } from '@/utils/constants' import CategoryDraggableModel from './CategoryDraggableModel.vue'
import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'BpmModel' }) defineOptions({ name: 'BpmModel' })
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const userStore = useUserStoreWithOut() // 用户信息缓存
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数 const isCategorySorting = ref(false) // 是否 category 正处于排序状态
const list = ref([]) // 列表的数据
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, name: undefined
pageSize: 10,
key: undefined,
name: undefined,
category: undefined
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const categoryList = ref([]) // 流程分类列表 const categoryGroup: any = ref([]) // 按照 category 分组的数据
const originalData: any = ref([]) // 原始数据
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ModelApi.getModelPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = () => {
queryParams.pageNo = 1
getList() getList()
} }
/** 重置按钮操作 */ /** 添加/修改操作 */
const resetQuery = () => { const formRef = ref()
queryFormRef.value.resetFields() const openForm = (type: string, id?: number) => {
handleQuery() formRef.value.open(type, id)
} }
/** '更多'操作按钮 */ /** 流程表单的详情按钮操作 */
const handleCommand = (command: string, row: any) => { const formDetailVisible = ref(false)
const formDetailPreview = ref({
rule: [],
option: {}
})
/** 右上角设置按钮 */
const handleCommand = (command: string) => {
switch (command) { switch (command) {
case 'handleDefinitionList': case 'handleCategoryAdd':
handleDefinitionList(row) handleCategoryAdd()
break break
case 'handleDelete': case 'handleCategorySort':
handleDelete(row) handleCategorySort()
break
case 'handleChangeState':
handleChangeState(row)
break break
default: default:
break break
} }
} }
/** 添加/修改操作 */ /** 新建分类 */
const formRef = ref() const categoryFormRef = ref()
const openForm = (type: string, id?: number) => { const handleCategoryAdd = () => {
formRef.value.open(type, id) categoryFormRef.value.open('create')
} }
/** 删除按钮操作 */ /** 分类排序的提交 */
const handleDelete = async (row: any) => { const handleCategorySort = () => {
try { // 保存初始数据
// 删除的二次确认 originalData.value = cloneDeep(categoryGroup.value)
await message.delConfirm() isCategorySorting.value = true
// 发起删除
await ModelApi.deleteModel(row.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
} }
/** 更新状态操作 */ /** 分类排序的取消 */
const handleChangeState = async (row: any) => { const handleCategorySortCancel = () => {
const state = row.processDefinition.suspensionState // 恢复初始数据
const newState = state === 1 ? 2 : 1 categoryGroup.value = cloneDeep(originalData.value)
try { isCategorySorting.value = false
// 修改状态的二次确认
const id = row.id
debugger
const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
// 发起修改状态
await ModelApi.updateModelState(id, newState)
message.success(statusState + '成功')
// 刷新列表
await getList()
} catch {}
} }
/** 设计流程 */ /** 分类排序的保存 */
const handleDesign = (row: any) => { const handleCategorySortSubmit = async () => {
if (row.type == BpmModelType.BPMN) { // 保存排序
push({ const ids = categoryGroup.value.map((item: any) => item.id)
name: 'BpmModelEditor', await CategoryApi.updateCategorySortBatch(ids)
query: {
modelId: row.id
}
})
} else {
push({
name: 'SimpleWorkflowDesignEditor',
query: {
modelId: row.id
}
})
}
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
try {
// 删除的二次确认
await message.confirm('是否部署该流程!!')
// 发起部署
await ModelApi.deployModel(row.id)
message.success(t('部署成功'))
// 刷新列表 // 刷新列表
isCategorySorting.value = false
message.success('排序分类成功')
await getList() await getList()
} catch {}
} }
/** 跳转到指定流程定义列表 */ /** 加载数据 */
const handleDefinitionList = (row) => { const getList = async () => {
push({ loading.value = true
name: 'BpmProcessDefinition', try {
query: { // 查询模型 + 分裂的列表
key: row.key const modelList = await ModelApi.getModelList(queryParams.name)
} const categoryList = await CategoryApi.getCategorySimpleList()
}) // 按照 category 聚合
} // 注意:必须一次性赋值给 categoryGroup,否则每次操作后,列表会重新渲染,滚动条的位置会偏离!!!
categoryGroup.value = categoryList.map((category: any) => ({
/** 流程表单的详情按钮操作 */ ...category,
const formDetailVisible = ref(false) modelList: modelList.filter((model: any) => model.categoryName == category.name)
const formDetailPreview = ref({ }))
rule: [], } finally {
option: {} loading.value = false
})
const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
// 设置表单
const data = await FormApi.getForm(row.formId)
setConfAndFields2(formDetailPreview, data.conf, data.fields)
// 弹窗打开
formDetailVisible.value = true
} else {
await push({
path: row.formCustomCreatePath
})
} }
} }
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
const userId = userStore.getUser.id
return row.managerUserIds && row.managerUserIds.includes(userId)
}
/** 初始化 **/ /** 初始化 **/
onMounted(async () => { onMounted(() => {
await getList() getList()
// 查询流程分类列表
categoryList.value = await CategoryApi.getCategorySimpleList()
}) })
</script> </script>
<style lang="scss" scoped>
:deep() {
.el-table--fit .el-table__inner-wrapper:before {
height: 0;
}
.el-card {
border-radius: 8px;
}
.el-form--inline .el-form-item {
margin-right: 10px;
}
.el-divider--horizontal {
margin-top: 6px;
}
}
</style>
<template>
<ContentWrap>
<div class="flex justify-between pl-20px items-center">
<h3 class="font-extrabold">表单管理</h3>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px flex"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item align="right" prop="key" class="ml-auto">
<el-input
v-model="queryParams.key"
placeholder="搜索流程"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
>
<template #prefix>
<Icon icon="ep:search" class="mx-10px" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新建流程
</el-button>
</el-form-item>
<el-form-item>
<el-dropdown placement="bottom-end">
<el-button class="w-30px" plain>
<Icon icon="ep:setting" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<Icon icon="ep:circle-plus" size="13" class="mr-5px" />
新建分组
</el-dropdown-item>
<el-dropdown-item>
<Icon icon="fa:sort-amount-desc" size="13" class="mr-5px" />
分组排序
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-form-item>
</el-form>
</div>
<el-divider />
<!-- 分类卡片组 -->
<div class="px-15px">
<ContentWrap :body-style="{ padding: 0 }" v-for="(list, title) in categoryGroup" :key="title">
<!-- 默认使其全部展开 -->
<el-collapse :modelValue="title">
<el-collapse-item :name="title">
<template #icon="{ isActive }">
<div
class="ml-20px flex items-center"
:class="['transition-transform duration-300', isActive ? 'rotate-180' : 'rotate-0']"
>
<Icon icon="ep:arrow-down-bold" color="#999" />
</div>
<div class="ml-auto mr-30px">
<el-button link type="info" class="mr-10px" @click.stop="handleSort">
<Icon icon="fa:sort-amount-desc" class="mr-5px" />
排序
</el-button>
<el-button link type="info" @click.stop="handleGroup">
<Icon icon="ep:setting" class="mr-5px" />
分组
</el-button>
</div>
</template>
<template #title>
<div class="flex items-center">
<h3 class="ml-20px mr-8px text-18px">{{ title }}</h3>
<div class="color-gray-600 text-16px"> ({{ list?.length || 0 }}) </div>
</div>
</template>
<el-table
:header-cell-style="{ backgroundColor: isDark ? '' : '#edeff0' }"
v-loading="loading"
:data="list"
>
<el-table-column label="流程名" prop="name" min-width="150">
<template #default="scope">
<div class="flex items-center">
<el-image :src="scope.row.icon" class="h-32px w-32px mr-10px rounded" />
{{ scope.row.name }}
</div>
</template>
</el-table-column>
<el-table-column label="可见范围" prop="startUserIds" min-width="100">
<template #default="scope">
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见
</el-text>
<el-text v-else-if="scope.row.startUsers.length == 1">
{{ scope.row.startUsers[0].nickname }}
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
>
{{ scope.row.startUsers[0].nickname }}
{{ scope.row.startUsers.length }} 人可见
</el-tooltip>
</el-text>
</template>
</el-table-column>
<el-table-column label="表单信息" prop="formType" min-width="200">
<template #default="scope">
<el-button
v-if="scope.row.formType === 10"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formName }}</span>
</el-button>
<el-button
v-else-if="scope.row.formType === 20"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formCustomCreatePath }}</span>
</el-button>
<label v-else>暂无表单</label>
</template>
</el-table-column>
<el-table-column label="最后发布" prop="deploymentTime" min-width="250">
<template #default="scope">
<span v-if="scope.row.processDefinition">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
<el-tag v-if="scope.row.processDefinition" class="ml-10px">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
<el-tag
v-if="scope.row.processDefinition?.suspensionState === 2"
type="warning"
class="ml-10px"
>
已停用
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
修改
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
设计
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
:disabled="!isManagerUser(scope.row)"
>
发布
</el-button>
<el-dropdown
class="!align-middle ml-5px"
@command="(command) => handleCommand(command, scope.row)"
v-hasPermi="[
'bpm:process-definition:query',
'bpm:model:update',
'bpm:model:delete'
]"
>
<el-button type="primary" link>更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleDefinitionList"
v-if="checkPermi(['bpm:process-definition:query'])"
>
历史
</el-dropdown-item>
<el-dropdown-item
command="handleChangeState"
v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
:disabled="!isManagerUser(scope.row)"
>
{{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleDelete"
v-if="checkPermi(['bpm:model:delete'])"
:disabled="!isManagerUser(scope.row)"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</el-collapse-item>
</el-collapse>
</ContentWrap>
</div>
</ContentWrap>
<!-- 表单弹窗:添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" />
<!-- 弹窗:表单详情 -->
<Dialog title="表单详情" v-model="formDetailVisible" width="800">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
</Dialog>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import ModelForm from './ModelForm.vue'
import { setConfAndFields2 } from '@/utils/formCreate'
import { CategoryApi } from '@/api/bpm/category'
import { BpmModelType } from '@/utils/constants'
import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user'
import { useAppStore } from '@/store/modules/app'
import { groupBy } from 'lodash-es'
defineOptions({ name: 'BpmModel' })
const appStore = useAppStore()
const message = useMessage() // 消息弹窗
const isDark = computed(() => appStore.getIsDark)
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const userStore = useUserStoreWithOut() // 用户信息缓存
const loading = ref(true) // 列表的加载中
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
key: undefined,
name: undefined,
category: undefined
})
const queryFormRef = ref() // 搜索的表单
const categoryList = ref([]) // 流程分类列表
const categoryGroup = ref<any>({}) // 按照category分组的数据
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
// TODO 芋艿:这里需要一个不分页查全部的流程模型接口
const data = await ModelApi.getModelPage(queryParams)
categoryGroup.value = groupBy(data.list, 'categoryName')
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** '更多'操作按钮 */
const handleCommand = (command: string, row: any) => {
switch (command) {
case 'handleDefinitionList':
handleDefinitionList(row)
break
case 'handleDelete':
handleDelete(row)
break
case 'handleChangeState':
handleChangeState(row)
break
default:
break
}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (row: any) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ModelApi.deleteModel(row.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row: any) => {
const state = row.processDefinition.suspensionState
const newState = state === 1 ? 2 : 1
try {
// 修改状态的二次确认
const id = row.id
debugger
const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
// 发起修改状态
await ModelApi.updateModelState(id, newState)
message.success(statusState + '成功')
// 刷新列表
await getList()
} catch {}
}
/** 设计流程 */
const handleDesign = (row: any) => {
if (row.type == BpmModelType.BPMN) {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
} else {
push({
name: 'SimpleWorkflowDesignEditor',
query: {
modelId: row.id
}
})
}
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
try {
// 删除的二次确认
await message.confirm('是否部署该流程!!')
// 发起部署
await ModelApi.deployModel(row.id)
message.success(t('部署成功'))
// 刷新列表
await getList()
} catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row) => {
push({
name: 'BpmProcessDefinition',
query: {
key: row.key
}
})
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
rule: [],
option: {}
})
const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
// 设置表单
const data = await FormApi.getForm(row.formId)
setConfAndFields2(formDetailPreview, data.conf, data.fields)
// 弹窗打开
formDetailVisible.value = true
} else {
await push({
path: row.formCustomCreatePath
})
}
}
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
const userId = userStore.getUser.id
return row.managerUserIds && row.managerUserIds.includes(userId)
}
/* 排序 */
const handleSort = () => {
console.log('排序')
}
/* 分组 */
const handleGroup = () => {
console.log('分组')
}
/** 初始化 **/
onMounted(async () => {
await getList()
// 查询流程分类列表
categoryList.value = await CategoryApi.getCategorySimpleList()
})
</script>
<style lang="scss" scoped>
:deep() {
.el-form--inline .el-form-item {
margin-right: 10px;
}
.el-divider--horizontal {
margin-top: 6px;
}
.el-collapse,
.el-collapse-item__header,
.el-collapse-item__wrap {
border: none;
}
.el-collapse-item__arrow {
margin-left: 10px;
font-size: 20px;
font-weight: 500;
}
}
</style>
<template>
<doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
<doc-alert
title="流程设计器(钉钉、飞书)"
url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
/>
<doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
<doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="流程标识" prop="key">
<el-input
v-model="queryParams.key"
placeholder="请输入流程标识"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入流程名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="流程分类" prop="category">
<el-select
v-model="queryParams.category"
placeholder="请选择流程分类"
clearable
class="!w-240px"
>
<el-option
v-for="category in categoryList"
:key="category.code"
:label="category.name"
:value="category.code"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['bpm:model:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新建
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="流程名称" align="center" prop="name" min-width="200" />
<el-table-column label="流程图标" align="center" prop="icon" min-width="100">
<template #default="scope">
<el-image :src="scope.row.icon" class="h-32px w-32px" />
</template>
</el-table-column>
<el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
<template #default="scope">
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见
</el-text>
<el-text v-else-if="scope.row.startUsers.length == 1">
{{ scope.row.startUsers[0].nickname }}
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
>
{{ scope.row.startUsers[0].nickname }}{{ scope.row.startUsers.length }} 人可见
</el-tooltip>
</el-text>
</template>
</el-table-column>
<el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
<el-table-column label="表单信息" align="center" prop="formType" min-width="200">
<template #default="scope">
<el-button
v-if="scope.row.formType === 10"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formName }}</span>
</el-button>
<el-button
v-else-if="scope.row.formType === 20"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formCustomCreatePath }}</span>
</el-button>
<label v-else>暂无表单</label>
</template>
</el-table-column>
<el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
<template #default="scope">
<span v-if="scope.row.processDefinition">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
<el-tag v-if="scope.row.processDefinition" class="ml-10px">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
<el-tag
v-if="scope.row.processDefinition?.suspensionState === 2"
type="warning"
class="ml-10px"
>
已停用
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
修改
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
设计
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
:disabled="!isManagerUser(scope.row)"
>
发布
</el-button>
<el-dropdown
class="!align-middle ml-5px"
@command="(command) => handleCommand(command, scope.row)"
v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
>
<el-button type="primary" link>更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleDefinitionList"
v-if="checkPermi(['bpm:process-definition:query'])"
>
历史
</el-dropdown-item>
<el-dropdown-item
command="handleChangeState"
v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
:disabled="!isManagerUser(scope.row)"
>
{{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleDelete"
v-if="checkPermi(['bpm:model:delete'])"
:disabled="!isManagerUser(scope.row)"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗:添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" />
<!-- 弹窗:表单详情 -->
<Dialog title="表单详情" v-model="formDetailVisible" width="800">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
</Dialog>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import ModelForm from './ModelForm.vue'
import { setConfAndFields2 } from '@/utils/formCreate'
import { CategoryApi } from '@/api/bpm/category'
import { BpmModelType } from '@/utils/constants'
import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'BpmModel' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const userStore = useUserStoreWithOut() // 用户信息缓存
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
key: undefined,
name: undefined,
category: undefined
})
const queryFormRef = ref() // 搜索的表单
const categoryList = ref([]) // 流程分类列表
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ModelApi.getModelList(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** '更多'操作按钮 */
const handleCommand = (command: string, row: any) => {
switch (command) {
case 'handleDefinitionList':
handleDefinitionList(row)
break
case 'handleDelete':
handleDelete(row)
break
case 'handleChangeState':
handleChangeState(row)
break
default:
break
}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (row: any) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ModelApi.deleteModel(row.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row: any) => {
const state = row.processDefinition.suspensionState
const newState = state === 1 ? 2 : 1
try {
// 修改状态的二次确认
const id = row.id
debugger
const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
// 发起修改状态
await ModelApi.updateModelState(id, newState)
message.success(statusState + '成功')
// 刷新列表
await getList()
} catch {}
}
/** 设计流程 */
const handleDesign = (row: any) => {
if (row.type == BpmModelType.BPMN) {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
} else {
push({
name: 'SimpleModelDesign',
query: {
modelId: row.id
}
})
}
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
try {
// 删除的二次确认
await message.confirm('是否部署该流程!!')
// 发起部署
await ModelApi.deployModel(row.id)
message.success(t('部署成功'))
// 刷新列表
await getList()
} catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row) => {
push({
name: 'BpmProcessDefinition',
query: {
key: row.key
}
})
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
rule: [],
option: {}
})
const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
// 设置表单
const data = await FormApi.getForm(row.formId)
setConfAndFields2(formDetailPreview, data.conf, data.fields)
// 弹窗打开
formDetailVisible.value = true
} else {
await push({
path: row.formCustomCreatePath
})
}
}
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
const userId = userStore.getUser.id
return row.managerUserIds && row.managerUserIds.includes(userId)
}
/** 初始化 **/
onMounted(async () => {
await getList()
// 查询流程分类列表
categoryList.value = await CategoryApi.getCategorySimpleList()
})
</script>
<template>
<ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
<div class="processInstance-wrap-main">
<el-scrollbar>
<div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div>
<el-divider class="!my-8px" />
<!-- 中间主要内容 tab 栏 -->
<el-tabs v-model="activeTab">
<!-- 表单信息 -->
<el-tab-pane label="表单填写" name="form">
<div class="form-scroll-area">
<el-scrollbar>
<el-row>
<el-col :span="17">
<form-create
:rule="detailForm.rule"
v-model:api="fApi"
v-model="detailForm.value"
:option="detailForm.option"
@submit="submitForm"
/>
</el-col>
<el-col :span="6" :offset="1">
<!-- 流程时间线 -->
<ProcessInstanceTimeline
ref="timelineRef"
:activity-nodes="activityNodes"
:show-status-icon="false"
@select-user-confirm="selectUserConfirm"
/>
</el-col>
</el-row>
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 流程图 -->
<el-tab-pane label="流程图" name="diagram">
<div class="form-scroll-area">
<!-- BPMN 流程图预览 -->
<ProcessInstanceBpmnViewer
:bpmn-xml="bpmnXML"
v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
/>
<!-- Simple 流程图预览 -->
<ProcessInstanceSimpleViewer
:simple-json="simpleJson"
v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
/>
</div>
</el-tab-pane>
</el-tabs>
<!-- 底部操作栏 -->
<div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
<!-- 操作栏按钮 -->
<div
v-if="activeTab === 'form'"
class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
>
<el-button plain type="success" @click="submitForm">
<Icon icon="ep:select" />&nbsp; 发起
</el-button>
<el-button plain type="danger" @click="handleCancel">
<Icon icon="ep:close" />&nbsp; 取消
</el-button>
</div>
</div>
</el-scrollbar>
</div>
</ContentWrap>
</template>
<script lang="ts" setup>
import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
import { BpmModelType } from '@/utils/constants'
import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition'
import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
defineOptions({ name: 'ProcessDefinitionDetail' })
const props = defineProps<{
selectProcessDefinition: any
}>()
const emit = defineEmits(['cancel'])
const { push, currentRoute } = useRouter() // 路由
const message = useMessage() // 消息弹窗
const { delView } = useTagsViewStore() // 视图操作
const detailForm: any = ref({
rule: [],
option: {},
value: {}
}) // 流程表单详情
const fApi = ref<ApiAttrs>()
// 指定审批人
const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人或抄送人的任务列表
const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
const bpmnXML: any = ref(null) // BPMN 数据
const simpleJson = ref<string | undefined>() // Simple 设计器数据 json 格式
const activeTab = ref('form') // 当前的 Tab
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
/** 设置表单信息、获取流程图数据 **/
const initProcessInfo = async (row: any, formVariables?: any) => {
// 重置指定审批人
startUserSelectTasks.value = []
startUserSelectAssignees.value = {}
// 情况一:流程表单
if (row.formType == 10) {
// 设置表单
// 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
// 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
// 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
for (const key in formVariables) {
if (!allowedFields.includes(key)) {
delete formVariables[key]
}
}
setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
await nextTick()
fApi.value?.btn.show(false) // 隐藏提交按钮
// 获取流程审批信息
await getApprovalDetail(row)
// 加载流程图
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml
simpleJson.value = processDefinitionDetail.simpleModel
}
// 情况二:业务表单
} else if (row.formCustomCreatePath) {
await push({
path: row.formCustomCreatePath
})
// 这里暂时无需加载流程图,因为跳出到另外个 Tab;
}
}
/** 获取审批详情 */
const getApprovalDetail = async (row: any) => {
try {
const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id })
if (!data) {
message.error('查询不到审批详情信息!')
return
}
// 获取发起人自选的任务
startUserSelectTasks.value = data.activityNodes?.filter(
(node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
)
if (startUserSelectTasks.value?.length > 0) {
for (const node of startUserSelectTasks.value) {
startUserSelectAssignees.value[node.id] = []
}
}
// 获取审批节点,显示 Timeline 的数据
activityNodes.value = data.activityNodes
} finally {
}
}
/** 提交按钮 */
const submitForm = async () => {
if (!fApi.value || !props.selectProcessDefinition) {
return
}
// 如果有指定审批人,需要校验
if (startUserSelectTasks.value?.length > 0) {
for (const userTask of startUserSelectTasks.value) {
if (
Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
startUserSelectAssignees.value[userTask.id].length === 0
)
return message.warning(`请选择${userTask.name}的候选人`)
}
}
// 提交请求
fApi.value.btn.loading(true)
try {
await ProcessInstanceApi.createProcessInstance({
processDefinitionId: props.selectProcessDefinition.id,
variables: detailForm.value.value,
startUserSelectAssignees: startUserSelectAssignees.value
})
// 提示
message.success('发起流程成功')
// 跳转回去
delView(unref(currentRoute))
await push({
name: 'BpmProcessInstanceMy'
})
} finally {
fApi.value.btn.loading(false)
}
}
/** 取消发起审批 */
const handleCancel = () => {
emit('cancel')
}
/** 选择发起人 */
const selectUserConfirm = (id: string, userList: any[]) => {
startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
}
defineExpose({ initProcessInfo })
</script>
<style lang="scss" scoped>
$wrap-padding-height: 20px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 105px;
.processInstance-wrap-main {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
);
overflow: auto;
.form-scroll-area {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
$process-header-height - 40px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
$process-header-height - 40px
);
overflow: auto;
}
}
.form-box {
:deep(.el-card) {
border: none;
}
}
</style>
<template> <template>
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<!-- 第一步,通过流程定义的列表,选择对应的流程 --> <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
<ContentWrap v-if="!selectProcessDefinition" v-loading="loading"> <template v-if="!selectProcessDefinition">
<el-tabs tab-position="left" v-model="categoryActive"> <el-input
<el-tab-pane v-model="searchName"
:label="category.name" class="!w-50% mb-15px"
:name="category.code" placeholder="请输入流程名称"
clearable
@input="handleQuery"
@clear="handleQuery"
>
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
<ContentWrap
:class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
class="position-relative pb-20px h-700px"
v-loading="loading"
>
<el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
<el-col :span="5">
<div class="flex flex-col">
<div
v-for="category in availableCategories"
:key="category.code" :key="category.code"
v-for="category in categoryList" class="flex items-center p-10px cursor-pointer text-14px rounded-md"
:class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
@click="handleCategoryClick(category)"
> >
<el-row :gutter="20"> {{ category.name }}
<el-col </div>
:lg="6" </div>
:sm="12" </el-col>
:xs="24" <el-col :span="19">
v-for="definition in categoryProcessDefinitionList" <el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll">
<div
class="mb-20px pl-10px"
v-for="(definitions, categoryCode) in processDefinitionGroup"
:key="categoryCode"
:ref="`category-${categoryCode}`"
>
<h3 class="text-18px font-bold mb-10px mt-5px">
{{ getCategoryName(categoryCode as any) }}
</h3>
<div class="grid grid-cols-3 gap3">
<el-tooltip
v-for="definition in definitions"
:key="definition.id" :key="definition.id"
:content="definition.description"
:disabled="!definition.description || definition.description.trim().length === 0"
placement="top"
> >
<el-card <el-card
shadow="hover" shadow="hover"
class="mb-20px cursor-pointer" class="cursor-pointer definition-item-card"
@click="handleSelect(definition)" @click="handleSelect(definition)"
> >
<template #default> <template #default>
...@@ -30,104 +63,53 @@ ...@@ -30,104 +63,53 @@
</div> </div>
</template> </template>
</el-card> </el-card>
</el-tooltip>
</div>
</div>
</el-scrollbar>
</el-col> </el-col>
</el-row> </el-row>
</el-tab-pane> <el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else />
</el-tabs>
</ContentWrap> </ContentWrap>
</template>
<!-- 第二步,填写表单,进行流程的提交 --> <!-- 第二步,填写表单,进行流程的提交 -->
<ContentWrap v-else> <ProcessDefinitionDetail
<el-card class="box-card"> v-else
<div class="clearfix"> ref="processDefinitionDetailRef"
<span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span> :selectProcessDefinition="selectProcessDefinition"
<el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined"> @cancel="selectProcessDefinition = undefined"
<Icon icon="ep:delete" /> 选择其它流程
</el-button>
</div>
<el-col :span="16" :offset="6" style="margin-top: 20px">
<form-create
:rule="detailForm.rule"
v-model:api="fApi"
v-model="detailForm.value"
:option="detailForm.option"
@submit="submitForm"
>
<template #type-startUserSelect>
<el-col :span="24">
<el-card class="mb-10px">
<template #header>指定审批人</template>
<el-form
:model="startUserSelectAssignees"
:rules="startUserSelectAssigneesFormRules"
ref="startUserSelectAssigneesFormRef"
>
<el-form-item
v-for="userTask in startUserSelectTasks"
:key="userTask.id"
:label="`任务【${userTask.name}】`"
:prop="userTask.id"
>
<el-select
v-model="startUserSelectAssignees[userTask.id]"
multiple
placeholder="请选择审批人"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/> />
</el-select>
</el-form-item>
</el-form>
</el-card>
</el-col>
</template>
</form-create>
</el-col>
</el-card>
<!-- 流程图预览 -->
<ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
</ContentWrap>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as DefinitionApi from '@/api/bpm/definition' import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance' import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { setConfAndFields2 } from '@/utils/formCreate' import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import type { ApiAttrs } from '@form-create/element-ui/types/config' import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue' import { groupBy } from 'lodash-es'
import { CategoryApi } from '@/api/bpm/category'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'BpmProcessInstanceCreate' }) defineOptions({ name: 'BpmProcessInstanceCreate' })
const { proxy } = getCurrentInstance() as any
const route = useRoute() // 路由 const route = useRoute() // 路由
const { push, currentRoute } = useRouter() // 路由
const message = useMessage() // 消息 const message = useMessage() // 消息
const { delView } = useTagsViewStore() // 视图操作
const processInstanceId = route.query.processInstanceId const searchName = ref('') // 当前搜索关键字
const processInstanceId: any = route.query.processInstanceId // 流程实例编号。场景:重新发起时
const loading = ref(true) // 加载中 const loading = ref(true) // 加载中
const categoryList = ref([]) // 分类的列表 const categoryList: any = ref([]) // 分类的列表
const categoryActive = ref('') // 选中的分类 const categoryActive: any = ref({}) // 选中的分类
const processDefinitionList = ref([]) // 流程定义的列表 const processDefinitionList = ref([]) // 流程定义的列表
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
// 流程分类 // 所有流程分类数据
categoryList.value = await CategoryApi.getCategorySimpleList() await getCategoryList()
if (categoryList.value.length > 0) { // 所有流程定义数据
categoryActive.value = categoryList.value[0].code await getProcessDefinitionList()
}
// 流程定义
processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
suspensionState: 1
})
// 如果 processInstanceId 非空,说明是重新发起 // 如果 processInstanceId 非空,说明是重新发起
if (processInstanceId?.length > 0) { if (processInstanceId?.length > 0) {
...@@ -137,7 +119,7 @@ const getList = async () => { ...@@ -137,7 +119,7 @@ const getList = async () => {
return return
} }
const processDefinition = processDefinitionList.value.find( const processDefinition = processDefinitionList.value.find(
(item) => item.key == processInstance.processDefinition?.key (item: any) => item.key == processInstance.processDefinition?.key
) )
if (!processDefinition) { if (!processDefinition) {
message.error('重新发起流程失败,原因:流程定义不存在') message.error('重新发起流程失败,原因:流程定义不存在')
...@@ -150,108 +132,168 @@ const getList = async () => { ...@@ -150,108 +132,168 @@ const getList = async () => {
} }
} }
/** 选中分类对应的流程定义列表 */ /** 获取所有流程分类数据 */
const categoryProcessDefinitionList = computed(() => { const getCategoryList = async () => {
return processDefinitionList.value.filter((item) => item.category == categoryActive.value) try {
// 流程分类
categoryList.value = await CategoryApi.getCategorySimpleList()
} finally {
}
}
/** 获取所有流程定义数据 */
const getProcessDefinitionList = async () => {
try {
// 流程定义
processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
suspensionState: 1
})
// 初始化过滤列表为全部流程定义
filteredProcessDefinitionList.value = processDefinitionList.value
// 在获取完所有数据后,设置第一个有效分类为激活状态
if (availableCategories.value.length > 0 && !categoryActive.value?.code) {
categoryActive.value = availableCategories.value[0]
}
} finally {
}
}
/** 搜索流程 */
const filteredProcessDefinitionList = ref([]) // 用于存储搜索过滤后的流程定义
const handleQuery = () => {
if (searchName.value.trim()) {
// 如果有搜索关键字,进行过滤
filteredProcessDefinitionList.value = processDefinitionList.value.filter(
(definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) // 假设搜索依据是流程定义的名称
)
} else {
// 如果没有搜索关键字,恢复所有数据
filteredProcessDefinitionList.value = processDefinitionList.value
}
}
/** 流程定义的分组 */
const processDefinitionGroup: any = computed(() => {
if (!processDefinitionList.value?.length) {
return {}
}
const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
// 按照 categoryList 的顺序重新组织数据
const orderedGroup = {}
categoryList.value.forEach((category: any) => {
if (grouped[category.code]) {
orderedGroup[category.code] = grouped[category.code]
}
})
return orderedGroup
}) })
/** 左侧分类切换 */
const handleCategoryClick = (category: any) => {
categoryActive.value = category
const categoryRef = proxy.$refs[`category-${category.code}`] // 获取点击分类对应的 DOM 元素
if (categoryRef?.length) {
const scrollWrapper = proxy.$refs.scrollWrapper // 获取右侧滚动容器
const categoryOffsetTop = categoryRef[0].offsetTop
// 滚动到对应位置
scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
}
}
/** 通过分类 code 获取对应的名称 */
const getCategoryName = (categoryCode: string) => {
return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name
}
// ========== 表单相关 ========== // ========== 表单相关 ==========
const fApi = ref<ApiAttrs>()
const detailForm = ref({
rule: [],
option: {},
value: {}
}) // 流程表单详情
const selectProcessDefinition = ref() // 选择的流程定义 const selectProcessDefinition = ref() // 选择的流程定义
const processDefinitionDetailRef = ref()
// 指定审批人
const bpmnXML = ref(null) // BPMN 数据
const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
const userList = ref<any[]>([]) // 用户列表
/** 处理选择流程的按钮操作 **/ /** 处理选择流程的按钮操作 **/
const handleSelect = async (row, formVariables) => { const handleSelect = async (row, formVariables?) => {
// 设置选择的流程 // 设置选择的流程
selectProcessDefinition.value = row selectProcessDefinition.value = row
// 初始化流程定义详情
await nextTick()
processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
}
// 重置指定审批人 /** 处理滚动事件,和左侧分类联动 */
startUserSelectTasks.value = [] const handleScroll = (e: any) => {
startUserSelectAssignees.value = {} // 直接使用事件对象获取滚动位置
startUserSelectAssigneesFormRules.value = {} const scrollTop = e.scrollTop
// 情况一:流程表单
if (row.formType == 10) {
// 设置表单
setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
// 加载流程图
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml
startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
// 设置指定审批人 // 获取所有分类区域的位置信息
if (startUserSelectTasks.value?.length > 0) { const categoryPositions = categoryList.value
detailForm.value.rule.push({ .map((category: CategoryVO) => {
type: 'startUserSelect', const categoryRef = proxy.$refs[`category-${category.code}`]
props: { if (categoryRef?.[0]) {
title: '指定审批人' return {
code: category.code,
offsetTop: categoryRef[0].offsetTop,
height: categoryRef[0].offsetHeight
} }
})
// 设置校验规则
for (const userTask of startUserSelectTasks.value) {
startUserSelectAssignees.value[userTask.id] = []
startUserSelectAssigneesFormRules.value[userTask.id] = [
{ required: true, message: '请选择审批人', trigger: 'blur' }
]
} }
// 加载用户列表 return null
userList.value = await UserApi.getSimpleUserList()
}
}
// 情况二:业务表单
} else if (row.formCustomCreatePath) {
await push({
path: row.formCustomCreatePath
}) })
// 这里暂时无需加载流程图,因为跳出到另外个 Tab; .filter(Boolean)
}
}
/** 提交按钮 */ // 查找当前滚动位置对应的分类
const submitForm = async (formData) => { let currentCategory = categoryPositions[0]
if (!fApi.value || !selectProcessDefinition.value) { for (const position of categoryPositions) {
return // 为了更好的用户体验,可以添加一个缓冲区域(比如 50px)
if (scrollTop >= position.offsetTop - 50) {
currentCategory = position
} else {
break
} }
// 如果有指定审批人,需要校验
if (startUserSelectTasks.value?.length > 0) {
await startUserSelectAssigneesFormRef.value.validate()
} }
// 提交请求 // 更新当前 active 的分类
fApi.value.btn.loading(true) if (currentCategory && categoryActive.value.code !== currentCategory.code) {
try { categoryActive.value = categoryList.value.find(
await ProcessInstanceApi.createProcessInstance({ (c: CategoryVO) => c.code === currentCategory.code
processDefinitionId: selectProcessDefinition.value.id, )
variables: formData,
startUserSelectAssignees: startUserSelectAssignees.value
})
// 提示
message.success('发起流程成功')
// 跳转回去
delView(unref(currentRoute))
await push({
name: 'BpmProcessInstanceMy'
})
} finally {
fApi.value.btn.loading(false)
} }
} }
/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
const availableCategories = computed(() => {
if (!categoryList.value?.length || !processDefinitionGroup.value) {
return []
}
// 获取所有有流程的分类代码
const availableCategoryCodes = Object.keys(processDefinitionGroup.value)
// 过滤出有流程的分类
return categoryList.value.filter((category: CategoryVO) =>
availableCategoryCodes.includes(category.code)
)
})
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {
getList() getList()
}) })
</script> </script>
<style lang="scss" scoped>
.process-definition-container::before {
content: '';
border-left: 1px solid #e6e6e6;
position: absolute;
left: 20.8%;
height: 100%;
}
:deep() {
.definition-item-card {
.el-card__body {
padding: 14px;
}
}
}
</style>
<template>
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<!-- 第一步,通过流程定义的列表,选择对应的流程 -->
<ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
<el-tabs tab-position="left" v-model="categoryActive">
<el-tab-pane
:label="category.name"
:name="category.code"
:key="category.code"
v-for="category in categoryList"
>
<el-row :gutter="20">
<el-col
:lg="6"
:sm="12"
:xs="24"
v-for="definition in categoryProcessDefinitionList"
:key="definition.id"
>
<el-card
shadow="hover"
class="mb-20px cursor-pointer"
@click="handleSelect(definition)"
>
<template #default>
<div class="flex">
<el-image :src="definition.icon" class="w-32px h-32px" />
<el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
</div>
</template>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</ContentWrap>
<!-- 第二步,填写表单,进行流程的提交 -->
<ContentWrap v-else>
<el-card class="box-card">
<div class="clearfix">
<span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
<el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
<Icon icon="ep:delete" /> 选择其它流程
</el-button>
</div>
<el-col :span="16" :offset="6" style="margin-top: 20px">
<form-create
:rule="detailForm.rule"
v-model:api="fApi"
v-model="detailForm.value"
:option="detailForm.option"
@submit="submitForm"
>
<template #type-startUserSelect>
<el-col :span="24">
<el-card class="mb-10px">
<template #header>指定审批人</template>
<el-form
:model="startUserSelectAssignees"
:rules="startUserSelectAssigneesFormRules"
ref="startUserSelectAssigneesFormRef"
>
<el-form-item
v-for="userTask in startUserSelectTasks"
:key="userTask.id"
:label="`任务【${userTask.name}】`"
:prop="userTask.id"
>
<el-select
v-model="startUserSelectAssignees[userTask.id]"
multiple
placeholder="请选择审批人"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/>
</el-select>
</el-form-item>
</el-form>
</el-card>
</el-col>
</template>
</form-create>
</el-col>
</el-card>
<!-- 流程图预览 -->
<ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
import { CategoryApi } from '@/api/bpm/category'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'BpmProcessInstanceCreate' })
const route = useRoute() // 路由
const { push, currentRoute } = useRouter() // 路由
const message = useMessage() // 消息
const { delView } = useTagsViewStore() // 视图操作
const processInstanceId = route.query.processInstanceId
const loading = ref(true) // 加载中
const categoryList = ref([]) // 分类的列表
const categoryActive = ref('') // 选中的分类
const processDefinitionList = ref([]) // 流程定义的列表
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
// 流程分类
categoryList.value = await CategoryApi.getCategorySimpleList()
if (categoryList.value.length > 0) {
categoryActive.value = categoryList.value[0].code
}
// 流程定义
processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
suspensionState: 1
})
// 如果 processInstanceId 非空,说明是重新发起
if (processInstanceId?.length > 0) {
const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
if (!processInstance) {
message.error('重新发起流程失败,原因:流程实例不存在')
return
}
const processDefinition = processDefinitionList.value.find(
(item) => item.key == processInstance.processDefinition?.key
)
if (!processDefinition) {
message.error('重新发起流程失败,原因:流程定义不存在')
return
}
await handleSelect(processDefinition, processInstance.formVariables)
}
} finally {
loading.value = false
}
}
/** 选中分类对应的流程定义列表 */
const categoryProcessDefinitionList = computed(() => {
return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
})
// ========== 表单相关 ==========
const fApi = ref<ApiAttrs>()
const detailForm = ref({
rule: [],
option: {},
value: {}
}) // 流程表单详情
const selectProcessDefinition = ref() // 选择的流程定义
// 指定审批人
const bpmnXML = ref(null) // BPMN 数据
const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
const userList = ref<any[]>([]) // 用户列表
/** 处理选择流程的按钮操作 **/
const handleSelect = async (row, formVariables) => {
// 设置选择的流程
selectProcessDefinition.value = row
// 重置指定审批人
startUserSelectTasks.value = []
startUserSelectAssignees.value = {}
startUserSelectAssigneesFormRules.value = {}
// 情况一:流程表单
if (row.formType == 10) {
// 设置表单
// 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
// 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
// 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
for (const key in formVariables) {
if (!allowedFields.includes(key)) {
delete formVariables[key]
}
}
setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
// 加载流程图
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml
startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
// 设置指定审批人
if (startUserSelectTasks.value?.length > 0) {
detailForm.value.rule.push({
type: 'startUserSelect',
props: {
title: '指定审批人'
}
})
// 设置校验规则
for (const userTask of startUserSelectTasks.value) {
startUserSelectAssignees.value[userTask.id] = []
startUserSelectAssigneesFormRules.value[userTask.id] = [
{ required: true, message: '请选择审批人', trigger: 'blur' }
]
}
// 加载用户列表
userList.value = await UserApi.getSimpleUserList()
}
}
// 情况二:业务表单
} else if (row.formCustomCreatePath) {
await push({
path: row.formCustomCreatePath
})
// 这里暂时无需加载流程图,因为跳出到另外个 Tab;
}
}
/** 提交按钮 */
const submitForm = async (formData) => {
if (!fApi.value || !selectProcessDefinition.value) {
return
}
// 如果有指定审批人,需要校验
if (startUserSelectTasks.value?.length > 0) {
await startUserSelectAssigneesFormRef.value.validate()
}
// 提交请求
fApi.value.btn.loading(true)
try {
await ProcessInstanceApi.createProcessInstance({
processDefinitionId: selectProcessDefinition.value.id,
variables: formData,
startUserSelectAssignees: startUserSelectAssignees.value
})
// 提示
message.success('发起流程成功')
// 跳转回去
delView(unref(currentRoute))
await push({
name: 'BpmProcessInstanceMy'
})
} finally {
fApi.value.btn.loading(false)
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>
<template> <template>
<el-card v-loading="loading" class="box-card"> <el-card v-loading="loading" class="box-card">
<template #header> <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
<span class="el-icon-picture-outline">流程图</span>
</template>
<MyProcessViewer
key="designer"
:activityData="activityList"
:prefix="bpmnControlForm.prefix"
:processInstanceData="processInstance"
:taskData="tasks"
:value="bpmnXml"
v-bind="bpmnControlForm"
/>
</el-card> </el-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package' import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
import * as ActivityApi from '@/api/bpm/activity'
defineOptions({ name: 'BpmProcessInstanceBpmnViewer' }) defineOptions({ name: 'BpmProcessInstanceBpmnViewer' })
const props = defineProps({ const props = defineProps({
loading: propTypes.bool, // 是否加载中 loading: propTypes.bool.def(false), // 是否加载中
id: propTypes.string, // 流程实例的编号 bpmnXml: propTypes.string, // BPMN XML
processInstance: propTypes.any, // 流程实例的信息 modelView: propTypes.object
tasks: propTypes.array, // 流程任务的数组
bpmnXml: propTypes.string // BPMN XML
}) })
const bpmnControlForm = ref({ const view = ref({
prefix: 'flowable' bpmnXml: ''
}) }) // BPMN 流程图数据
const activityList = ref([]) // 任务列表
/** 只有 loading 完成时,才去加载流程列表 */ /** 只有 loading 完成时,才去加载流程列表 */
watch( watch(
() => props.loading, () => props.modelView,
async (value) => { async (newModelView) => {
if (value && props.id) { // 加载最新
activityList.value = await ActivityApi.getActivityList({ if (newModelView) {
processInstanceId: props.id //@ts-ignore
}) view.value = newModelView
}
} }
)
/** 监听 bpmnXml */
watch(
() => props.bpmnXml,
(value) => {
view.value.bpmnXml = value
} }
) )
</script> </script>
<style> <style lang="scss" scoped>
.box-card { .box-card {
height: 100%;
width: 100%; width: 100%;
margin-bottom: 20px; margin-bottom: 0;
:deep(.el-card__body) {
height: 100%;
padding: 0;
}
:deep(.process-viewer) {
height: 100% !important;
min-height: 100%;
width: 100%;
overflow: auto;
}
} }
</style> </style>
<template> <template>
<div <div
class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container" class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
v-if="runningTask.id"
> >
<!-- 【通过】按钮 --> <!-- 【通过】按钮 -->
<el-popover <el-popover
:visible="passVisible" :visible="popOverVisible.approve"
placement="top-end" placement="top-end"
:width="500" :width="420"
trigger="click" trigger="click"
v-if="isShowButton(OperationButtonType.APPROVE)" v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.APPROVE)"
> >
<template #reference> <template #reference>
<el-button plain type="success" @click="openPopover('1')"> <el-button plain type="success" @click="openPopover('approve')">
<Icon icon="ep:select" />&nbsp; {{ getButtonDisplayName(OperationButtonType.APPROVE) }} <Icon icon="ep:select" />&nbsp; {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button> </el-button>
</template> </template>
...@@ -22,17 +21,11 @@ ...@@ -22,17 +21,11 @@
label-position="top" label-position="top"
class="mb-auto" class="mb-auto"
ref="formRef" ref="formRef"
:model="auditForm" :model="genericForm"
:rules="auditRule" :rules="genericRule"
label-width="100px" label-width="100px"
> >
<el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人"> <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
{{ processInstance?.startUser.nickname }}
<el-tag size="small" type="info" class="ml-8px">
{{ processInstance?.startUser.deptName }}
</el-tag>
</el-form-item>
<el-card v-if="runningTask.formId > 0" class="mb-15px !-mt-10px">
<template #header> <template #header>
<span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}</span> <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}</span>
</template> </template>
...@@ -43,25 +36,19 @@ ...@@ -43,25 +36,19 @@
:rule="approveForm.rule" :rule="approveForm.rule"
/> />
</el-card> </el-card>
<el-form-item label="审批建议" prop="reason"> <el-form-item label="审批意见" prop="reason">
<el-input v-model="auditForm.reason" placeholder="请输入审批建议" type="textarea" /> <el-input
</el-form-item> v-model="genericForm.reason"
<el-form-item label="抄送人" prop="copyUserIds"> placeholder="请输入审批意见"
<el-select v-model="auditForm.copyUserIds" multiple placeholder="请选择抄送人"> type="textarea"
<el-option :rows="4"
v-for="itemx in userOptions"
:key="itemx.id"
:label="itemx.nickname"
:value="itemx.id"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button :disabled="formLoading" type="success" @click="handleAudit(true)"> <el-button :disabled="formLoading" type="success" @click="handleAudit(true)">
{{ getButtonDisplayName(OperationButtonType.APPROVE) }} {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button> </el-button>
<el-button @click="passVisible = false"> 取消 </el-button> <el-button @click="popOverVisible.approve = false"> 取消 </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
...@@ -69,14 +56,14 @@ ...@@ -69,14 +56,14 @@
<!-- 【拒绝】按钮 --> <!-- 【拒绝】按钮 -->
<el-popover <el-popover
:visible="rejectVisible" :visible="popOverVisible.reject"
placement="top-end" placement="top-end"
:width="500" :width="420"
trigger="click" trigger="click"
v-if="isShowButton(OperationButtonType.REJECT)" v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.REJECT)"
> >
<template #reference> <template #reference>
<el-button class="mr-20px" plain type="danger" @click="openPopover('2')"> <el-button class="mr-20px" plain type="danger" @click="openPopover('reject')">
<Icon icon="ep:close" />&nbsp; {{ getButtonDisplayName(OperationButtonType.REJECT) }} <Icon icon="ep:close" />&nbsp; {{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button> </el-button>
</template> </template>
...@@ -86,17 +73,11 @@ ...@@ -86,17 +73,11 @@
label-position="top" label-position="top"
class="mb-auto" class="mb-auto"
ref="formRef" ref="formRef"
:model="auditForm" :model="genericForm"
:rules="auditRule" :rules="genericRule"
label-width="100px" label-width="100px"
> >
<el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人"> <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
{{ processInstance?.startUser.nickname }}
<el-tag size="small" type="info" class="ml-8px">
{{ processInstance?.startUser.deptName }}
</el-tag>
</el-form-item>
<el-card v-if="runningTask.formId > 0" class="mb-15px !-mt-10px">
<template #header> <template #header>
<span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}</span> <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}</span>
</template> </template>
...@@ -107,107 +88,458 @@ ...@@ -107,107 +88,458 @@
:rule="approveForm.rule" :rule="approveForm.rule"
/> />
</el-card> </el-card>
<el-form-item label="审批建议" prop="reason"> <el-form-item label="审批意见" prop="reason">
<el-input v-model="auditForm.reason" placeholder="请输入审批建议" type="textarea" /> <el-input
</el-form-item> v-model="genericForm.reason"
<el-form-item label="抄送人" prop="copyUserIds"> placeholder="请输入审批意见"
<el-select v-model="auditForm.copyUserIds" multiple placeholder="请选择抄送人"> type="textarea"
<el-option :rows="4"
v-for="itemx in userOptions"
:key="itemx.id"
:label="itemx.nickname"
:value="itemx.id"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button :disabled="formLoading" type="danger" @click="handleAudit(false)"> <el-button :disabled="formLoading" type="danger" @click="handleAudit(false)">
{{ getButtonDisplayName(OperationButtonType.REJECT) }} {{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button> </el-button>
<el-button @click="rejectVisible = false"> 取消 </el-button> <el-button @click="popOverVisible.reject = false"> 取消 </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
</el-popover> </el-popover>
<!-- 【抄送】按钮 --> <!-- 【抄送】按钮 -->
<div @click="handleSend"> <Icon :size="14" icon="svg-icon:send" />&nbsp;抄送 </div> <el-popover
:visible="popOverVisible.copy"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.COPY)"
>
<template #reference>
<div @click="openPopover('copy')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="svg-icon:send" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.COPY) }}
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="formRef"
:model="genericForm"
:rules="genericRule"
label-width="100px"
>
<el-form-item label="抄送人" prop="copyUserIds">
<el-select
v-model="genericForm.copyUserIds"
clearable
style="width: 100%"
multiple
placeholder="请选择抄送人"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="抄送意见" prop="copyReason">
<el-input
v-model="genericForm.copyReason"
clearable
placeholder="请输入抄送意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleCopy">
{{ getButtonDisplayName(OperationButtonType.COPY) }}
</el-button>
<el-button @click="popOverVisible.copy = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 【转交】按钮 --> <!-- 【转交】按钮 -->
<div @click="openTaskUpdateAssigneeForm" v-if="isShowButton(OperationButtonType.TRANSFER)"> <el-popover
:visible="popOverVisible.transfer"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.TRANSFER)"
>
<template #reference>
<div @click="openPopover('transfer')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="fa:share-square-o" />&nbsp; <Icon :size="14" icon="fa:share-square-o" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.TRANSFER) }} {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
</div> </div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="formRef"
:model="genericForm"
:rules="genericRule"
label-width="100px"
>
<el-form-item label="新审批人" prop="assigneeUserId">
<el-select v-model="genericForm.assigneeUserId" clearable style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="genericForm.reason"
clearable
placeholder="请输入审批意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleTransfer()">
{{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
</el-button>
<el-button @click="popOverVisible.transfer = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 【委托】按钮 --> <!-- 【委派】按钮 -->
<div @click="handleDelegate" v-if="isShowButton(OperationButtonType.DELEGATE)"> <el-popover
:visible="popOverVisible.delegate"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.DELEGATE)"
>
<template #reference>
<div @click="openPopover('delegate')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="ep:position" />&nbsp; <Icon :size="14" icon="ep:position" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.DELEGATE) }} {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
</div> </div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="formRef"
:model="genericForm"
:rules="genericRule"
label-width="100px"
>
<el-form-item label="接收人" prop="delegateUserId">
<el-select v-model="genericForm.delegateUserId" clearable style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="genericForm.reason"
clearable
placeholder="请输入审批意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleDelegate()">
{{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
</el-button>
<el-button @click="popOverVisible.delegate = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 【加签】 --> <!-- 【加签】按钮 当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
<div @click="handleSign" v-if="isShowButton(OperationButtonType.ADD_SIGN)"> <el-popover
:visible="popOverVisible.addSign"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.ADD_SIGN)"
>
<template #reference>
<div @click="openPopover('addSign')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="ep:plus" />&nbsp; <Icon :size="14" icon="ep:plus" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }} {{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</div> </div>
<!-- TODO @jason:减签 --> </template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="formRef"
:model="genericForm"
:rules="genericRule"
label-width="100px"
>
<el-form-item label="加签处理人" prop="addSignUserIds">
<el-select v-model="genericForm.addSignUserIds" multiple clearable style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="genericForm.reason"
clearable
placeholder="请输入审批意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handlerAddSign('before')">
向前{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</el-button>
<el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')">
向后{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</el-button>
<el-button @click="popOverVisible.addSign = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 【减签】按钮 -->
<el-popover
:visible="popOverVisible.deleteSign"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask?.children.length > 0"
>
<template #reference>
<div @click="openPopover('deleteSign')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="ep:semi-select" />&nbsp; 减签
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="formRef"
:model="genericForm"
:rules="genericRule"
label-width="100px"
>
<el-form-item label="减签人员" prop="deleteSignTaskId">
<el-select v-model="genericForm.deleteSignTaskId" clearable style="width: 100%">
<el-option
v-for="item in runningTask.children"
:key="item.id"
:label="getDeleteSignUserLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="genericForm.reason"
clearable
placeholder="请输入审批意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()">
减签
</el-button>
<el-button @click="popOverVisible.deleteSign = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 【退回】按钮 --> <!-- 【退回】按钮 -->
<div @click="handleBack" v-if="isShowButton(OperationButtonType.RETURN)"> <el-popover
<Icon :size="14" icon="fa:mail-reply" />&nbsp; :visible="popOverVisible.return"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)"
>
<template #reference>
<div @click="openReturnPopover" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="ep:back" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.RETURN) }} {{ getButtonDisplayName(OperationButtonType.RETURN) }}
</div> </div>
</template>
<!--TODO @jason:撤回 --> <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<!--TODO @jason:再次发起 --> <el-form
label-position="top"
class="mb-auto"
ref="formRef"
:model="genericForm"
:rules="genericRule"
label-width="100px"
>
<el-form-item label="退回节点" prop="targetTaskDefinitionKey">
<el-select v-model="genericForm.targetTaskDefinitionKey" clearable style="width: 100%">
<el-option
v-for="item in returnList"
:key="item.taskDefinitionKey"
:label="item.name"
:value="item.taskDefinitionKey"
/>
</el-select>
</el-form-item>
<el-form-item label="退回理由" prop="returnReason">
<el-input
v-model="genericForm.returnReason"
clearable
placeholder="请输入退回理由"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleReturn()">
{{ getButtonDisplayName(OperationButtonType.RETURN) }}
</el-button>
<el-button @click="popOverVisible.return = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div> </div>
</el-popover>
<!-- 弹窗:转派审批人 --> <!--【取消】按钮 这个对应发起人的取消, 只有发起人可以取消 -->
<TaskTransferForm ref="taskTransferFormRef" @success="getDetail" /> <el-popover
<!-- 弹窗:回退节点 --> :visible="popOverVisible.cancel"
<TaskReturnForm ref="taskReturnFormRef" @success="getDetail" /> placement="top-start"
<!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中--> :width="420"
<TaskDelegateForm ref="taskDelegateForm" @success="getDetail" /> trigger="click"
<!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> v-if="
<TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" /> userId === processInstance?.startUser?.id && !isEndProcessStatus(processInstance?.status)
"
>
<template #reference>
<div @click="openPopover('cancel')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="fa:mail-reply" />&nbsp; 取消
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="formRef"
:model="genericForm"
:rules="genericRule"
label-width="100px"
>
<el-form-item label="取消理由" prop="cancelReason">
<span class="text-#878c93 text-12px">&nbsp; 取消后,该审批流程将自动结束</span>
<el-input
v-model="genericForm.cancelReason"
clearable
placeholder="请输入取消理由"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleCancel()">
取消
</el-button>
<el-button @click="popOverVisible.cancel = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 【再次提交】 按钮-->
<div
@click="handleReCreate()"
class="hover-bg-gray-100 rounded-xl p-6px"
v-if="
userId === processInstance?.startUser?.id &&
isEndProcessStatus(processInstance?.status) &&
processDefinition?.formType === 10
"
>
<Icon :size="14" icon="ep:refresh" />&nbsp; 再次提交
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useUserStoreWithOut } from '@/store/modules/user'
import { setConfAndFields2 } from '@/utils/formCreate' import { setConfAndFields2 } from '@/utils/formCreate'
import { useUserStore } from '@/store/modules/user'
import * as TaskApi from '@/api/bpm/task' import * as TaskApi from '@/api/bpm/task'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import TaskReturnForm from './dialog/TaskReturnForm.vue'
import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
import TaskTransferForm from './dialog/TaskTransferForm.vue'
import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { isEmpty } from '@/utils/is'
import { import {
OperationButtonType, OperationButtonType,
OPERATION_BUTTON_NAME OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts' } from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'ProcessInstanceBtnConatiner' }) import { BpmProcessInstanceStatus } from '@/utils/constants'
defineOptions({ name: 'ProcessInstanceBtnContainer' })
const userId = useUserStore().getUser.id // 当前登录的编号 const router = useRouter() // 路由
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { proxy } = getCurrentInstance() as any const { proxy } = getCurrentInstance() as any
const userId = useUserStoreWithOut().getUser.id // 当前登录的编号
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
defineProps({ const props = defineProps({
processInstance: propTypes.any, // 流程实例信息 processInstance: propTypes.object, // 流程实例信息
processDefinition: propTypes.object, // 流程定义信息
userOptions: propTypes.any userOptions: propTypes.any
}) })
const formLoading = ref(false) // 表单加载中 const formLoading = ref(false) // 表单加载中
const passVisible = ref(false) // 是否显示 const popOverVisible = ref({
const rejectVisible = ref(false) // 是否显示 approve: false,
reject: false,
transfer: false,
delegate: false,
addSign: false,
return: false,
copy: false,
cancel: false,
deleteSign: false
}) // 气泡卡是否展示
const returnList = ref([] as any) // 退回节点
// ========== 审批信息 ========== // ========== 审批信息 ==========
const runningTask = ref<any>({}) // 运行中的任务 const runningTask = ref<any>() // 运行中的任务
const auditForm = ref<any>({}) // 审批任务的表单 const genericForm = ref<any>({}) // 通用表单
const approveForm = ref<any>({}) // 审批通过时,额外的补充信息 const approveForm = ref<any>({}) // 审批通过时,额外的补充信息
const approveFormFApi = ref<any>({}) // approveForms 的 fAPi const approveFormFApi = ref<any>({}) // approveForms 的 fAPi
const formRef = ref() const formRef = ref()
const auditRule = reactive({ const genericRule = reactive({
reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }] reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
}) returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }],
cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }],
copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }],
assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
addSignUserIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }],
targetTaskDefinitionKey: [{ required: true, message: '退回节点不能为空', trigger: 'change' }]
}) // 表单校验规则
/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */ /** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
watch( watch(
...@@ -221,52 +553,32 @@ watch( ...@@ -221,52 +553,32 @@ watch(
} }
) )
// TODO @jaosn:具体的审批任务,要不改成后端返回。让前端弱化下 /** 弹出退回气泡卡 */
/** const openReturnPopover = async () => {
* 设置 runningTasks 中的任务 returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id)
*/ if (returnList.value.length === 0) {
const loadRunningTask = (tasks: any[]) => { message.warning('当前没有可退回的节点')
runningTask.value = {}
auditForm.value = {}
approveForm.value = {}
approveFormFApi.value = {}
tasks.forEach((task: any) => {
if (!isEmpty(task.children)) {
loadRunningTask(task.children)
}
// 2.1 只有待处理才需要
if (task.status !== 1 && task.status !== 6) {
return
}
// 2.2 自己不是处理人
if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return return
} }
// 2.3 添加到处理任务 await openPopover('return')
runningTask.value = { ...task } }
auditForm.value = {
reason: '',
copyUserIds: []
}
// 2.4 处理 approve 表单 /** 弹出气泡卡 */
if (task.formId && task.formConf) { const openPopover = async (type: string) => {
const tempApproveForm = {} Object.keys(popOverVisible.value).forEach((item) => {
setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables) popOverVisible.value[item] = item === type
approveForm.value = tempApproveForm
} else {
approveForm.value = {} // 占位,避免为空
}
}) })
await nextTick()
formRef.value.resetFields()
} }
/** 处理审批通过和不通过的操作 */ /** 处理审批通过和不通过的操作 */
const handleAudit = async (pass: any) => { const handleAudit = async (pass: boolean) => {
formLoading.value = true formLoading.value = true
try { try {
const auditFormRef = proxy.$refs['formRef'] const genericFormRef = proxy.$refs['formRef']
// 1.2 校验表单 // 1.2 校验表单
const elForm = unref(auditFormRef) const elForm = unref(genericFormRef)
if (!elForm) return if (!elForm) return
const valid = await elForm.validate() const valid = await elForm.validate()
if (!valid) return if (!valid) return
...@@ -274,8 +586,7 @@ const handleAudit = async (pass: any) => { ...@@ -274,8 +586,7 @@ const handleAudit = async (pass: any) => {
// 2.1 提交审批 // 2.1 提交审批
const data = { const data = {
id: runningTask.value.id, id: runningTask.value.id,
reason: auditForm.value.reason, reason: genericForm.value.reason
copyUserIds: auditForm.value.copyUserIds
} }
if (pass) { if (pass) {
// 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交 // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
...@@ -286,61 +597,246 @@ const handleAudit = async (pass: any) => { ...@@ -286,61 +597,246 @@ const handleAudit = async (pass: any) => {
data.variables = approveForm.value.value data.variables = approveForm.value.value
} }
await TaskApi.approveTask(data) await TaskApi.approveTask(data)
popOverVisible.value.approve = false
message.success('审批通过成功') message.success('审批通过成功')
} else { } else {
await TaskApi.rejectTask(data) await TaskApi.rejectTask(data)
popOverVisible.value.reject = false
message.success('审批不通过成功') message.success('审批不通过成功')
} }
// 2.2 加载最新数据 // 2.2 加载最新数据
getDetail() reload()
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
} }
/* 抄送 TODO */ /** 处理抄送 */
const handleSend = () => {} const handleCopy = async () => {
formLoading.value = true
// TODO 代码优化:这里 flag 改成 approve: boolean 。因为 flag 目前就只有 1 和 2 try {
const openPopover = (flag) => { const copyFormRef = proxy.$refs['formRef']
passVisible.value = false // 1. 校验表单
rejectVisible.value = false const elForm = unref(copyFormRef)
formRef.value.resetFields() if (!elForm) return
flag === '1' ? (passVisible.value = true) : (rejectVisible.value = true) const valid = await elForm.validate()
if (!valid) return
// 2. 提交抄送
const data = {
id: runningTask.value.id,
reason: genericForm.value.copyReason,
copyUserIds: genericForm.value.copyUserIds
}
await TaskApi.copyTask(data)
popOverVisible.value.copy = false
message.success('操作成功')
} finally {
formLoading.value = false
}
} }
/** 转派审批人 */ /** 处理转交 */
const taskTransferFormRef = ref() const handleTransfer = async () => {
const openTaskUpdateAssigneeForm = () => { formLoading.value = true
taskTransferFormRef.value.open(runningTask.value.id) try {
const transferFormRef = proxy.$refs['formRef']
// 1.1 校验表单
const elForm = unref(transferFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 1.2 提交转交
const data = {
id: runningTask.value.id,
reason: genericForm.value.reason,
assigneeUserId: genericForm.value.assigneeUserId
}
await TaskApi.transferTask(data)
popOverVisible.value.transfer = false
message.success('操作成功')
// 2. 加载最新数据
reload()
} finally {
formLoading.value = false
}
} }
/** 处理审批退回的操作 */ /** 处理委派 */
const taskDelegateForm = ref()
const handleDelegate = async () => { const handleDelegate = async () => {
taskDelegateForm.value.open(runningTask.value.id) formLoading.value = true
try {
const deletegateFormRef = proxy.$refs['formRef']
// 1.1 校验表单
const elForm = unref(deletegateFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 1.2 处理委派
const data = {
id: runningTask.value.id,
reason: genericForm.value.reason,
delegateUserId: genericForm.value.delegateUserId
}
await TaskApi.delegateTask(data)
popOverVisible.value.delegate = false
message.success('操作成功')
// 2. 加载最新数据
reload()
} finally {
formLoading.value = false
}
} }
/** 处理审批退回的操作 */ /** 处理加签 */
const taskReturnFormRef = ref() const handlerAddSign = async (type: string) => {
const handleBack = async () => { formLoading.value = true
taskReturnFormRef.value.open(runningTask.value.id) try {
const transferFormRef = proxy.$refs['formRef']
// 1.1 校验表单
const elForm = unref(transferFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 1.2 提交加签
const data = {
id: runningTask.value.id,
type,
reason: genericForm.value.reason,
userIds: genericForm.value.addSignUserIds
}
await TaskApi.signCreateTask(data)
message.success('操作成功')
popOverVisible.value.addSign = false
// 2 加载最新数据
reload()
} finally {
formLoading.value = false
}
} }
/** 处理审批加签的操作 */ /** 处理退回 */
const taskSignCreateFormRef = ref() const handleReturn = async () => {
const handleSign = async () => { formLoading.value = true
taskSignCreateFormRef.value.open(runningTask.value.id) try {
const returnFormRef = proxy.$refs['formRef']
// 1.1 校验表单
const elForm = unref(returnFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 1.2 提交退回
const data = {
id: runningTask.value.id,
reason: genericForm.value.returnReason,
targetTaskDefinitionKey: genericForm.value.targetTaskDefinitionKey
}
await TaskApi.returnTask(data)
popOverVisible.value.return = false
message.success('操作成功')
// 2 重新加载数据
reload()
} finally {
formLoading.value = false
}
} }
/** 获得详情 */
const getDetail = () => { /** 处理取消 */
const handleCancel = async () => {
formLoading.value = true
try {
const cancelFormRef = proxy.$refs['formRef']
// 1.1 校验表单
const elForm = unref(cancelFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 1.2 提交取消
await ProcessInstanceApi.cancelProcessInstanceByStartUser(
props.processInstance.id,
genericForm.value.cancelReason
)
popOverVisible.value.return = false
message.success('操作成功')
// 2 重新加载数据
reload()
} finally {
formLoading.value = false
}
}
/** 处理再次提交 */
const handleReCreate = async () => {
// 跳转发起流程界面
await router.push({
name: 'BpmProcessInstanceCreate',
query: { processInstanceId: props.processInstance?.id }
})
}
/** 获取减签人员标签 */
const getDeleteSignUserLabel = (task: any): string => {
const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName
const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname
return `${nickname} ( 所属部门:${deptName} )`
}
/** 处理减签 */
const handlerDeleteSign = async () => {
formLoading.value = true
try {
const deleteFormRef = proxy.$refs['formRef']
// 1.1 校验表单
const elForm = unref(deleteFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 1.2 提交减签
const data = {
id: genericForm.value.deleteSignTaskId,
reason: genericForm.value.reason
}
await TaskApi.signDeleteTask(data)
message.success('减签成功')
popOverVisible.value.deleteSign = false
// 2 加载最新数据
reload()
} finally {
formLoading.value = false
}
}
/** 重新加载数据 */
const reload = () => {
emit('success') emit('success')
} }
/** 任务是否为处理中状态 */
const isHandleTaskStatus = () => {
let canHandle = false
if (TaskApi.TaskStatusEnum.RUNNING === runningTask.value?.status) {
canHandle = true
}
return canHandle
}
/** 流程状态是否为结束状态 */
const isEndProcessStatus = (status: number) => {
let isEndStatus = false
if (
BpmProcessInstanceStatus.APPROVE === status ||
BpmProcessInstanceStatus.REJECT === status ||
BpmProcessInstanceStatus.CANCEL === status
) {
isEndStatus = true
}
return isEndStatus
}
/** 是否显示按钮 */ /** 是否显示按钮 */
const isShowButton = (btnType: OperationButtonType): boolean => { const isShowButton = (btnType: OperationButtonType): boolean => {
let isShow = true let isShow = true
if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) { if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
isShow = runningTask.value.buttonsSetting[btnType].enable isShow = runningTask.value.buttonsSetting[btnType].enable
} }
return isShow return isShow
...@@ -349,13 +845,28 @@ const isShowButton = (btnType: OperationButtonType): boolean => { ...@@ -349,13 +845,28 @@ const isShowButton = (btnType: OperationButtonType): boolean => {
/** 获取按钮的显示名称 */ /** 获取按钮的显示名称 */
const getButtonDisplayName = (btnType: OperationButtonType) => { const getButtonDisplayName = (btnType: OperationButtonType) => {
let displayName = OPERATION_BUTTON_NAME.get(btnType) let displayName = OPERATION_BUTTON_NAME.get(btnType)
if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) { if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
displayName = runningTask.value.buttonsSetting[btnType].displayName displayName = runningTask.value.buttonsSetting[btnType].displayName
} }
return displayName return displayName
} }
defineExpose({ loadRunningTask }) const loadTodoTask = (task: any) => {
genericForm.value = {}
approveForm.value = {}
approveFormFApi.value = {}
runningTask.value = task
// 处理 approve 表单.
if (task && task.formId && task.formConf) {
const tempApproveForm = {}
setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables)
approveForm.value = tempApproveForm
} else {
approveForm.value = {} // 占位,避免为空
}
}
defineExpose({ loadTodoTask })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -366,7 +877,7 @@ defineExpose({ loadRunningTask }) ...@@ -366,7 +877,7 @@ defineExpose({ loadRunningTask })
.btn-container { .btn-container {
> div { > div {
display: flex; display: flex;
margin: 0 15px; margin: 0 8px;
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
......
<template>
<div v-loading="loading" class="process-viewer-container">
<SimpleProcessViewer
:flow-node="simpleModel"
:tasks="tasks"
:process-instance="processInstance"
class="process-viewer"
/>
</div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { TaskStatusEnum } from '@/api/bpm/task'
import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({ name: 'BpmProcessInstanceSimpleViewer' })
const props = defineProps({
loading: propTypes.bool.def(false), // 是否加载中
modelView: propTypes.object,
simpleJson: propTypes.string // Simple 模型结构数据 (json 格式)
})
const simpleModel = ref()
// 用户任务
const tasks = ref([])
// 流程实例
const processInstance = ref()
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks
processInstance.value = newModelView.processInstance
// 已经拒绝的活动节点编号集合,只包括 UserTask
const rejectedTaskActivityIds: string[] = newModelView.rejectedTaskActivityIds
// 进行中的活动节点编号集合, 只包括 UserTask
const unfinishedTaskActivityIds: string[] = newModelView.unfinishedTaskActivityIds
// 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
const finishedActivityIds: string[] = newModelView.finishedTaskActivityIds
// 已经完成的连线节点编号集合,只包括 SequenceFlow
const finishedSequenceFlowActivityIds: string[] = newModelView.finishedSequenceFlowActivityIds
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds
)
simpleModel.value = newModelView.simpleModel
}
}
)
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value)
}
}
)
const setSimpleModelNodeTaskStatus = (
simpleModel: SimpleFlowNode | undefined,
processStatus: number,
rejectedTaskActivityIds: string[],
unfinishedTaskActivityIds: string[],
finishedActivityIds: string[],
finishedSequenceFlowActivityIds: string[]
) => {
if (!simpleModel) {
return
}
// 结束节点
if (simpleModel.type === NodeType.END_EVENT_NODE) {
if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = processStatus
} else {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
return
}
// 审批节点
if (
simpleModel.type === NodeType.START_USER_NODE ||
simpleModel.type === NodeType.USER_TASK_NODE
) {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
if (rejectedTaskActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.REJECT
} else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.RUNNING
} else if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
}
// TODO 是不是还缺一个 cancel 的状态
}
// 抄送节点
if (simpleModel.type === NodeType.COPY_TASK_NODE) {
// 抄送节点 只有通过和未执行状态
if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
} else {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
}
// 条件节点 对应 SequenceFlow
if (simpleModel.type === NodeType.CONDITION_NODE) {
// 条件节点。只有通过和未执行状态
if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
} else {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
}
// 网关节点
if (
simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE
) {
// 网关节点。只有通过和未执行状态
if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
} else {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
simpleModel.conditionNodes?.forEach((node) => {
setSimpleModelNodeTaskStatus(
node,
processStatus,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds
)
})
}
setSimpleModelNodeTaskStatus(
simpleModel.childNode,
processStatus,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds
)
}
</script>
<style lang="scss" scoped>
.process-viewer-container {
height: 100%;
width: 100%;
:deep(.process-viewer) {
height: 100% !important;
min-height: 100%;
width: 100%;
overflow: auto;
}
}
</style>
<template> <template>
<el-card v-loading="loading" class="box-card"> <el-table :data="tasks" border header-cell-class-name="table-header-gray">
<template #header> <el-table-column label="审批节点" prop="name" min-width="120" align="center" />
<span class="el-icon-picture-outline">审批记录</span> <el-table-column label="审批人" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template> </template>
<el-col :offset="3" :span="17"> </el-table-column>
<div class="block"> <el-table-column
<el-timeline> :formatter="dateFormatter"
<el-timeline-item align="center"
v-if="processInstance.endTime" label="开始时间"
:type="getProcessInstanceTimelineItemType(processInstance)" prop="createTime"
> min-width="140"
<p style="font-weight: 700">
结束流程:在 {{ formatDate(processInstance?.endTime) }} 结束
<dict-tag
:type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
:value="processInstance.status"
/> />
</p> <el-table-column
</el-timeline-item> :formatter="dateFormatter"
<el-timeline-item align="center"
v-for="(item, index) in tasks" label="结束时间"
:key="index" prop="endTime"
:type="getTaskTimelineItemType(item)" min-width="140"
> />
<p style="font-weight: 700"> <el-table-column align="center" label="审批状态" prop="status" min-width="90">
审批任务:{{ item.name }} <template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" /> <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
<el-button </template>
class="ml-10px" </el-table-column>
v-if="!isEmpty(item.children)" <el-table-column align="center" label="审批建议" prop="reason" min-width="200">
@click="openChildrenTask(item)" <template #default="scope">
size="small" {{ scope.row.reason }}
>
<Icon icon="ep:memo" /> 子任务
</el-button>
<el-button <el-button
class="ml-10px" class="ml-10px"
size="small" size="small"
v-if="item.formId > 0" v-if="scope.row.formId > 0"
@click="handleFormDetail(item)" @click="handleFormDetail(scope.row)"
> >
<Icon icon="ep:document" /> 查看表单 <Icon icon="ep:document" /> 查看表单
</el-button> </el-button>
</p> </template>
<el-card :body-style="{ padding: '10px' }"> </el-table-column>
<label v-if="item.assigneeUser" style="margin-right: 30px; font-weight: normal"> <el-table-column align="center" label="耗时" prop="durationInMillis" min-width="100">
审批人:{{ item.assigneeUser.nickname }} <template #default="scope">
<el-tag size="small" type="info">{{ item.assigneeUser.deptName }}</el-tag> {{ formatPast2(scope.row.durationInMillis) }}
</label> </template>
<label v-if="item.createTime" style="font-weight: normal">创建时间:</label> </el-table-column>
<label style="font-weight: normal; color: #8a909c"> </el-table>
{{ formatDate(item?.createTime) }}
</label>
<label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
审批时间:
</label>
<label v-if="item.endTime" style="font-weight: normal; color: #8a909c">
{{ formatDate(item?.endTime) }}
</label>
<label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
耗时:
</label>
<label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c">
{{ formatPast2(item?.durationInMillis) }}
</label>
<p v-if="item.reason"> 审批建议:{{ item.reason }} </p>
</el-card>
</el-timeline-item>
<el-timeline-item type="success">
<p style="font-weight: 700">
发起流程:【{{ processInstance.startUser?.nickname }}】在
{{ formatDate(processInstance?.startTime) }} 发起【 {{ processInstance.name }} 】流程
</p>
</el-timeline-item>
</el-timeline>
</div>
</el-col>
</el-card>
<!-- 弹窗:子任务 -->
<TaskSignList ref="taskSignListRef" @success="refresh" />
<!-- 弹窗:表单 --> <!-- 弹窗:表单 -->
<Dialog title="表单详情" v-model="taskFormVisible" width="600"> <Dialog title="表单详情" v-model="taskFormVisible" width="600">
<form-create <form-create
...@@ -91,61 +56,20 @@ ...@@ -91,61 +56,20 @@
</Dialog> </Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { formatDate, formatPast2 } from '@/utils/formatTime' import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { DICT_TYPE } from '@/utils/dict' import { DICT_TYPE } from '@/utils/dict'
import { isEmpty } from '@/utils/is'
import TaskSignList from './dialog/TaskSignList.vue'
import type { ApiAttrs } from '@form-create/element-ui/types/config' import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { setConfAndFields2 } from '@/utils/formCreate' import { setConfAndFields2 } from '@/utils/formCreate'
import * as TaskApi from '@/api/bpm/task'
defineOptions({ name: 'BpmProcessInstanceTaskList' }) defineOptions({ name: 'BpmProcessInstanceTaskList' })
defineProps({ const props = defineProps({
loading: propTypes.bool, // 是否加载中 loading: propTypes.bool.def(false), // 是否加载中
processInstance: propTypes.object, // 流程实例 id: propTypes.string // 流程实例的编号
tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组
}) })
const tasks = ref([]) // 流程任务的数组
/** 获得流程实例对应的颜色 */
const getProcessInstanceTimelineItemType = (item: any) => {
if (item.status === 2) {
return 'success'
}
if (item.status === 3) {
return 'danger'
}
if (item.status === 4) {
return 'warning'
}
return ''
}
/** 获得任务对应的颜色 */
const getTaskTimelineItemType = (item: any) => {
if ([0, 1, 6, 7].includes(item.status)) {
return 'primary'
}
if (item.status === 2) {
return 'success'
}
if (item.status === 3) {
return 'danger'
}
if (item.status === 4) {
return 'info'
}
if (item.status === 5) {
return 'warning'
}
return ''
}
/** 子任务 */
const taskSignListRef = ref()
const openChildrenTask = (item: any) => {
taskSignListRef.value.open(item)
}
/** 查看表单 */ /** 查看表单 */
const fApi = ref<ApiAttrs>() // form-create 的 API 操作类 const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
...@@ -155,7 +79,7 @@ const taskForm = ref({ ...@@ -155,7 +79,7 @@ const taskForm = ref({
value: {} value: {}
}) // 流程任务的表单详情 }) // 流程任务的表单详情
const taskFormVisible = ref(false) const taskFormVisible = ref(false)
const handleFormDetail = async (row) => { const handleFormDetail = async (row: any) => {
// 设置表单 // 设置表单
setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables) setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables)
// 弹窗打开 // 弹窗打开
...@@ -167,9 +91,13 @@ const handleFormDetail = async (row) => { ...@@ -167,9 +91,13 @@ const handleFormDetail = async (row) => {
fApi.value?.fapi?.disabled(true) fApi.value?.fapi?.disabled(true)
} }
/** 刷新数据 */ /** 只有 loading 完成时,才去加载流程列表 */
const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调 watch(
const refresh = () => { () => props.loading,
emit('refresh') async (value) => {
} if (value) {
tasks.value = await TaskApi.getTaskListByProcessInstanceId(props.id)
}
}
)
</script> </script>
...@@ -3,155 +3,189 @@ ...@@ -3,155 +3,189 @@
<el-timeline class="pt-20px"> <el-timeline class="pt-20px">
<!-- 遍历每个审批节点 --> <!-- 遍历每个审批节点 -->
<el-timeline-item <el-timeline-item
v-for="(activity, index) in approveNodes" v-for="(activity, index) in activityNodes"
:key="index" :key="index"
size="large" size="large"
:icon="getApprovalNodeIcon(activity.status, activity.nodeType)" :icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
:color="getApprovalNodeColor(activity.status)" :color="getApprovalNodeColor(activity.status)"
> >
<div class="flex flex-col items-start"> <template #dot>
<div
class="position-absolute left--10px top--6px rounded-full border border-solid border-#dedede w-30px h-30px flex justify-center items-center bg-#3f73f7 p-5px"
>
<img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
<div
v-if="showStatusIcon"
class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
>
<el-icon :size="11" color="#fff">
<component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
</el-icon>
</div>
</div>
</template>
<div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`">
<!-- 第一行:节点名称、时间 -->
<div class="flex w-full">
<div class="font-bold"> {{ activity.name }}</div> <div class="font-bold"> {{ activity.name }}</div>
<div class="flex items-center mt-1"> <!-- 信息:时间 -->
<div
v-if="activity.status !== TaskStatusEnum.NOT_START"
class="text-#a5a5a5 text-13px mt-1 ml-auto"
>
{{ getApprovalNodeTime(activity) }}
</div>
</div>
<!-- 需要自定义选择审批人 -->
<div
class="flex flex-wrap gap2 items-center"
v-if="
isEmpty(activity.tasks) &&
isEmpty(activity.candidateUsers) &&
CandidateStrategy.START_USER_SELECT === activity.candidateStrategy
"
>
<!-- && activity.nodeType === NodeType.USER_TASK_NODE -->
<el-tooltip content="添加用户" placement="left">
<el-button
class="!px-6px"
@click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
>
<img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
</el-button>
</el-tooltip>
<div
v-for="(user, idx1) in customApproveUsers[activity.id]"
:key="idx1"
class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
>
<el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
<el-avatar class="!m-5px" :size="28" v-else>
{{ user.nickname.substring(0, 1) }}
</el-avatar>
{{ user.nickname }}
</div>
</div>
<div v-else class="flex items-center flex-wrap mt-1 gap2">
<!-- 情况一:遍历每个审批节点下的【进行中】task 任务 --> <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
<div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center"> <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
<div class="flex items-center flex-col pr-2"> <div
<div class="position-relative" v-if="task.assigneeUser || task.ownerUser"> class="position-relative flex flex-wrap gap2"
<!-- 信息:头像 --> v-if="task.assigneeUser || task.ownerUser"
>
<!-- 信息:头像昵称 -->
<div
class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
>
<template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
<el-avatar <el-avatar
:size="36" class="!m-5px"
v-if="task.assigneeUser && task.assigneeUser.avatar" :size="28"
:src="task.assigneeUser.avatar" v-if="task.assigneeUser?.avatar"
:src="task.assigneeUser?.avatar"
/> />
<el-avatar v-else-if="task.assigneeUser && task.assigneeUser.nickname"> <el-avatar class="!m-5px" :size="28" v-else>
{{ task.assigneeUser.nickname.substring(0, 1) }} {{ task.assigneeUser?.nickname.substring(0, 1) }}
</el-avatar> </el-avatar>
{{ task.assigneeUser?.nickname }}
</template>
<template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
<el-avatar <el-avatar
v-else-if="task.ownerUser && task.ownerUser.avatar" class="!m-5px"
:src="task.ownerUser.avatar" :size="28"
v-if="task.ownerUser?.avatar"
:src="task.ownerUser?.avatar"
/> />
<el-avatar v-else-if="task.ownerUser && task.ownerUser.nickname"> <el-avatar class="!m-5px" :size="28" v-else>
{{ task.ownerUser.nickname.substring(0, 1) }} {{ task.ownerUser?.nickname.substring(0, 1) }}
</el-avatar> </el-avatar>
{{ task.ownerUser?.nickname }}
</template>
<!-- 信息:任务 ICON --> <!-- 信息:任务 ICON -->
<div <div
class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px" v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: statusIconMap2[task.status]?.color }"
> >
<Icon <Icon :size="11" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
:size="12"
:icon="statusIconMap2[task.status]?.icon"
:color="statusIconMap2[task.status]?.color"
/>
</div> </div>
</div> </div>
<div class="flex flex-col mt-1">
<!-- 信息:昵称 -->
<div
v-if="task.assigneeUser && task.assigneeUser.nickname"
class="text-10px text-align-center"
>
{{ task.assigneeUser.nickname }}
</div> </div>
<teleport defer :to="`#activity-task-${activity.id}`">
<div <div
v-else-if="task.ownerUser && task.ownerUser.nickname" v-if="
class="text-10px text-align-center" task.reason &&
[NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(activity.nodeType)
"
class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
> >
{{ task.ownerUser.nickname }} 审批意见:{{ task.reason }}
</div>
<!-- TODO @jason:审批意见,要展示哈。 -->
<!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
</div>
</div> </div>
</teleport>
</div> </div>
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 --> <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
<div <div
v-for="(user, idx1) in activity.candidateUserList" v-for="(user, idx1) in activity.candidateUsers"
:key="idx1" :key="idx1"
class="flex items-center" class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
> >
<div class="flex items-center flex-col pr-2"> <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
<div class="position-relative"> <el-avatar class="!m-5px" :size="28" v-else>
<!-- 信息:头像 -->
<el-avatar :size="36" v-if="user.avatar" :src="user.avatar" />
<el-avatar v-else-if="user.nickname && user.nickname">
{{ user.nickname.substring(0, 1) }} {{ user.nickname.substring(0, 1) }}
</el-avatar> </el-avatar>
<!-- 信息:任务 ICON -->
<div
class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
>
<Icon
:size="12"
:icon="statusIconMap2['-1']?.icon"
:color="statusIconMap2['-1']?.color"
/>
</div>
</div>
<div class="flex flex-col mt-1">
<!-- 信息:昵称 -->
<div v-if="user.nickname" class="text-10px text-align-center">
{{ user.nickname }} {{ user.nickname }}
</div>
<!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
</div>
</div>
</div>
</div>
<!-- 信息:时间 -->
<div
v-if="activity.status !== TaskStatusEnum.NOT_START"
class="text-#a5a5a5 text-13px mt-1"
>
{{ getApprovalNodeTime(activity) }}
</div>
<!-- TODO @jason:审批意见,要展示哈。 --> <!-- 信息:任务 ICON -->
<!-- <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
<div v-if="activity.opinion" class="text-#a5a5a5 text-12px w-100%">
<div class="mb-5px">审批意见:</div>
<div <div
class="w-100% border-1px border-#a5a5a5 border-dashed rounded py-5px px-15px text-#2d2d2d" v-if="showStatusIcon"
class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: statusIconMap2['-1']?.color }"
> >
{{ activity.opinion }} <Icon :size="11" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
</div>
</div> </div>
</div> </div>
<div v-if="activity.createTime" class="text-#a5a5a5 text-13px">
{{ formatDate(activity.createTime) }}
</div> -->
</div> </div>
</el-timeline-item> </el-timeline-item>
</el-timeline> </el-timeline>
<!-- 用户选择弹窗 -->
<UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import * as ProcessInstanceApi from '@/api/bpm/processInstance' import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { TaskStatusEnum } from '@/api/bpm/task' import { TaskStatusEnum } from '@/api/bpm/task'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts' import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
import { isEmpty } from '@/utils/is'
import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue' import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
import starterSvg from '@/assets/svgs/bpm/starter.svg'
import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
import copySvg from '@/assets/svgs/bpm/copy.svg'
import conditionSvg from '@/assets/svgs/bpm/condition.svg'
import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
import finishSvg from '@/assets/svgs/bpm/finish.svg'
defineOptions({ name: 'BpmProcessInstanceTimeline' }) defineOptions({ name: 'BpmProcessInstanceTimeline' })
const props = defineProps({ withDefaults(
// 流程实例编号 defineProps<{
processInstanceId: { activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
type: String, showStatusIcon?: boolean // 是否显示头像右下角状态图标
required: false, }>(),
default: '' {
}, showStatusIcon: true // 默认值为 true
// 流程定义编号
processDefinitionId: {
type: String,
required: false,
default: ''
} }
}) )
// 审批节点 // 审批节点
const approveNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
const statusIconMap2 = { const statusIconMap2 = {
// 未开始 // 未开始
'-1': { color: '#e5e7ec', icon: 'ep-clock' }, '-1': { color: '#909398', icon: 'ep-clock' },
// 待审批 // 待审批
'0': { color: '#e5e7ec', icon: 'ep:loading' }, '0': { color: '#00b32a', icon: 'ep:loading' },
// 审批中 // 审批中
'1': { color: '#448ef7', icon: 'ep:loading' }, '1': { color: '#448ef7', icon: 'ep:loading' },
// 审批通过 // 审批通过
...@@ -160,7 +194,7 @@ const statusIconMap2 = { ...@@ -160,7 +194,7 @@ const statusIconMap2 = {
'3': { color: '#f46b6c', icon: 'fa-solid:times-circle' }, '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
// 取消 // 取消
'4': { color: '#cccccc', icon: 'ep:delete-filled' }, '4': { color: '#cccccc', icon: 'ep:delete-filled' },
// 回退 // 退回
'5': { color: '#f46b6c', icon: 'ep:remove-filled' }, '5': { color: '#f46b6c', icon: 'ep:remove-filled' },
// 委派中 // 委派中
'6': { color: '#448ef7', icon: 'ep:loading' }, '6': { color: '#448ef7', icon: 'ep:loading' },
...@@ -170,8 +204,8 @@ const statusIconMap2 = { ...@@ -170,8 +204,8 @@ const statusIconMap2 = {
const statusIconMap = { const statusIconMap = {
// 审批未开始 // 审批未开始
'-1': { color: '#e5e7ec', icon: Clock }, '-1': { color: '#909398', icon: Clock },
'0': { color: '#e5e7ec', icon: Clock }, '0': { color: '#00b32a', icon: Clock },
// 审批中 // 审批中
'1': { color: '#448ef7', icon: Loading }, '1': { color: '#448ef7', icon: Loading },
// 审批通过 // 审批通过
...@@ -180,7 +214,7 @@ const statusIconMap = { ...@@ -180,7 +214,7 @@ const statusIconMap = {
'3': { color: '#f46b6c', icon: Close }, '3': { color: '#f46b6c', icon: Close },
// 已取消 // 已取消
'4': { color: '#cccccc', icon: Delete }, '4': { color: '#cccccc', icon: Delete },
// 回退 // 退回
'5': { color: '#f46b6c', icon: Minus }, '5': { color: '#f46b6c', icon: Minus },
// 委派中 // 委派中
'6': { color: '#448ef7', icon: Loading }, '6': { color: '#448ef7', icon: Loading },
...@@ -188,13 +222,27 @@ const statusIconMap = { ...@@ -188,13 +222,27 @@ const statusIconMap = {
'7': { color: '#00b32a', icon: Check } '7': { color: '#00b32a', icon: Check }
} }
/** 获得审批详情 */ const nodeTypeSvgMap = {
const getApprovalDetail = async () => { // 结束节点
const data = await ProcessInstanceApi.getApprovalDetail( [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
props.processInstanceId, // 发起人节点
props.processDefinitionId [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
) // 审批人节点
approveNodes.value = data.approveNodes [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
// 抄送人节点
[NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg },
// 条件分支节点
[NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
// 并行分支节点
[NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg }
}
// 只有只有状态是 -1、0、1 才展示头像右小角状态小icon
const onlyStatusIconShow = [-1, 0, 1]
// timeline时间线上icon图标
const getApprovalNodeImg = (nodeType: NodeType) => {
return nodeTypeSvgMap[nodeType]?.svg
} }
const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => { const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
...@@ -202,7 +250,11 @@ const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => { ...@@ -202,7 +250,11 @@ const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
return statusIconMap[taskStatus]?.icon return statusIconMap[taskStatus]?.icon
} }
if (nodeType === NodeType.START_USER_NODE || nodeType === NodeType.USER_TASK_NODE) { if (
nodeType === NodeType.START_USER_NODE ||
nodeType === NodeType.USER_TASK_NODE ||
nodeType === NodeType.END_EVENT_NODE
) {
return statusIconMap[taskStatus]?.icon return statusIconMap[taskStatus]?.icon
} }
} }
...@@ -212,22 +264,29 @@ const getApprovalNodeColor = (taskStatus: number) => { ...@@ -212,22 +264,29 @@ const getApprovalNodeColor = (taskStatus: number) => {
} }
const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => { const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
if (node.nodeType === NodeType.START_USER_NODE && node.startTime) {
return `${formatDate(node.startTime)}`
}
if (node.endTime) { if (node.endTime) {
return `结束时间:${formatDate(node.endTime)}` return `${formatDate(node.endTime)}`
} }
if (node.startTime) { if (node.startTime) {
return `创建时间:${formatDate(node.startTime)}` return `${formatDate(node.startTime)}`
} }
} }
/** 重新刷新审批详情 */ // 选择自定义审批人
const refresh = () => { const userSelectFormRef = ref()
getApprovalDetail() const handleSelectUser = (activityId, selectedList) => {
userSelectFormRef.value.open(activityId, selectedList)
}
const emit = defineEmits<{
selectUserConfirm: [id: any, userList: any[]]
}>()
const customApproveUsers: any = ref({}) // key:activityId,value:用户列表
// 选择完成
const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
customApproveUsers.value[activityId] = userList || []
emit('selectUserConfirm', activityId, userList)
} }
defineExpose({ refresh })
onMounted(async () => {
await getApprovalDetail()
})
</script> </script>
<template>
<Dialog v-model="dialogVisible" title="委派任务" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="接收人" prop="delegateUserId">
<el-select v-model="formData.delegateUserId" clearable style="width: 100%">
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="委派理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入委派理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'BpmTaskDelegateForm' })
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
delegateUserId: undefined,
reason: ''
})
const formRules = ref({
delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const userList = ref<any[]>([]) // 用户列表
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
// 获得用户列表
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.delegateTask(formData.value)
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
delegateUserId: undefined,
reason: ''
}
formRef.value?.resetFields()
}
</script>
<template>
<Dialog v-model="dialogVisible" title="回退任务" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="退回节点" prop="targetTaskDefinitionKey">
<el-select v-model="formData.targetTaskDefinitionKey" clearable style="width: 100%">
<el-option
v-for="item in returnList"
:key="item.taskDefinitionKey"
:label="item.name"
:value="item.taskDefinitionKey"
/>
</el-select>
</el-form-item>
<el-form-item label="回退理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入回退理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="TaskRollbackDialogForm" setup>
import * as TaskApi from '@/api/bpm/task'
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
targetTaskDefinitionKey: undefined,
reason: ''
})
const formRules = ref({
targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const returnList = ref([] as any)
/** 打开弹窗 */
const open = async (id: string) => {
returnList.value = await TaskApi.getTaskListByReturn(id)
if (returnList.value.length === 0) {
message.warning('当前没有可回退的节点')
return false
}
dialogVisible.value = true
resetForm()
formData.value.id = id
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.returnTask(formData.value)
message.success('回退成功')
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
targetTaskDefinitionKey: undefined,
reason: ''
}
formRef.value?.resetFields()
}
</script>
<template>
<Dialog v-model="dialogVisible" title="加签" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="加签处理人" prop="userIds">
<el-select v-model="formData.userIds" multiple clearable style="width: 100%">
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="加签理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入加签理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm('before')">
向前加签
</el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm('after')">
向后加签
</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'TaskSignCreateForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
userIds: [],
type: '',
reason: ''
})
const formRules = ref({
userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }]
})
const formRef = ref() // 表单 Ref
const userList = ref<any[]>([]) // 用户列表
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
// 获得用户列表
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async (type: string) => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
formData.value.type = type
try {
await TaskApi.signCreateTask(formData.value)
message.success('加签成功')
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
userIds: [],
type: '',
reason: ''
}
formRef.value?.resetFields()
}
</script>
<template>
<Dialog v-model="dialogVisible" title="减签" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="减签任务" prop="id">
<el-radio-group v-model="formData.id">
<el-radio-button v-for="item in childrenTaskList" :key="item.id" :value="item.id">
{{ item.name }}
({{ item.assigneeUser?.deptName || item.ownerUser?.deptName }} -
{{ item.assigneeUser?.nickname || item.ownerUser?.nickname }})
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="减签理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入减签理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'TaskSignDeleteForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
reason: ''
})
const formRules = ref({
id: [{ required: true, message: '必须选择减签任务', trigger: 'change' }],
reason: [{ required: true, message: '减签理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const childrenTaskList = ref([])
/** 打开弹窗 */
const open = async (id: string) => {
childrenTaskList.value = await TaskApi.getChildrenTaskList(id)
if (isEmpty(childrenTaskList.value)) {
message.warning('当前没有可减签的任务')
return false
}
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.signDeleteTask(formData.value)
message.success('减签成功')
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
reason: ''
}
formRef.value?.resetFields()
}
</script>
<template>
<el-drawer v-model="drawerVisible" title="子任务" size="880px">
<!-- 当前任务 -->
<template #header>
<h4>{{ parentTask.name }} 】审批人:{{ parentTask?.assigneeUser?.nickname }}</h4>
<el-button
style="margin-left: 5px"
v-if="isSignDeleteButtonVisible(parentTask)"
type="danger"
plain
@click="handleSignDelete(parentTask)"
>
<Icon icon="ep:remove" /> 减签
</el-button>
</template>
<!-- 子任务列表 -->
<el-table :data="parentTask.children" style="width: 100%" row-key="id" border>
<el-table-column prop="assigneeUser.nickname" label="审批人" min-width="100">
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column prop="assigneeUser.deptName" label="所在部门" min-width="100">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column label="审批状态" prop="status" width="120">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="提交时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="结束时间"
align="center"
prop="endTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" prop="operation" width="90">
<template #default="scope">
<el-button
v-if="isSignDeleteButtonVisible(scope.row)"
type="danger"
plain
size="small"
@click="handleSignDelete(scope.row)"
>
<Icon icon="ep:remove" /> 减签
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 减签 -->
<TaskSignDeleteForm ref="taskSignDeleteFormRef" @success="handleSignDeleteSuccess" />
</el-drawer>
</template>
<script lang="ts" setup>
import { isEmpty } from '@/utils/is'
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import TaskSignDeleteForm from './TaskSignDeleteForm.vue'
defineOptions({ name: 'TaskSignList' })
const message = useMessage() // 消息弹窗
const drawerVisible = ref(false) // 抽屉的是否展示
const parentTask = ref({} as any)
/** 打开弹窗 */
const open = async (task: any) => {
if (isEmpty(task.children)) {
message.warning('该任务没有子任务')
return
}
parentTask.value = task
// 展开抽屉
drawerVisible.value = true
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 发起减签 */
const taskSignDeleteFormRef = ref()
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const handleSignDelete = (item: any) => {
taskSignDeleteFormRef.value.open(item.id)
}
const handleSignDeleteSuccess = () => {
emit('success')
// 关闭抽屉
drawerVisible.value = false
}
/** 是否显示减签按钮 */
const isSignDeleteButtonVisible = (task: any) => {
return task && task.children && !isEmpty(task.children)
}
</script>
<template>
<Dialog v-model="dialogVisible" title="转派任务" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="新审批人" prop="assigneeUserId">
<el-select v-model="formData.assigneeUserId" clearable style="width: 100%">
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="转派理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入转派理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'TaskTransferForm' })
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
assigneeUserId: undefined,
reason: ''
})
const formRules = ref({
assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const userList = ref<any[]>([]) // 用户列表
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
// 获得用户列表
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.transferTask(formData.value)
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
assigneeUserId: undefined,
reason: ''
}
formRef.value?.resetFields()
}
</script>
<template> <template>
<ContentWrap> <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }" class="position-relative">
<!-- 审批信息 --> <div class="processInstance-wrap-main">
<el-card <el-scrollbar>
v-for="(item, index) in runningTasks" <img
:key="index" class="position-absolute right-20px"
v-loading="processInstanceLoading" width="150"
class="box-card" :src="auditIconsMap[processInstance.status]"
> alt=""
<template #header>
<span class="el-icon-picture-outline">审批任务【{{ item.name }}</span>
</template>
<el-col :offset="6" :span="16">
<el-form
:ref="'form' + index"
:model="auditForms[index]"
:rules="auditRule"
label-width="100px"
>
<el-form-item v-if="processInstance && processInstance.name" label="流程名">
{{ processInstance.name }}
</el-form-item>
<el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
{{ processInstance?.startUser.nickname }}
<el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag>
</el-form-item>
<el-card v-if="runningTasks[index].formId > 0" class="mb-15px !-mt-10px">
<template #header>
<span class="el-icon-picture-outline">
填写表单【{{ runningTasks[index]?.formName }}
</span>
</template>
<form-create
v-model="approveForms[index].value"
v-model:api="approveFormFApis[index]"
:option="approveForms[index].option"
:rule="approveForms[index].rule"
/> />
</el-card> <div class="text-#878c93 h-15px">编号:{{ id }}</div>
<el-form-item label="审批建议" prop="reason"> <el-divider class="!my-8px" />
<el-input <div class="flex items-center gap-5 mb-10px h-40px">
v-model="auditForms[index].reason" <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
placeholder="请输入审批建议" <dict-tag
type="textarea" v-if="processInstance.status"
:type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
:value="processInstance.status"
/> />
</el-form-item> </div>
<el-form-item label="抄送人" prop="copyUserIds">
<el-select v-model="auditForms[index].copyUserIds" multiple placeholder="请选择抄送人"> <div class="flex items-center gap-5 mb-10px text-13px h-35px">
<el-option <div
v-for="itemx in userOptions" class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
:key="itemx.id"
:label="itemx.nickname"
:value="itemx.id"
/>
</el-select>
</el-form-item>
</el-form>
<div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
<!-- TODO @jason:建议搞个 if 来判断,替代现有的 !item.buttonsSetting || item.buttonsSetting[OpsButtonType.APPROVE]?.enable -->
<el-button
type="success"
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.APPROVE]?.enable"
@click="handleAudit(item, true)"
>
<Icon icon="ep:select" />
<!-- TODO @jason:这个也是类似哈,搞个方法来生成名字 -->
{{
item.buttonsSetting?.[OperationButtonType.APPROVE]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.APPROVE)
}}
</el-button>
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.REJECT]?.enable"
type="danger"
@click="handleAudit(item, false)"
>
<Icon icon="ep:close" />
{{
item.buttonsSetting?.[OperationButtonType.REJECT].displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.REJECT)
}}
</el-button>
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.TRANSFER]?.enable"
type="primary"
@click="openTaskUpdateAssigneeForm(item.id)"
>
<Icon icon="ep:edit" />
{{
item.buttonsSetting?.[OperationButtonType.TRANSFER]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.TRANSFER)
}}
</el-button>
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.DELEGATE]?.enable"
type="primary"
@click="handleDelegate(item)"
>
<Icon icon="ep:position" />
{{
item.buttonsSetting?.[OperationButtonType.DELEGATE]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.DELEGATE)
}}
</el-button>
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.ADD_SIGN]?.enable"
type="primary"
@click="handleSign(item)"
>
<Icon icon="ep:plus" />
{{
item.buttonsSetting?.[OperationButtonType.ADD_SIGN]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.ADD_SIGN)
}}
</el-button>
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.RETURN]?.enable"
type="warning"
@click="handleBack(item)"
> >
<Icon icon="ep:back" /> <el-avatar
{{ :size="28"
item.buttonsSetting?.[OperationButtonType.RETURN]?.displayName || v-if="processInstance?.startUser?.avatar"
OPERATION_BUTTON_NAME.get(OperationButtonType.RETURN) :src="processInstance?.startUser?.avatar"
}} />
</el-button> <el-avatar :size="28" v-else-if="processInstance?.startUser?.nickname">
{{ processInstance?.startUser?.nickname.substring(0, 1) }}
</el-avatar>
{{ processInstance?.startUser?.nickname }}
</div>
<div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
</div> </div>
</el-col>
</el-card>
<!-- 申请信息 --> <el-tabs v-model="activeTab">
<el-card v-loading="processInstanceLoading" class="box-card"> <!-- 表单信息 -->
<template #header> <el-tab-pane label="审批详情" name="form">
<span class="el-icon-document">申请信息【{{ processInstance.name }}</span> <div class="form-scroll-area">
</template> <el-scrollbar>
<el-row>
<el-col :span="17" class="!flex !flex-col formCol">
<!-- 表单信息 -->
<div
v-loading="processInstanceLoading"
class="form-box flex flex-col mb-30px flex-1"
>
<!-- 情况一:流程表单 --> <!-- 情况一:流程表单 -->
<el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16"> <el-col v-if="processDefinition?.formType === 10">
<form-create <form-create
v-model="detailForm.value" v-model="detailForm.value"
v-model:api="fApi" v-model:api="fApi"
...@@ -143,80 +58,110 @@ ...@@ -143,80 +58,110 @@
/> />
</el-col> </el-col>
<!-- 情况二:业务表单 --> <!-- 情况二:业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20"> <div v-if="processDefinition?.formType === 20">
<BusinessFormComponent :id="processInstance.businessKey" /> <BusinessFormComponent :id="processInstance.businessKey" />
</div> </div>
</el-card> </div>
</el-col>
<el-col :span="7">
<!-- 审批记录时间线 -->
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
</el-col>
</el-row>
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 审批记录 --> <!-- 流程图 -->
<ProcessInstanceTaskList <el-tab-pane label="流程图" name="diagram">
:loading="tasksLoad" <div class="form-scroll-area">
:process-instance="processInstance" <ProcessInstanceSimpleViewer
:tasks="tasks" v-show="
@refresh="getTaskList" processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
"
:loading="processInstanceLoading"
:model-view="processModelView"
/> />
<!-- 高亮流程图 -->
<ProcessInstanceBpmnViewer <ProcessInstanceBpmnViewer
:id="`${id}`" v-show="
:bpmn-xml="bpmnXml" processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
"
:loading="processInstanceLoading" :loading="processInstanceLoading"
:process-instance="processInstance" :model-view="processModelView"
:tasks="tasks"
/> />
</div>
</el-tab-pane>
<!-- 流转记录 -->
<el-tab-pane label="流转记录" name="record">
<div class="form-scroll-area">
<el-scrollbar>
<ProcessInstanceTaskList :loading="processInstanceLoading" :id="id" />
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 弹窗:转派审批人 --> <!-- 流转评论 TODO 待开发 -->
<TaskTransferForm ref="taskTransferFormRef" @success="getDetail" /> <el-tab-pane label="流转评论" name="comment" v-if="false">
<!-- 弹窗:回退节点 --> <div class="form-scroll-area">
<TaskReturnForm ref="taskReturnFormRef" @success="getDetail" /> <el-scrollbar> 流转评论 </el-scrollbar>
<!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中--> </div>
<TaskDelegateForm ref="taskDelegateForm" @success="getDetail" /> </el-tab-pane>
<!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> </el-tabs>
<TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
<div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:process-instance="processInstance"
:process-definition="processDefinition"
:userOptions="userOptions"
@success="refresh"
/>
</div>
</el-scrollbar>
</div>
</ContentWrap> </ContentWrap>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useUserStore } from '@/store/modules/user' import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
import { BpmModelType } from '@/utils/constants'
import { setConfAndFields2 } from '@/utils/formCreate' import { setConfAndFields2 } from '@/utils/formCreate'
import { registerComponent } from '@/utils/routerHelper'
import type { ApiAttrs } from '@form-create/element-ui/types/config' import type { ApiAttrs } from '@form-create/element-ui/types/config'
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance' import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as TaskApi from '@/api/bpm/task' import * as UserApi from '@/api/system/user'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue' import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceSimpleViewer from './ProcessInstanceSimpleViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue' import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
import TaskReturnForm from './dialog/TaskReturnForm.vue' import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
import TaskDelegateForm from './dialog/TaskDelegateForm.vue' import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
import TaskTransferForm from './dialog/TaskTransferForm.vue' import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue' import { TaskStatusEnum } from '@/api/bpm/task'
import { registerComponent } from '@/utils/routerHelper' import runningSvg from '@/assets/svgs/bpm/running.svg'
import { isEmpty } from '@/utils/is' import approveSvg from '@/assets/svgs/bpm/approve.svg'
import * as UserApi from '@/api/system/user' import rejectSvg from '@/assets/svgs/bpm/reject.svg'
import { import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
OperationButtonType,
OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'BpmProcessInstanceDetail' }) defineOptions({ name: 'BpmProcessInstanceDetail' })
const props = defineProps<{
const { query } = useRoute() // 查询参数 id: string // 流程实例的编号
taskId?: string // 任务编号
activityId?: string //流程活动编号,用于抄送查看
}>()
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const { proxy } = getCurrentInstance() as any
const userId = useUserStore().getUser.id // 当前登录的编号
const id = query.id as unknown as string // 流程实例的编号
const processInstanceLoading = ref(false) // 流程实例的加载中 const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例 const processInstance = ref<any>({}) // 流程实例
const bpmnXml = ref('') // BPMN XML const processDefinition = ref<any>({}) // 流程定义
const tasksLoad = ref(true) // 任务的加载中 const processModelView = ref<any>({}) // 流程模型视图
const tasks = ref<any[]>([]) // 任务列表 const operationButtonRef = ref() // 操作按钮组件 ref
// ========== 审批信息 ========== const auditIconsMap = {
const runningTasks = ref<any[]>([]) // 运行中的任务 [TaskStatusEnum.RUNNING]: runningSvg,
const auditForms = ref<any[]>([]) // 审批任务的表单 [TaskStatusEnum.APPROVE]: approveSvg,
const auditRule = reactive({ [TaskStatusEnum.REJECT]: rejectSvg,
reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }] [TaskStatusEnum.CANCEL]: cancelSvg
}) }
const approveForms = ref<any[]>([]) // 审批通过时,额外的补充信息
const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi
// ========== 申请信息 ========== // ========== 申请信息 ==========
const fApi = ref<ApiAttrs>() // const fApi = ref<ApiAttrs>() //
...@@ -226,253 +171,124 @@ const detailForm = ref({ ...@@ -226,253 +171,124 @@ const detailForm = ref({
value: {} value: {}
}) // 流程实例的表单详情 }) // 流程实例的表单详情
/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
watch(
() => approveFormFApis.value,
(value) => {
value?.forEach((api) => {
api.btn.show(false)
api.resetBtn.show(false)
})
},
{
deep: true
}
)
/** 处理审批通过和不通过的操作 */
const handleAudit = async (task, pass) => {
// 1.1 获得对应表单
const index = runningTasks.value.indexOf(task)
const auditFormRef = proxy.$refs['form' + index][0]
// 1.2 校验表单
const elForm = unref(auditFormRef)
if (!elForm) return
let valid = await elForm.validate()
if (!valid) return
// 校验申请表单(可编辑字段)
// TODO @jason:之前这里是 if (!fApi.value) return;针对业务表单的情况下,会导致没办法审核,可能要看下。我这里改了点,看看是不是还有别的地方兼容性
if (fApi.value) {
valid = await fApi.value.validate()
if (!valid) return
}
// 2.1 提交审批
const data = {
id: task.id,
reason: auditForms.value[index].reason,
copyUserIds: auditForms.value[index].copyUserIds
}
if (pass) {
// 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
const formCreateApi = approveFormFApis.value[index]
if (formCreateApi) {
await formCreateApi.validate()
data.variables = approveForms.value[index].value
}
// 获取表单可编辑字段的值
if (fApi.value) {
data.variables = getWritableValueOfForm(task.fieldsPermission)
}
await TaskApi.approveTask(data)
message.success('审批通过成功')
} else {
await TaskApi.rejectTask(data)
message.success('审批不通过成功')
}
// 2.2 加载最新数据
getDetail()
}
/** 转派审批人 */
const taskTransferFormRef = ref()
const openTaskUpdateAssigneeForm = (id: string) => {
taskTransferFormRef.value.open(id)
}
/** 处理审批退回的操作 */
const taskDelegateForm = ref()
const handleDelegate = async (task) => {
taskDelegateForm.value.open(task.id)
}
/** 处理审批退回的操作 */
const taskReturnFormRef = ref()
const handleBack = async (task: any) => {
taskReturnFormRef.value.open(task.id)
}
/** 处理审批加签的操作 */
const taskSignCreateFormRef = ref()
const handleSign = async (task: any) => {
taskSignCreateFormRef.value.open(task.id)
}
/** 获得详情 */ /** 获得详情 */
const getDetail = async () => { const getDetail = () => {
// 1. 获得流程任务列表(审批记录)。 需要先获取任务,表单的权限设置需要根据任务来设置 getApprovalDetail()
await getTaskList()
// 2. 获得流程实例相关 getProcessModelView()
getProcessInstance()
} }
/** 加载流程实例 */ /** 加载流程实例 */
const BusinessFormComponent = ref(null) // 异步组件 const BusinessFormComponent = ref<any>(null) // 异步组件
const getProcessInstance = async () => { /** 获取审批详情 */
try { const getApprovalDetail = async () => {
processInstanceLoading.value = true processInstanceLoading.value = true
const data = await ProcessInstanceApi.getProcessInstance(id) try {
const param = {
processInstanceId: props.id,
activityId: props.activityId,
taskId: props.taskId
}
const data = await ProcessInstanceApi.getApprovalDetail(param)
if (!data) { if (!data) {
message.error('查询不到审批详情信息!')
return
}
if (!data.processDefinition || !data.processInstance) {
message.error('查询不到流程信息!') message.error('查询不到流程信息!')
return return
} }
processInstance.value = data processInstance.value = data.processInstance
processDefinition.value = data.processDefinition
// 设置表单信息 // 设置表单信息
const processDefinition = data.processDefinition if (processDefinition.value.formType === 10) {
if (processDefinition.formType === 10) { // 获取表单字段权限
const formFieldsPermission = data.formFieldsPermission
if (detailForm.value.rule.length > 0) { if (detailForm.value.rule.length > 0) {
detailForm.value.value = data.formVariables // 避免刷新 form-create 显示不了
detailForm.value.value = processInstance.value.formVariables
} else { } else {
setConfAndFields2( setConfAndFields2(
detailForm, detailForm,
processDefinition.formConf, processDefinition.value.formConf,
processDefinition.formFields, processDefinition.value.formFields,
data.formVariables processInstance.value.formVariables
) )
} }
nextTick().then(() => { nextTick().then(() => {
fApi.value?.btn.show(false) fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false) fApi.value?.resetBtn.show(false)
//@ts-ignore
fApi.value?.disabled(true) fApi.value?.disabled(true)
// 设置表单权限。后续需要改造成。只处理一个运行中的任务 // 设置表单字段权限
if (runningTasks.value.length > 0) { if (formFieldsPermission) {
const task = runningTasks.value.at(0) Object.keys(data.formFieldsPermission).forEach((item) => {
if (task.fieldsPermission) { setFieldPermission(item, formFieldsPermission[item])
Object.keys(task.fieldsPermission).forEach((item) => {
setFieldPermission(item, task.fieldsPermission[item])
}) })
} }
}
}) })
} else { } else {
// 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath) BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
} }
// 加载流程图 // 获取审批节点,显示 Timeline 的数据
bpmnXml.value = ( activityNodes.value = data.activityNodes
await DefinitionApi.getProcessDefinition(processDefinition.id as number)
)?.bpmnXml
} finally {
processInstanceLoading.value = false
}
}
/** 加载任务列表 */
const getTaskList = async () => {
runningTasks.value = []
auditForms.value = []
approveForms.value = []
approveFormFApis.value = []
try {
// 获得未取消的任务
tasksLoad.value = true
const data = await TaskApi.getTaskListByProcessInstanceId(id)
tasks.value = []
// 1.1 移除已取消的审批
data.forEach((task) => {
if (task.status !== 4) {
tasks.value.push(task)
}
})
// 1.2 排序,将未完成的排在前面,已完成的排在后面;
tasks.value.sort((a, b) => {
// 有已完成的情况,按照完成时间倒序
if (a.endTime && b.endTime) {
return b.endTime - a.endTime
} else if (a.endTime) {
return 1
} else if (b.endTime) {
return -1
// 都是未完成,按照创建时间倒序
} else {
return b.createTime - a.createTime
}
})
// 获得需要自己审批的任务 // 获取待办任务显示操作按钮
loadRunningTask(tasks.value) operationButtonRef.value?.loadTodoTask(data.todoTask)
} finally { } finally {
tasksLoad.value = false processInstanceLoading.value = false
} }
} }
/** /** 获取流程模型视图*/
* 设置 runningTasks 中的任务 const getProcessModelView = async () => {
*/ if (BpmModelType.BPMN === processDefinition.value?.modelType) {
const loadRunningTask = (tasks) => { // 重置,解决 BPMN 流程图刷新不会重新渲染问题
tasks.forEach((task) => { processModelView.value = {
if (!isEmpty(task.children)) { bpmnXml: ''
loadRunningTask(task.children)
} }
// 2.1 只有待处理才需要
if (task.status !== 1 && task.status !== 6) {
return
} }
// 2.2 自己不是处理人 const data = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
if (!task.assigneeUser || task.assigneeUser.id !== userId) { if (data) {
return processModelView.value = data
}
// 2.3 添加到处理任务
runningTasks.value.push({ ...task })
auditForms.value.push({
reason: '',
copyUserIds: []
})
// 2.4 处理 approve 表单
if (task.formId && task.formConf) {
const approveForm = {}
setConfAndFields2(approveForm, task.formConf, task.formFields, task.formVariables)
approveForms.value.push(approveForm)
} else {
approveForms.value.push({}) // 占位,避免为空
} }
})
} }
// 审批节点信息
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
/** /**
* 设置表单权限 * 设置表单权限
*/ */
const setFieldPermission = (field: string, permission: string) => { const setFieldPermission = (field: string, permission: string) => {
if (permission === '1') { if (permission === FieldPermissionType.READ) {
//@ts-ignore
fApi.value?.disabled(true, field) fApi.value?.disabled(true, field)
} }
if (permission === '2') { if (permission === FieldPermissionType.WRITE) {
//@ts-ignore
fApi.value?.disabled(false, field) fApi.value?.disabled(false, field)
} }
if (permission === '3') { if (permission === FieldPermissionType.NONE) {
//@ts-ignore
fApi.value?.hidden(true, field) fApi.value?.hidden(true, field)
} }
} }
/** /**
* 获取可以编辑字段的值 * 操作成功后刷新
*/ */
const getWritableValueOfForm = (fieldsPermission: Object) => { const refresh = () => {
const fieldsValue = {} // 重新获取详情
if (fieldsPermission && fApi.value) { getDetail()
Object.keys(fieldsPermission).forEach((item) => {
if (fieldsPermission[item] === '2') {
fieldsValue[item] = fApi.value.getValue(item)
}
})
}
return fieldsValue
} }
/** 当前的Tab */
const activeTab = ref('form')
/** 初始化 */ /** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => { onMounted(async () => {
...@@ -481,3 +297,50 @@ onMounted(async () => { ...@@ -481,3 +297,50 @@ onMounted(async () => {
userOptions.value = await UserApi.getSimpleUserList() userOptions.value = await UserApi.getSimpleUserList()
}) })
</script> </script>
<style lang="scss" scoped>
$wrap-padding-height: 20px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 194px;
.processInstance-wrap-main {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
);
overflow: auto;
.form-scroll-area {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
$process-header-height - 40px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
$process-header-height - 40px
);
overflow: auto;
display: flex;
flex-direction: column;
:deep(.box-card) {
height: 100%;
flex: 1;
.el-card__body {
height: 100%;
padding: 0;
}
}
}
}
.form-box {
:deep(.el-card) {
border: none;
}
}
</style>
<template>
<ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
<div class="processInstance-wrap-main">
<el-scrollbar>
<img
class="position-absolute right-20px"
width="150"
:src="auditIcons[processInstance.status]"
alt=""
/>
<div class="text-#878c93 h-15px">编号:{{ id }}</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px h-40px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
</div>
<div class="flex items-center gap-5 mb-10px text-13px h-35px">
<div
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
>
<img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
{{ processInstance?.startUser?.nickname }}
</div>
<div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
</div>
<el-tabs v-model="activeTab">
<!-- 表单信息 -->
<el-tab-pane label="审批详情" name="form">
<div class="form-scroll-area">
<el-scrollbar>
<el-row :gutter="10">
<el-col :span="18" class="!flex !flex-col formCol">
<!-- 表单信息 -->
<div
v-loading="processInstanceLoading"
class="form-box flex flex-col mb-30px flex-1"
>
<!-- 情况一:流程表单 -->
<el-col
v-if="processInstance?.processDefinition?.formType === 10"
:offset="6"
:span="16"
>
<form-create
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
</el-col>
<!-- 情况二:业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
</div>
</el-col>
<el-col :span="6">
<!-- 审批记录时间线 -->
<ProcessInstanceTimeline ref="timelineRef" :process-instance-id="id" />
</el-col>
</el-row>
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 流程图 -->
<el-tab-pane label="流程图" name="diagram">
<ProcessInstanceBpmnViewer
:id="`${id}`"
:bpmn-xml="bpmnXml"
:loading="processInstanceLoading"
:process-instance="processInstance"
:tasks="tasks"
/>
</el-tab-pane>
<!-- 流转记录 -->
<el-tab-pane label="流转记录" name="record">
<ProcessInstanceTaskList
:loading="tasksLoad"
:process-instance="processInstance"
:tasks="tasks"
@refresh="getTaskList"
/>
</el-tab-pane>
<!-- 流转评论 TODO 待开发 -->
<el-tab-pane label="流转评论" name="comment"> 流转评论 </el-tab-pane>
</el-tabs>
<div
class="b-t-solid border-t-1px border-[var(--el-border-color)]"
v-if="activeTab === 'form'"
>
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:processInstance="processInstance"
:userOptions="userOptions"
@success="refresh"
/>
</div>
</el-scrollbar>
</div>
</ContentWrap>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
import { setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as TaskApi from '@/api/bpm/task'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
import { registerComponent } from '@/utils/routerHelper'
import * as UserApi from '@/api/system/user'
import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
import audit1 from '@/assets/svgs/bpm/audit1.svg'
import audit2 from '@/assets/svgs/bpm/audit2.svg'
import audit3 from '@/assets/svgs/bpm/audit3.svg'
defineOptions({ name: 'BpmProcessInstanceDetail' })
const props = defineProps<{
id: string // 流程实例的编号
taskId?: string // 任务编号
activityId?: string //流程活动编号,用于抄送查看
}>()
const message = useMessage() // 消息弹窗
const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例
const operationButtonRef = ref()
const timelineRef = ref()
const bpmnXml = ref('') // BPMN XML
const tasksLoad = ref(true) // 任务的加载中
const tasks = ref<any[]>([]) // 任务列表
const auditIcons = {
1: audit1,
2: audit2,
3: audit3
}
// ========== 申请信息 ==========
const fApi = ref<ApiAttrs>() //
const detailForm = ref({
rule: [],
option: {},
value: {}
}) // 流程实例的表单详情
/** 获得详情 */
const getDetail = () => {
// 1. 获得流程实例相关
getProcessInstance()
// 2. 获得流程任务列表(审批记录)
getTaskList()
}
/** 加载流程实例 */
const BusinessFormComponent = ref<any>(null) // 异步组件
const getProcessInstance = async () => {
try {
processInstanceLoading.value = true
const data = await ProcessInstanceApi.getProcessInstance(props.id)
if (!data) {
message.error('查询不到流程信息!')
return
}
processInstance.value = data
// 设置表单信息
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
// 获取表单字段权限
let fieldsPermission = undefined
if (props.taskId || props.activityId) {
fieldsPermission = await ProcessInstanceApi.getFormFieldsPermission({
processInstanceId: props.id,
taskId: props.taskId,
activityId: props.activityId
})
}
setConfAndFields2(
detailForm,
processDefinition.formConf,
processDefinition.formFields,
data.formVariables
)
nextTick().then(() => {
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
if (fieldsPermission) {
Object.keys(fieldsPermission).forEach((item) => {
setFieldPermission(item, fieldsPermission[item])
})
}
})
} else {
// 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
}
// 加载流程图
bpmnXml.value = (await DefinitionApi.getProcessDefinition(processDefinition.id))?.bpmnXml
} finally {
processInstanceLoading.value = false
}
}
/**
* 设置表单权限
*/
const setFieldPermission = (field: string, permission: string) => {
if (permission === FieldPermissionType.READ) {
fApi.value?.disabled(true, field)
}
if (permission === FieldPermissionType.WRITE) {
fApi.value?.disabled(false, field)
}
if (permission === FieldPermissionType.NONE) {
fApi.value?.hidden(true, field)
}
}
/** 加载任务列表 */
const getTaskList = async () => {
try {
// 获得未取消的任务
tasksLoad.value = true
const data = await TaskApi.getTaskListByProcessInstanceId(props.id)
tasks.value = []
// 1.1 移除已取消的审批
data.forEach((task: any) => {
if (task.status !== 4) {
tasks.value.push(task)
}
})
// 1.2 排序,将未完成的排在前面,已完成的排在后面;
tasks.value.sort((a, b) => {
// 有已完成的情况,按照完成时间倒序
if (a.endTime && b.endTime) {
return b.endTime - a.endTime
} else if (a.endTime) {
return 1
} else if (b.endTime) {
return -1
// 都是未完成,按照创建时间倒序
} else {
return b.createTime - a.createTime
}
})
// 获得需要自己审批的任务
operationButtonRef.value?.loadRunningTask(tasks.value)
} finally {
tasksLoad.value = false
}
}
/**
* 操作成功后刷新
*/
const refresh = () => {
// 重新获取详情
getDetail()
// 刷新审批详情 Timeline
timelineRef.value?.refresh()
}
/** 当前的Tab */
const activeTab = ref('form')
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => {
getDetail()
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
})
</script>
<style lang="scss" scoped>
$wrap-padding-height: 30px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 194px;
.processInstance-wrap-main {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
);
overflow: auto;
.form-scroll-area {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
$process-header-height - 40px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
$process-header-height - 40px
);
overflow: auto;
}
}
.form-box {
:deep(.el-card) {
border: none;
}
}
</style>
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
:inline="true" :inline="true"
label-width="68px" label-width="68px"
> >
<el-form-item label="流程名称" prop="name"> <el-form-item label="" prop="name">
<el-input <el-input
v-model="queryParams.name" v-model="queryParams.name"
placeholder="请输入流程名称" placeholder="请输入流程名称"
...@@ -19,21 +19,50 @@ ...@@ -19,21 +19,50 @@
class="!w-240px" class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="所属流程" prop="processDefinitionKey">
<el-input <el-form-item>
v-model="queryParams.processDefinitionKey" <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
placeholder="请输入流程定义的标识" </el-form-item>
<!-- TODO @ tuituji:style 可以使用 unocss -->
<el-form-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }">
<!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 -->
<el-select
v-model="queryParams.category"
placeholder="请选择流程分类"
clearable clearable
@keyup.enter="handleQuery" class="!w-155px"
class="!w-240px" >
<el-option
v-for="category in categoryList"
:key="category.code"
:label="category.name"
:value="category.code"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="流程分类" prop="category">
<!-- 高级筛选 -->
<!-- TODO @ tuituji:style 可以使用 unocss -->
<el-form-item :style="{ position: 'absolute', right: '0px' }">
<el-button v-popover="popoverRef" v-click-outside="onClickOutside" :icon="List">
高级筛选
</el-button>
<el-popover
ref="popoverRef"
trigger="click"
virtual-triggering
persistent
:width="400"
:show-arrow="false"
placement="bottom-end"
>
<el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
<el-select <el-select
v-model="queryParams.category" v-model="queryParams.category"
placeholder="请选择流程分类" placeholder="请选择流程发起人"
clearable clearable
class="!w-240px" class="!w-390px"
> >
<el-option <el-option
v-for="category in categoryList" v-for="category in categoryList"
...@@ -43,12 +72,26 @@ ...@@ -43,12 +72,26 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="流程状态" prop="status"> <el-form-item
label="所属流程"
class="bold-label"
label-position="top"
prop="processDefinitionKey"
>
<el-input
v-model="queryParams.processDefinitionKey"
placeholder="请输入流程定义的标识"
clearable
@keyup.enter="handleQuery"
class="!w-390px"
/>
</el-form-item>
<el-form-item label="流程状态" class="bold-label" label-position="top" prop="status">
<el-select <el-select
v-model="queryParams.status" v-model="queryParams.status"
placeholder="请选择流程状态" placeholder="请选择流程状态"
clearable clearable
class="!w-240px" class="!w-390px"
> >
<el-option <el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
...@@ -58,7 +101,7 @@ ...@@ -58,7 +101,7 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="发起时间" prop="createTime"> <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
<el-date-picker <el-date-picker
v-model="queryParams.createTime" v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
...@@ -69,17 +112,8 @@ ...@@ -69,17 +112,8 @@
class="!w-240px" class="!w-240px"
/> />
</el-form-item> </el-form-item>
<el-form-item> </el-popover>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <!-- TODO @tuituji:这里应该有确认,和取消、清空搜索条件,三个按钮。 -->
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
v-hasPermi="['bpm:process-instance:query']"
@click="handleCreate(undefined)"
>
<Icon icon="ep:plus" class="mr-5px" /> 发起流程
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
...@@ -95,6 +129,8 @@ ...@@ -95,6 +129,8 @@
min-width="100" min-width="100"
fixed="left" fixed="left"
/> />
<!-- TODO @芋艿:摘要 -->
<!-- TODO @tuituji:流程状态。可见需求文档里 -->
<el-table-column label="流程状态" prop="status" width="120"> <el-table-column label="流程状态" prop="status" width="120">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
...@@ -114,7 +150,7 @@ ...@@ -114,7 +150,7 @@
width="180" width="180"
:formatter="dateFormatter" :formatter="dateFormatter"
/> />
<el-table-column align="center" label="耗时" prop="durationInMillis" width="160"> <!--<el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
<template #default="scope"> <template #default="scope">
{{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }} {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
</template> </template>
...@@ -126,7 +162,7 @@ ...@@ -126,7 +162,7 @@
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="流程编号" align="center" prop="id" min-width="320px" /> -->
<el-table-column label="操作" align="center" fixed="right" width="180"> <el-table-column label="操作" align="center" fixed="right" width="180">
<template #default="scope"> <template #default="scope">
<el-button <el-button
...@@ -162,11 +198,13 @@ ...@@ -162,11 +198,13 @@
</ContentWrap> </ContentWrap>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。
import { List } from '@element-plus/icons-vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, formatPast2 } from '@/utils/formatTime' import { dateFormatter } from '@/utils/formatTime'
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import * as ProcessInstanceApi from '@/api/bpm/processInstance' import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { CategoryApi } from '@/api/bpm/category' import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import { ProcessInstanceVO } from '@/api/bpm/processInstance' import { ProcessInstanceVO } from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition' import * as DefinitionApi from '@/api/bpm/definition'
...@@ -189,7 +227,7 @@ const queryParams = reactive({ ...@@ -189,7 +227,7 @@ const queryParams = reactive({
createTime: [] createTime: []
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const categoryList = ref([]) // 流程分类列表 const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
...@@ -222,7 +260,6 @@ const handleCreate = async (row?: ProcessInstanceVO) => { ...@@ -222,7 +260,6 @@ const handleCreate = async (row?: ProcessInstanceVO) => {
const processDefinitionDetail = await DefinitionApi.getProcessDefinition( const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
row.processDefinitionId row.processDefinitionId
) )
debugger
if (processDefinitionDetail.formType === 20) { if (processDefinitionDetail.formType === 20) {
message.error('重新发起流程失败,原因:该流程使用业务表单,不支持重新发起') message.error('重新发起流程失败,原因:该流程使用业务表单,不支持重新发起')
return return
...@@ -261,6 +298,15 @@ const handleCancel = async (row) => { ...@@ -261,6 +298,15 @@ const handleCancel = async (row) => {
await getList() await getList()
} }
// TODO @tuituji:这个 import 是不是没用哈?
import { ClickOutside as vClickOutside } from 'element-plus'
// TODO @tuituji:onClickAdvancedSearch。方法名叫这个,会更好一些哇?打开高级搜索。
const popoverRef = ref()
const onClickOutside = () => {
unref(popoverRef).popperRef?.delayHide?.()
}
/** 激活时 **/ /** 激活时 **/
onActivated(() => { onActivated(() => {
getList() getList()
...@@ -272,3 +318,8 @@ onMounted(async () => { ...@@ -272,3 +318,8 @@ onMounted(async () => {
categoryList.value = await CategoryApi.getCategorySimpleList() categoryList.value = await CategoryApi.getCategorySimpleList()
}) })
</script> </script>
<style>
.bold-label .el-form-item__label {
font-weight: bold; /* 将字体加粗 */
}
</style>
<template> <template>
<SimpleProcessDesigner :model-id="modelId" /> <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
<SimpleProcessDesigner :model-id="modelId" @success="close" />
</ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/' import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({ defineOptions({
name: 'SimpleWorkflowDesignEditor' name: 'SimpleModelDesign'
}) })
const router = useRouter() // 路由
const { query } = useRoute() // 路由的查询 const { query } = useRoute() // 路由的查询
const modelId = query.modelId as string const modelId = query.modelId as string
const close = () => {
router.push({ path: '/bpm/manager/model' })
}
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
...@@ -45,7 +45,12 @@ ...@@ -45,7 +45,12 @@
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" /> <el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" />
<el-table-column align="center" label="流程发起人" prop="startUserName" min-width="100" /> <el-table-column
align="center"
label="流程发起人"
prop="startUser.nickname"
min-width="100"
/>
<el-table-column <el-table-column
:formatter="dateFormatter" :formatter="dateFormatter"
align="center" align="center"
...@@ -53,8 +58,11 @@ ...@@ -53,8 +58,11 @@
prop="processInstanceStartTime" prop="processInstanceStartTime"
width="180" width="180"
/> />
<el-table-column align="center" label="抄送任务" prop="taskName" min-width="180" /> <el-table-column align="center" label="抄送节点" prop="activityName" min-width="180" />
<el-table-column align="center" label="抄送人" prop="creatorName" min-width="100" /> <el-table-column align="center" label="抄送人" min-width="100">
<template #default="scope"> {{ scope.row.createUser?.nickname || '系统' }} </template>
</el-table-column>
<el-table-column align="center" label="抄送意见" prop="reason" width="150" />
<el-table-column <el-table-column
align="center" align="center"
label="抄送时间" label="抄送时间"
......
...@@ -24,7 +24,7 @@ defineOptions({ name: 'CrmProductDetail' }) ...@@ -24,7 +24,7 @@ defineOptions({ name: 'CrmProductDetail' })
const route = useRoute() const route = useRoute()
const message = useMessage() const message = useMessage()
const id = Number(route.params.id) // 编号 const id = route.params.id // 编号
const loading = ref(true) // 加载中 const loading = ref(true) // 加载中
const product = ref<ProductApi.ProductVO>({} as ProductApi.ProductVO) // 详情 const product = ref<ProductApi.ProductVO>({} as ProductApi.ProductVO) // 详情
......
...@@ -49,7 +49,11 @@ const designerConfig = ref({ ...@@ -49,7 +49,11 @@ const designerConfig = ref({
switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段 switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
autoActive: true, // 是否自动选中拖入的组件 autoActive: true, // 是否自动选中拖入的组件
useTemplate: false, // 是否生成vue2语法的模板组件 useTemplate: false, // 是否生成vue2语法的模板组件
formOptions: {}, // 定义表单配置默认值 formOptions: {
form: {
labelWidth: '100px' // 设置默认的 label 宽度为 100px
}
}, // 定义表单配置默认值
fieldReadonly: false, // 配置field是否可以编辑 fieldReadonly: false, // 配置field是否可以编辑
hiddenDragMenu: false, // 隐藏拖拽操作按钮 hiddenDragMenu: false, // 隐藏拖拽操作按钮
hiddenDragBtn: false, // 隐藏拖拽按钮 hiddenDragBtn: false, // 隐藏拖拽按钮
......
...@@ -27,7 +27,7 @@ defineOptions({ name: 'IoTDeviceDetail' }) ...@@ -27,7 +27,7 @@ defineOptions({ name: 'IoTDeviceDetail' })
const route = useRoute() const route = useRoute()
const message = useMessage() const message = useMessage()
const id = Number(route.params.id) // 编号 const id = route.params.id // 编号
const loading = ref(true) // 加载中 const loading = ref(true) // 加载中
const product = ref<ProductVO>({} as ProductVO) // 产品详情 const product = ref<ProductVO>({} as ProductVO) // 产品详情
const device = ref<DeviceVO>({} as DeviceVO) // 设备详情 const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
......
...@@ -33,7 +33,7 @@ const { currentRoute } = useRouter() ...@@ -33,7 +33,7 @@ const { currentRoute } = useRouter()
const route = useRoute() const route = useRoute()
const message = useMessage() const message = useMessage()
const id = Number(route.params.id) // 编号 const id = route.params.id // 编号
const loading = ref(true) // 加载中 const loading = ref(true) // 加载中
const product = ref<ProductVO>({} as ProductVO) // 详情 const product = ref<ProductVO>({} as ProductVO) // 详情
const activeTab = ref('info') // 默认激活的标签页 const activeTab = ref('info') // 默认激活的标签页
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
:rules="rules" :rules="rules"
label-width="120px" label-width="120px"
> >
<el-form-item label="分销类型" props="subCommissionType"> <el-form-item label="分销类型" prop="subCommissionType">
<el-radio-group <el-radio-group
v-model="formData.subCommissionType" v-model="formData.subCommissionType"
class="w-80" class="w-80"
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<el-radio :value="true" class="radio">单独设置</el-radio> <el-radio :value="true" class="radio">单独设置</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="商品规格" props="specType"> <el-form-item label="商品规格" prop="specType">
<el-radio-group v-model="formData.specType" class="w-80" @change="onChangeSpec"> <el-radio-group v-model="formData.specType" class="w-80" @change="onChangeSpec">
<el-radio :value="false" class="radio">单规格</el-radio> <el-radio :value="false" class="radio">单规格</el-radio>
<el-radio :value="true">多规格</el-radio> <el-radio :value="true">多规格</el-radio>
......
...@@ -80,6 +80,7 @@ ...@@ -80,6 +80,7 @@
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"> <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" label="ID" min-width="180" prop="id" />
<el-table-column align="center" label="封面" min-width="80" prop="picUrl"> <el-table-column align="center" label="封面" min-width="80" prop="picUrl">
<template #default="{ row }"> <template #default="{ row }">
<el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" /> <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
......
...@@ -5,21 +5,34 @@ ...@@ -5,21 +5,34 @@
v-loading="formLoading" v-loading="formLoading"
:model="formData" :model="formData"
:rules="formRules" :rules="formRules"
label-width="80" label-width="90"
> >
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12" :xs="24"> <el-col :span="12" :xs="24">
<el-form-item label="可用佣金" prop="price"> <el-form-item label="分销员" prop="userId">
<el-input-number v-model="formData.price" :min="0" class="w-1/1!" /> <el-input
</el-form-item> v-model="formData.userId"
</el-col> v-loading="formLoading"
<el-col :span="12" :xs="24"> placeholder="请输入分销员编号"
<el-form-item label="冻结佣金" prop="price"> >
<el-input-number v-model="formData.frozenPrice" :min="0" class="w-1/1!" /> <template #append>
<el-button @click="handleGetUser(formData.userId, '分销员')">
<Icon class="mr-5px" icon="ep:search" />
</el-button>
</template>
</el-input>
</el-form-item> </el-form-item>
<!-- 展示分销员的信息 -->
<el-descriptions v-if="userInfo.user" :column="1" border>
<el-descriptions-item label="头像">
<el-avatar :src="userInfo.user?.avatar" />
</el-descriptions-item>
<el-descriptions-item label="昵称">{{ userInfo.user?.nickname }}</el-descriptions-item>
</el-descriptions>
</el-col> </el-col>
<el-col :span="12" :xs="24"> <el-col :span="12" :xs="24">
<el-form-item label="推广人" prop="bindUserId"> <el-form-item label="上级推广人" prop="bindUserId">
<el-input <el-input
v-model="formData.bindUserId" v-model="formData.bindUserId"
v-loading="formLoading" v-loading="formLoading"
...@@ -49,40 +62,6 @@ ...@@ -49,40 +62,6 @@
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-col> </el-col>
<el-col :span="12" :xs="24">
<el-form-item label="分销员" prop="userId">
<el-input
v-model="formData.userId"
v-loading="formLoading"
placeholder="请输入分销员编号"
>
<template #append>
<el-button @click="handleGetUser(formData.userId, '分销员')">
<Icon class="mr-5px" icon="ep:search" />
</el-button>
</template>
</el-input>
</el-form-item>
<!-- 展示分销员的信息 -->
<el-descriptions v-if="userInfo.user" :column="1" border>
<el-descriptions-item label="头像">
<el-avatar :src="userInfo.user?.avatar" />
</el-descriptions-item>
<el-descriptions-item label="昵称">{{ userInfo.user?.nickname }}</el-descriptions-item>
<el-descriptions-item label="推广资格">
<el-switch
v-model="formData.brokerageEnabled"
:disabled="!checkPermi(['trade:brokerage-user:update-bind-user'])"
active-text="有"
inactive-text="无"
inline-prompt
/>
</el-descriptions-item>
<el-descriptions-item label="成为推广员的时间">
{{ formatDate(userInfo.user?.brokerageTime) }}
</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
...@@ -95,9 +74,8 @@ ...@@ -95,9 +74,8 @@
import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user' import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
import * as UserApi from '@/api/member/user' import * as UserApi from '@/api/member/user'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { checkPermi } from '@/utils/permission'
defineOptions({ name: 'CreateUserForm' }) defineOptions({ name: 'BrokerageUserCreateForm' })
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
...@@ -106,10 +84,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示 ...@@ -106,10 +84,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formData = ref({ const formData = ref({
userId: undefined, userId: undefined,
bindUserId: undefined, bindUserId: undefined
brokerageEnabled: false,
price: 0,
frozenPrice: 0
}) })
const formRef = ref() // 表单 Ref const formRef = ref() // 表单 Ref
const formRules = reactive({ const formRules = reactive({
...@@ -152,10 +127,7 @@ const resetForm = () => { ...@@ -152,10 +127,7 @@ const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
formData.value = { formData.value = {
userId: undefined, userId: undefined,
bindUserId: undefined, bindUserId: undefined
brokerageEnabled: false,
price: 0,
frozenPrice: 0
} }
userInfo.bindUser = undefined userInfo.bindUser = undefined
......
...@@ -43,8 +43,8 @@ ...@@ -43,8 +43,8 @@
import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user' import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
/** 修改上级推广人表单 */ /** 修改分销用户 */
defineOptions({ name: 'UpdateBindUserForm' }) defineOptions({ name: 'BrokerageUserUpdateForm' })
const { t } = useI18n() // 国际化 const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
......
...@@ -194,13 +194,13 @@ ...@@ -194,13 +194,13 @@
/> />
</ContentWrap> </ContentWrap>
<!-- 修改上级推广人表单 --> <!-- 修改上级推广人表单 -->
<UpdateBindUserForm ref="updateBindUserFormRef" @success="getList" /> <BrokerageUserUpdateForm ref="updateFormRef" @success="getList" />
<!-- 推广人列表 --> <!-- 推广人列表 -->
<BrokerageUserListDialog ref="brokerageUserListDialogRef" /> <BrokerageUserListDialog ref="listDialogRef" />
<!-- 推广订单列表 --> <!-- 推广订单列表 -->
<BrokerageOrderListDialog ref="brokerageOrderListDialogRef" /> <BrokerageOrderListDialog ref="orderDialogRef" />
<!-- 创建分销员 --> <!-- 创建分销员 -->
<CreateUserForm ref="createUserFormRef" /> <BrokerageUserCreateForm ref="createFormRef" @success="getList" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
...@@ -208,10 +208,10 @@ import { dateFormatter } from '@/utils/formatTime' ...@@ -208,10 +208,10 @@ import { dateFormatter } from '@/utils/formatTime'
import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user' import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
import { checkPermi } from '@/utils/permission' import { checkPermi } from '@/utils/permission'
import { fenToYuanFormat } from '@/utils/formatter' import { fenToYuanFormat } from '@/utils/formatter'
import UpdateBindUserForm from '@/views/mall/trade/brokerage/user/UpdateBindUserForm.vue' import BrokerageUserUpdateForm from '@/views/mall/trade/brokerage/user/BrokerageUserUpdateForm.vue'
import BrokerageUserListDialog from '@/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue' import BrokerageUserListDialog from '@/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue'
import BrokerageOrderListDialog from '@/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue' import BrokerageOrderListDialog from '@/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue'
import CreateUserForm from '@/views/mall/trade/brokerage/user/CreateUserForm.vue' import BrokerageUserCreateForm from '@/views/mall/trade/brokerage/user/BrokerageUserCreateForm.vue'
defineOptions({ name: 'TradeBrokerageUser' }) defineOptions({ name: 'TradeBrokerageUser' })
...@@ -271,27 +271,27 @@ const handleCommand = (command: string, row: BrokerageUserApi.BrokerageUserVO) = ...@@ -271,27 +271,27 @@ const handleCommand = (command: string, row: BrokerageUserApi.BrokerageUserVO) =
} }
/** 打开推广人列表 */ /** 打开推广人列表 */
const brokerageUserListDialogRef = ref() const listDialogRef = ref()
const openBrokerageUserTable = (id: number) => { const openBrokerageUserTable = (id: number) => {
brokerageUserListDialogRef.value.open(id) listDialogRef.value.open(id)
} }
/** 打开推广订单列表 */ /** 打开推广订单列表 */
const brokerageOrderListDialogRef = ref() const orderDialogRef = ref()
const openBrokerageOrderTable = (id: number) => { const openBrokerageOrderTable = (id: number) => {
brokerageOrderListDialogRef.value.open(id) orderDialogRef.value.open(id)
} }
/** 打开表单:修改上级推广人 */ /** 打开表单:修改上级推广人 */
const updateBindUserFormRef = ref() const updateFormRef = ref()
const openUpdateBindUserForm = (row: BrokerageUserApi.BrokerageUserVO) => { const openUpdateBindUserForm = (row: BrokerageUserApi.BrokerageUserVO) => {
updateBindUserFormRef.value.open(row) updateFormRef.value.open(row)
} }
/** 创建分销员 */ /** 创建分销员 */
const createUserFormRef = ref<InstanceType<typeof CreateUserForm>>() const createFormRef = ref<InstanceType<typeof CreateUserForm>>()
const openCreateUserForm = () => { const openCreateUserForm = () => {
createUserFormRef.value?.open() createFormRef.value?.open()
} }
/** 清除上级推广人 */ /** 清除上级推广人 */
......
...@@ -113,7 +113,7 @@ const getUserData = async (id: number) => { ...@@ -113,7 +113,7 @@ const getUserData = async (id: number) => {
const { currentRoute } = useRouter() // 路由 const { currentRoute } = useRouter() // 路由
const { delView } = useTagsViewStore() // 视图操作 const { delView } = useTagsViewStore() // 视图操作
const route = useRoute() const route = useRoute()
const id = Number(route.params.id) const id = route.params.id
/* 用户钱包相关信息 */ /* 用户钱包相关信息 */
const WALLET_INIT_DATA = { const WALLET_INIT_DATA = {
balance: 0, balance: 0,
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<el-form-item label="应用名" prop="name"> <el-form-item label="应用名" prop="name">
<el-input v-model="formData.name" placeholder="请输入应用名" /> <el-input v-model="formData.name" placeholder="请输入应用名" />
</el-form-item> </el-form-item>
<el-form-item label="应用标识" prop="name"> <el-form-item label="应用标识" prop="appKey">
<el-input v-model="formData.appKey" placeholder="请输入应用标识" /> <el-input v-model="formData.appKey" placeholder="请输入应用标识" />
</el-form-item> </el-form-item>
<el-form-item label="开启状态" prop="status"> <el-form-item label="开启状态" prop="status">
......
...@@ -257,7 +257,6 @@ const resetForm = (appId, code) => { ...@@ -257,7 +257,6 @@ const resetForm = (appId, code) => {
const fileBeforeUpload = (file, fileAccept) => { const fileBeforeUpload = (file, fileAccept) => {
let format = '.' + file.name.split('.')[1] let format = '.' + file.name.split('.')[1]
if (format !== fileAccept) { if (format !== fileAccept) {
debugger
message.error('请上传指定格式"' + fileAccept + '"文件') message.error('请上传指定格式"' + fileAccept + '"文件')
return false return false
} }
......
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
plain plain
@click="handleExport" @click="handleExport"
:loading="exportLoading" :loading="exportLoading"
v-hasPermi="['system:tenant:export']" v-hasPermi="['pay:order:export']"
> >
<Icon icon="ep:download" class="mr-5px" /> 导出 <Icon icon="ep:download" class="mr-5px" /> 导出
</el-button> </el-button>
...@@ -192,6 +192,7 @@ import { dateFormatter } from '@/utils/formatTime' ...@@ -192,6 +192,7 @@ import { dateFormatter } from '@/utils/formatTime'
import * as OrderApi from '@/api/pay/order' import * as OrderApi from '@/api/pay/order'
import OrderDetail from './OrderDetail.vue' import OrderDetail from './OrderDetail.vue'
import download from '@/utils/download' import download from '@/utils/download'
import { getAppList } from '@/api/pay/app'
defineOptions({ name: 'PayOrder' }) defineOptions({ name: 'PayOrder' })
...@@ -263,6 +264,7 @@ const openDetail = (id: number) => { ...@@ -263,6 +264,7 @@ const openDetail = (id: number) => {
/** 初始化 **/ /** 初始化 **/
onMounted(async () => { onMounted(async () => {
await getList() await getList()
appList.value = await getAppList()
}) })
</script> </script>
<style> <style>
......
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
label="创建时间" label="创建时间"
align="center" align="center"
prop="createTime" prop="createTime"
width="180" width="170"
:formatter="dateFormatter" :formatter="dateFormatter"
/> />
<el-table-column label="支付金额" align="center" prop="payPrice" width="100"> <el-table-column label="支付金额" align="center" prop="payPrice" width="100">
...@@ -157,7 +157,7 @@ ...@@ -157,7 +157,7 @@
</p> </p>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="退款状态" align="center" prop="status"> <el-table-column label="退款状态" align="center" prop="status" width="100">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.PAY_REFUND_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.PAY_REFUND_STATUS" :value="scope.row.status" />
</template> </template>
......
...@@ -15,6 +15,8 @@ import { defineComponent } from 'vue' ...@@ -15,6 +15,8 @@ import { defineComponent } from 'vue'
title: 'title' 设置该路由在侧边栏和面包屑中展示的名字 title: 'title' 设置该路由在侧边栏和面包屑中展示的名字
titleSuffix: '2' 当 path 和 title 重复时的后缀或备注
icon: 'svg-name' 设置该路由的图标 icon: 'svg-name' 设置该路由的图标
noCache: true 如果设置为true,则不会被 <keep-alive> 缓存(默认 false) noCache: true 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
...@@ -37,6 +39,7 @@ declare module 'vue-router' { ...@@ -37,6 +39,7 @@ declare module 'vue-router' {
hidden?: boolean hidden?: boolean
alwaysShow?: boolean alwaysShow?: boolean
title?: string title?: string
titleSuffix?: string
icon?: string icon?: string
noCache?: boolean noCache?: boolean
breadcrumb?: boolean breadcrumb?: boolean
......
...@@ -43,7 +43,7 @@ export default ({command, mode}: ConfigEnv): UserConfig => { ...@@ -43,7 +43,7 @@ export default ({command, mode}: ConfigEnv): UserConfig => {
css: { css: {
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
additionalData: '@import "./src/styles/variables.scss";', additionalData: '@use "@/styles/variables.scss" as *;',
javascriptEnabled: true javascriptEnabled: true
} }
} }
......
{
"$schema": "https://json.schemastore.org/web-types",
"framework": "vue",
"name": "name written in package.json",
"version": "version written in package.json",
"contributions": {
"html": {
"types-syntax": "typescript",
"attributes": [
{
"name": "v-hasPermi"
},
{
"name": "v-hasRole"
}
]
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment