diff --git a/.eslintrc.js b/.eslintrc.js index 70c91784..b28255ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -68,6 +68,8 @@ module.exports = defineConfig({ ], 'vue/multi-word-component-names': 'off', 'vue/no-v-html': 'off', - 'prettier/prettier': 'off' // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件 + 'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件 + '@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐 + '@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐 } }) diff --git a/.image/common/bpm-feature.png b/.image/common/bpm-feature.png new file mode 100644 index 00000000..23787fb4 Binary files /dev/null and b/.image/common/bpm-feature.png differ diff --git a/.image/common/infra-feature.png b/.image/common/infra-feature.png new file mode 100644 index 00000000..f5cef50c Binary files /dev/null and b/.image/common/infra-feature.png differ diff --git a/.image/common/system-feature.png b/.image/common/system-feature.png new file mode 100644 index 00000000..366087ce Binary files /dev/null and b/.image/common/system-feature.png differ diff --git a/README.md b/README.md index f78b0cba..7bac2225 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ | 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 | | 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 | + + ### 工作流程 | | 功能 | 描述 | @@ -129,6 +131,8 @@ | 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 | | 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 | + + ### 支付系统 | | 功能 | 描述 | @@ -164,6 +168,8 @@ ps:核心功能已经实现,正在对接微信小程序中... | 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 | | 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 | + + ### 数据报表 | | 功能 | 描述 | diff --git a/package.json b/package.json index bafdf048..58460358 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yudao-ui-admin-vue3", - "version": "2.0.0-snapshot", + "version": "2.0.1-snapshot", "description": "基于vue3、vite4、element-plus、typesScript", "author": "xingyu", "private": false, @@ -30,12 +30,12 @@ "@form-create/element-ui": "^3.1.24", "@iconify/iconify": "^3.1.1", "@videojs-player/vue": "^1.0.0", - "@vueuse/core": "^10.6.1", + "@vueuse/core": "^10.9.0", "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.10", "@zxcvbn-ts/core": "^3.0.4", "animate.css": "^4.1.1", - "axios": "^1.6.1", + "axios": "^1.6.7", "benz-amr-recorder": "^1.1.5", "bpmn-js-token-simulation": "^0.10.0", "camunda-bpmn-moddle": "^7.0.1", @@ -44,9 +44,9 @@ "dayjs": "^1.11.10", "diagram-js": "^12.8.0", "driver.js": "^1.3.1", - "echarts": "^5.4.3", + "echarts": "^5.5.0", "echarts-wordcloud": "^2.1.0", - "element-plus": "2.4.2", + "element-plus": "2.5.3", "fast-xml-parser": "^4.3.2", "highlight.js": "^11.9.0", "jsencrypt": "^3.3.2", @@ -55,77 +55,78 @@ "mitt": "^3.0.1", "nprogress": "^0.2.0", "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.0", "qrcode": "^1.5.3", "qs": "^6.11.2", "steady-xml": "^0.1.0", "url": "^0.11.3", "video.js": "^7.21.5", - "vue": "^3.3.8", + "vue": "3.4.20", "vue-dompurify-html": "^4.1.4", - "vue-i18n": "^9.6.5", - "vue-router": "^4.2.5", + "vue-i18n": "9.9.1", + "vue-router": "^4.3.0", "vue-types": "^5.1.1", "vuedraggable": "^4.1.0", "web-storage-cache": "^1.1.1", "xml-js": "^1.6.11" }, "devDependencies": { - "@commitlint/cli": "^18.4.1", - "@commitlint/config-conventional": "^18.4.0", - "@iconify/json": "^2.2.142", - "@intlify/unplugin-vue-i18n": "^1.5.0", + "@commitlint/cli": "^19.0.1", + "@commitlint/config-conventional": "^19.0.0", + "@iconify/json": "^2.2.187", + "@intlify/unplugin-vue-i18n": "^2.0.0", "@purge-icons/generated": "^0.9.0", - "@types/lodash-es": "^4.17.11", - "@types/node": "^20.9.0", + "@types/lodash-es": "^4.17.12", + "@types/node": "^20.11.21", "@types/nprogress": "^0.2.3", "@types/qrcode": "^1.5.5", - "@types/qs": "^6.9.10", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", - "@unocss/transformer-variant-group": "^0.57.4", + "@types/qs": "^6.9.12", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "@unocss/transformer-variant-group": "^0.58.5", "@unocss/eslint-config": "^0.57.4", - "@vitejs/plugin-legacy": "^4.1.1", - "@vitejs/plugin-vue": "^4.4.1", - "@vitejs/plugin-vue-jsx": "^3.0.2", - "autoprefixer": "^10.4.16", + "@vitejs/plugin-legacy": "^5.3.1", + "@vitejs/plugin-vue": "^5.0.4", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "autoprefixer": "^10.4.17", "bpmn-js": "8.9.0", "bpmn-js-properties-panel": "0.46.0", "consola": "^3.2.3", - "eslint": "^8.53.0", - "eslint-config-prettier": "^9.0.0", - "eslint-define-config": "^1.24.1", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-vue": "^9.18.1", - "lint-staged": "^15.1.0", - "postcss": "^8.4.31", - "postcss-html": "^1.5.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-define-config": "^2.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-vue": "^9.22.0", + "lint-staged": "^15.2.2", + "postcss": "^8.4.35", + "postcss-html": "^1.6.0", "postcss-scss": "^4.0.9", - "prettier": "^3.1.0", + "prettier": "^3.2.5", "prettier-eslint": "^16.3.0", "rimraf": "^5.0.5", - "rollup": "^4.4.1", + "rollup": "^4.12.0", "sass": "^1.69.5", - "stylelint": "^15.11.0", + "stylelint": "^16.2.1", "stylelint-config-html": "^1.1.0", - "stylelint-config-recommended": "^13.0.0", - "stylelint-config-standard": "^34.0.0", - "stylelint-order": "^6.0.3", - "terser": "^5.24.0", - "typescript": "5.2.2", - "unocss": "^0.57.4", + "stylelint-config-recommended": "^14.0.0", + "stylelint-config-standard": "^36.0.0", + "stylelint-order": "^6.0.4", + "terser": "^5.28.1", + "typescript": "5.3.3", + "unocss": "^0.58.5", "unplugin-auto-import": "^0.16.7", "unplugin-element-plus": "^0.8.0", "unplugin-vue-components": "^0.25.2", - "vite": "4.5.0", + "vite": "5.1.4", "vite-plugin-compression": "^0.5.1", - "vite-plugin-ejs": "^1.6.4", + "vite-plugin-ejs": "^1.7.0", "vite-plugin-eslint": "^1.8.1", "vite-plugin-progress": "^0.0.7", - "vite-plugin-purge-icons": "^0.9.2", + "vite-plugin-purge-icons": "^0.10.0", "vite-plugin-svg-icons": "^2.0.1", "vite-plugin-top-level-await": "^1.3.1", "vue-eslint-parser": "^9.3.2", - "vue-tsc": "^1.8.22" + "vue-tsc": "^1.8.27" }, "license": "MIT", "repository": { diff --git a/src/api/bpm/category/index.ts b/src/api/bpm/category/index.ts new file mode 100644 index 00000000..d1e109cb --- /dev/null +++ b/src/api/bpm/category/index.ts @@ -0,0 +1,43 @@ +import request from '@/config/axios' + +// BPM 流程分类 VO +export interface CategoryVO { + id: number // 分类编号 + name: string // 分类名 + code: string // 分类标志 + status: number // 分类状态 + sort: number // 分类排序 +} + +// BPM 流程分类 API +export const CategoryApi = { + // 查询流程分类分页 + getCategoryPage: async (params: any) => { + return await request.get({ url: `/bpm/category/page`, params }) + }, + + // 查询流程分类列表 + getCategorySimpleList: async () => { + return await request.get({ url: `/bpm/category/simple-list` }) + }, + + // 查询流程分类详情 + getCategory: async (id: number) => { + return await request.get({ url: `/bpm/category/get?id=` + id }) + }, + + // 新增流程分类 + createCategory: async (data: CategoryVO) => { + return await request.post({ url: `/bpm/category/create`, data }) + }, + + // 修改流程分类 + updateCategory: async (data: CategoryVO) => { + return await request.put({ url: `/bpm/category/update`, data }) + }, + + // 删除流程分类 + deleteCategory: async (id: number) => { + return await request.delete({ url: `/bpm/category/delete?id=` + id }) + } +} diff --git a/src/api/bpm/definition/index.ts b/src/api/bpm/definition/index.ts index c0e51fab..cb6d4271 100644 --- a/src/api/bpm/definition/index.ts +++ b/src/api/bpm/definition/index.ts @@ -1,8 +1,9 @@ import request from '@/config/axios' -export const getProcessDefinitionBpmnXML = async (id: number) => { +export const getProcessDefinition = async (id: number, key: string) => { return await request.get({ - url: '/bpm/process-definition/get-bpmn-xml?id=' + id + url: '/bpm/process-definition/get', + params: { id, key } }) } diff --git a/src/api/bpm/form/index.ts b/src/api/bpm/form/index.ts index 142ed24c..7fce11fc 100644 --- a/src/api/bpm/form/index.ts +++ b/src/api/bpm/form/index.ts @@ -49,8 +49,8 @@ export const getFormPage = async (params) => { } // 获得动态表单的精简列表 -export const getSimpleFormList = async () => { +export const getFormSimpleList = async () => { return await request.get({ - url: '/bpm/form/list-all-simple' + url: '/bpm/form/simple-list' }) } diff --git a/src/api/bpm/leave/index.ts b/src/api/bpm/leave/index.ts index d4fe8d58..4f374b2f 100644 --- a/src/api/bpm/leave/index.ts +++ b/src/api/bpm/leave/index.ts @@ -2,7 +2,7 @@ import request from '@/config/axios' export type LeaveVO = { id: number - result: number + status: number type: number reason: string processInstanceId: string diff --git a/src/api/bpm/processExpression/index.ts b/src/api/bpm/processExpression/index.ts new file mode 100644 index 00000000..af6a7372 --- /dev/null +++ b/src/api/bpm/processExpression/index.ts @@ -0,0 +1,42 @@ +import request from '@/config/axios' + +// BPM 流程表达式 VO +export interface ProcessExpressionVO { + id: number // 编号 + name: string // 表达式名字 + status: number // 表达式状态 + expression: string // 表达式 +} + +// BPM 流程表达式 API +export const ProcessExpressionApi = { + // 查询BPM 流程表达式分页 + getProcessExpressionPage: async (params: any) => { + return await request.get({ url: `/bpm/process-expression/page`, params }) + }, + + // 查询BPM 流程表达式详情 + getProcessExpression: async (id: number) => { + return await request.get({ url: `/bpm/process-expression/get?id=` + id }) + }, + + // 新增BPM 流程表达式 + createProcessExpression: async (data: ProcessExpressionVO) => { + return await request.post({ url: `/bpm/process-expression/create`, data }) + }, + + // 修改BPM 流程表达式 + updateProcessExpression: async (data: ProcessExpressionVO) => { + return await request.put({ url: `/bpm/process-expression/update`, data }) + }, + + // 删除BPM 流程表达式 + deleteProcessExpression: async (id: number) => { + return await request.delete({ url: `/bpm/process-expression/delete?id=` + id }) + }, + + // 导出BPM 流程表达式 Excel + exportProcessExpression: async (params) => { + return await request.download({ url: `/bpm/process-expression/export-excel`, params }) + } +} \ No newline at end of file diff --git a/src/api/bpm/processInstance/index.ts b/src/api/bpm/processInstance/index.ts index a937eae2..81640625 100644 --- a/src/api/bpm/processInstance/index.ts +++ b/src/api/bpm/processInstance/index.ts @@ -20,51 +20,49 @@ export type ProcessInstanceVO = { endTime: string } -export type ProcessInstanceCCVO = { - type: number, - taskName: string, - taskKey: string, - processInstanceName: string, - processInstanceKey: string, - startUserId: string, - options:string [], +export type ProcessInstanceCopyVO = { + type: number + taskName: string + taskKey: string + processInstanceName: string + processInstanceKey: string + startUserId: string + options: string[] reason: string } -export const getMyProcessInstancePage = async (params) => { +export const getProcessInstanceMyPage = async (params: any) => { return await request.get({ url: '/bpm/process-instance/my-page', params }) } +export const getProcessInstanceManagerPage = async (params: any) => { + return await request.get({ url: '/bpm/process-instance/manager-page', params }) +} + export const createProcessInstance = async (data) => { return await request.post({ url: '/bpm/process-instance/create', data: data }) } -export const cancelProcessInstance = async (id: number, reason: string) => { +export const cancelProcessInstanceByStartUser = async (id: number, reason: string) => { const data = { id: id, reason: reason } - return await request.delete({ url: '/bpm/process-instance/cancel', data: data }) + return await request.delete({ url: '/bpm/process-instance/cancel-by-start-user', data: data }) } -export const getProcessInstance = async (id: number) => { +export const cancelProcessInstanceByAdmin = async (id: number, reason: string) => { + const data = { + id: id, + reason: reason + } + return await request.delete({ url: '/bpm/process-instance/cancel-by-admin', data: data }) +} + +export const getProcessInstance = async (id: string) => { return await request.get({ url: '/bpm/process-instance/get?id=' + id }) } -/** - * 抄送 - * @param data 抄送数据 - * @returns 是否抄送成功 - */ -export const createProcessInstanceCC = async (data) => { - return await request.post({ url: '/bpm/process-instance/cc/create', data: data }) +export const getProcessInstanceCopyPage = async (params: any) => { + return await request.get({ url: '/bpm/process-instance/copy/page', params }) } - -/** - * 抄送列表 - * @param params - * @returns - */ -export const getProcessInstanceCCPage = async (params) => { - return await request.get({ url: '/bpm/process-instance/cc/my-page', params }) -} \ No newline at end of file diff --git a/src/api/bpm/processListener/index.ts b/src/api/bpm/processListener/index.ts new file mode 100644 index 00000000..dabaa476 --- /dev/null +++ b/src/api/bpm/processListener/index.ts @@ -0,0 +1,40 @@ +import request from '@/config/axios' + +// BPM 流程监听器 VO +export interface ProcessListenerVO { + id: number // 编号 + name: string // 监听器名字 + type: string // 监听器类型 + status: number // 监听器状态 + event: string // 监听事件 + valueType: string // 监听器值类型 + value: string // 监听器值 +} + +// BPM 流程监听器 API +export const ProcessListenerApi = { + // 查询流程监听器分页 + getProcessListenerPage: async (params: any) => { + return await request.get({ url: `/bpm/process-listener/page`, params }) + }, + + // 查询流程监听器详情 + getProcessListener: async (id: number) => { + return await request.get({ url: `/bpm/process-listener/get?id=` + id }) + }, + + // 新增流程监听器 + createProcessListener: async (data: ProcessListenerVO) => { + return await request.post({ url: `/bpm/process-listener/create`, data }) + }, + + // 修改流程监听器 + updateProcessListener: async (data: ProcessListenerVO) => { + return await request.put({ url: `/bpm/process-listener/update`, data }) + }, + + // 删除流程监听器 + deleteProcessListener: async (id: number) => { + return await request.delete({ url: `/bpm/process-listener/delete?id=` + id }) + } +} diff --git a/src/api/bpm/task/index.ts b/src/api/bpm/task/index.ts index df6d8160..f3cda9f7 100644 --- a/src/api/bpm/task/index.ts +++ b/src/api/bpm/task/index.ts @@ -4,78 +4,63 @@ export type TaskVO = { id: number } -export const getTodoTaskPage = async (params) => { +export const getTaskTodoPage = async (params: any) => { return await request.get({ url: '/bpm/task/todo-page', params }) } -export const getDoneTaskPage = async (params) => { +export const getTaskDonePage = async (params: any) => { return await request.get({ url: '/bpm/task/done-page', params }) } -export const completeTask = async (data) => { - return await request.put({ url: '/bpm/task/complete', data }) +export const getTaskManagerPage = async (params: any) => { + return await request.get({ url: '/bpm/task/manager-page', params }) } -export const approveTask = async (data) => { +export const approveTask = async (data: any) => { return await request.put({ url: '/bpm/task/approve', data }) } -export const rejectTask = async (data) => { +export const rejectTask = async (data: any) => { return await request.put({ url: '/bpm/task/reject', data }) } -export const backTask = async (data) => { - return await request.put({ url: '/bpm/task/back', data }) -} -export const updateTaskAssignee = async (data) => { - return await request.put({ url: '/bpm/task/update-assignee', data }) -} - -export const getTaskListByProcessInstanceId = async (processInstanceId) => { +export const getTaskListByProcessInstanceId = async (processInstanceId: string) => { return await request.get({ url: '/bpm/task/list-by-process-instance-id?processInstanceId=' + processInstanceId }) } -// 导出任务 -export const exportTask = async (params) => { - return await request.download({ url: '/bpm/task/export', params }) -} - // 获取所有可回退的节点 -export const getReturnList = async (params) => { - return await request.get({ url: '/bpm/task/return-list', params }) +export const getTaskListByReturn = async (id: string) => { + return await request.get({ url: '/bpm/task/list-by-return', params: { id } }) } // 回退 -export const returnTask = async (data) => { +export const returnTask = async (data: any) => { return await request.put({ url: '/bpm/task/return', data }) } -/** - * 委派 - */ -export const delegateTask = async (data) => { +// 委派 +export const delegateTask = async (data: any) => { return await request.put({ url: '/bpm/task/delegate', data }) } -/** - * 加签 - */ -export const taskAddSign = async (data) => { +// 转派 +export const transferTask = async (data: any) => { + return await request.put({ url: '/bpm/task/transfer', data }) +} + +// 加签 +export const signCreateTask = async (data: any) => { return await request.put({ url: '/bpm/task/create-sign', data }) } -/** - * 获取减签任务列表 - */ -export const getChildrenTaskList = async (id: string) => { - return await request.get({ url: '/bpm/task/children-list?taskId=' + id }) -} - -/** - * 减签 - */ -export const taskSubSign = async (data) => { +// 减签 +export const signDeleteTask = async (data: any) => { return await request.delete({ url: '/bpm/task/delete-sign', data }) } + +// 获取减签任务列表 +export const getChildrenTaskList = async (id: string) => { + return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id }) +} diff --git a/src/api/bpm/taskAssignRule/index.ts b/src/api/bpm/taskAssignRule/index.ts deleted file mode 100644 index 5fbe342d..00000000 --- a/src/api/bpm/taskAssignRule/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import request from '@/config/axios' - -export type TaskAssignVO = { - id: number - modelId: string - processDefinitionId: string - taskDefinitionKey: string - taskDefinitionName: string - options: string[] - type: number -} - -export const getTaskAssignRuleList = async (params) => { - return await request.get({ url: '/bpm/task-assign-rule/list', params }) -} - -export const createTaskAssignRule = async (data: TaskAssignVO) => { - return await request.post({ - url: '/bpm/task-assign-rule/create', - data: data - }) -} - -export const updateTaskAssignRule = async (data: TaskAssignVO) => { - return await request.put({ - url: '/bpm/task-assign-rule/update', - data: data - }) -} diff --git a/src/api/bpm/userGroup/index.ts b/src/api/bpm/userGroup/index.ts index 035762bf..7d12755e 100644 --- a/src/api/bpm/userGroup/index.ts +++ b/src/api/bpm/userGroup/index.ts @@ -4,7 +4,7 @@ export type UserGroupVO = { id: number name: string description: string - memberUserIds: number[] + userIds: number[] status: number remark: string createTime: string @@ -42,6 +42,6 @@ export const getUserGroupPage = async (params) => { } // 获取用户组精简信息列表 -export const getSimpleUserGroupList = async (): Promise<UserGroupVO[]> => { - return await request.get({ url: '/bpm/user-group/list-all-simple' }) +export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => { + return await request.get({ url: '/bpm/user-group/simple-list' }) } diff --git a/src/assets/imgs/avatar.jpg b/src/assets/imgs/avatar.jpg new file mode 100644 index 00000000..d46a70a4 Binary files /dev/null and b/src/assets/imgs/avatar.jpg differ diff --git a/src/components/ContentWrap/src/ContentWrap.vue b/src/components/ContentWrap/src/ContentWrap.vue index e3bd5972..454e95c9 100644 --- a/src/components/ContentWrap/src/ContentWrap.vue +++ b/src/components/ContentWrap/src/ContentWrap.vue @@ -25,6 +25,9 @@ defineProps({ </template> <Icon :size="14" class="ml-5px" icon="ep:question-filled" /> </ElTooltip> + <div class="flex flex-grow pl-20px"> + <slot name="header"></slot> + </div> </div> </template> <div> diff --git a/src/components/Editor/src/Editor.vue b/src/components/Editor/src/Editor.vue index ec40bca2..eff82745 100644 --- a/src/components/Editor/src/Editor.vue +++ b/src/components/Editor/src/Editor.vue @@ -180,12 +180,12 @@ defineExpose({ </script> <template> - <div class="z-99 border-1 border-[var(--el-border-color)] border-solid"> + <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-10"> <!-- 工具栏 --> <Toolbar :editor="editorRef" :editorId="editorId" - class="border-0 b-b-1 border-[var(--el-border-color)] border-solid" + class="border-0 b-b-1 border-solid border-[var(--tags-view-border-color)]" /> <!-- 编辑器 --> <Editor diff --git a/src/components/ImageViewer/index.ts b/src/components/ImageViewer/index.ts index 38681356..35764d6b 100644 --- a/src/components/ImageViewer/index.ts +++ b/src/components/ImageViewer/index.ts @@ -12,7 +12,7 @@ export function createImageViewer(options: ImageViewerProps) { initialIndex = 0, infinite = true, hideOnClickModal = false, - appendToBody = false, + teleported = false, zIndex = 2000, show = true } = options @@ -23,7 +23,7 @@ export function createImageViewer(options: ImageViewerProps) { propsData.initialIndex = initialIndex propsData.infinite = infinite propsData.hideOnClickModal = hideOnClickModal - propsData.appendToBody = appendToBody + propsData.teleported = teleported propsData.zIndex = zIndex propsData.show = show diff --git a/src/components/ImageViewer/src/ImageViewer.vue b/src/components/ImageViewer/src/ImageViewer.vue index 5c4921ed..c84d06be 100644 --- a/src/components/ImageViewer/src/ImageViewer.vue +++ b/src/components/ImageViewer/src/ImageViewer.vue @@ -13,7 +13,7 @@ const props = defineProps({ initialIndex: propTypes.number.def(0), infinite: propTypes.bool.def(true), hideOnClickModal: propTypes.bool.def(false), - appendToBody: propTypes.bool.def(false), + teleported: propTypes.bool.def(false), show: propTypes.bool.def(false) }) diff --git a/src/components/ImageViewer/src/types.ts b/src/components/ImageViewer/src/types.ts index 1932d74d..2fff4c0a 100644 --- a/src/components/ImageViewer/src/types.ts +++ b/src/components/ImageViewer/src/types.ts @@ -4,6 +4,6 @@ export interface ImageViewerProps { initialIndex?: number infinite?: boolean hideOnClickModal?: boolean - appendToBody?: boolean + teleported?: boolean show?: boolean } diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue index b88997b1..6bb00b3a 100644 --- a/src/components/Pagination/index.vue +++ b/src/components/Pagination/index.vue @@ -53,7 +53,7 @@ const props = defineProps({ } }) -const emit = defineEmits(['update:page', 'update:limit', 'pagination', 'pagination']) +const emit = defineEmits(['update:page', 'update:limit', 'pagination']) const currentPage = computed({ get() { return props.page diff --git a/src/components/RouterSearch/index.vue b/src/components/RouterSearch/index.vue index e9310b8f..c0352422 100644 --- a/src/components/RouterSearch/index.vue +++ b/src/components/RouterSearch/index.vue @@ -26,7 +26,7 @@ placeholder="请输入菜单内容" :remote-method="remoteMethod" class="overflow-hidden transition-all-600" - :class="showTopSearch ? 'w-220px ml2' : 'w-0'" + :class="showTopSearch ? '!w-220px ml2' : '!w-0'" @change="handleChange" > <el-option diff --git a/src/components/SimpleProcessDesigner/src/addNode.vue b/src/components/SimpleProcessDesigner/src/addNode.vue new file mode 100644 index 00000000..6d09ae8a --- /dev/null +++ b/src/components/SimpleProcessDesigner/src/addNode.vue @@ -0,0 +1,237 @@ +/* stylelint-disable order/properties-order */ +<template> + <div class="add-node-btn-box"> + <div class="add-node-btn"> + <el-popover placement="right-start" v-model="visible" width="auto"> + <div class="add-node-popover-body"> + <a class="add-node-popover-item approver" @click="addType(1)"> + <div class="item-wrapper"> + <span class="iconfont"></span> + </div> + <p>审批人</p> + </a> + <a class="add-node-popover-item notifier" @click="addType(2)"> + <div class="item-wrapper"> + <span class="iconfont"></span> + </div> + <p>抄送人</p> + </a> + <a class="add-node-popover-item condition" @click="addType(4)"> + <div class="item-wrapper"> + <span class="iconfont"></span> + </div> + <p>条件分支</p> + </a> + </div> + <template #reference> + <button class="btn" type="button"> + <span class="iconfont"></span> + </button> + </template> + </el-popover> + </div> + </div> +</template> +<script setup> +import { ref } from 'vue' +let props = defineProps({ + childNodeP: { + type: Object, + default: () => ({}) + } +}) +let emits = defineEmits(['update:childNodeP']) +let visible = ref(false) +const addType = (type) => { + visible.value = false + if (type != 4) { + var data + if (type == 1) { + data = { + nodeName: '审核人', + error: true, + type: 1, + settype: 1, + selectMode: 0, + selectRange: 0, + directorLevel: 1, + examineMode: 1, + noHanderAction: 1, + examineEndDirectorLevel: 0, + childNode: props.childNodeP, + nodeUserList: [] + } + } else if (type == 2) { + data = { + nodeName: '抄送人', + type: 2, + ccSelfSelectFlag: 1, + childNode: props.childNodeP, + nodeUserList: [] + } + } + emits('update:childNodeP', data) + } else { + emits('update:childNodeP', { + nodeName: '路由', + type: 4, + childNode: null, + conditionNodes: [ + { + nodeName: '条件1', + error: true, + type: 3, + priorityLevel: 1, + conditionList: [], + nodeUserList: [], + childNode: props.childNodeP + }, + { + nodeName: '条件2', + type: 3, + priorityLevel: 2, + conditionList: [], + nodeUserList: [], + childNode: null + } + ] + }) + } +} +</script> +<style scoped lang="scss"> +.add-node-btn-box { + width: 240px; + display: inline-flex; + -ms-flex-negative: 0; + flex-shrink: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + position: relative; + + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + margin: auto; + width: 2px; + height: 100%; + background-color: #cacaca; + } + + .add-node-btn { + user-select: none; + width: 240px; + padding: 20px 0 32px; + display: flex; + -webkit-box-pack: center; + justify-content: center; + flex-shrink: 0; + -webkit-box-flex: 1; + flex-grow: 1; + + .btn { + outline: none; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1); + width: 30px; + height: 30px; + background: #3296fa; + border-radius: 50%; + position: relative; + border: none; + line-height: 30px; + -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + + .iconfont { + color: #fff; + font-size: 16px; + } + + &:hover { + transform: scale(1.3); + box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1); + } + + &:active { + transform: none; + background: #1e83e9; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1); + } + } + } +} + +.add-node-popover-body { + display: flex; + + .add-node-popover-item { + margin-right: 10px; + cursor: pointer; + text-align: center; + flex: 1; + color: #191f25 !important; + + .item-wrapper { + user-select: none; + display: inline-block; + width: 80px; + height: 80px; + margin-bottom: 5px; + background: #fff; + border: 1px solid #e2e2e2; + border-radius: 50%; + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + + .iconfont { + font-size: 35px; + line-height: 80px; + } + } + + &.approver { + .item-wrapper { + color: #ff943e; + } + } + + &.notifier { + .item-wrapper { + color: #3296fa; + } + } + + &.condition { + .item-wrapper { + color: #15bc83; + } + } + + &:hover { + .item-wrapper { + background: #3296fa; + box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4); + } + + .iconfont { + color: #fff; + } + } + + &:active { + .item-wrapper { + box-shadow: none; + background: #eaeaea; + } + + .iconfont { + color: inherit; + } + } + } +} +</style> diff --git a/src/components/SimpleProcessDesigner/src/nodeWrap.vue b/src/components/SimpleProcessDesigner/src/nodeWrap.vue new file mode 100644 index 00000000..3c9d5eb1 --- /dev/null +++ b/src/components/SimpleProcessDesigner/src/nodeWrap.vue @@ -0,0 +1,297 @@ +<!-- eslint-disable vue/no-mutating-props --> +<!-- + * @Date: 2022-09-21 14:41:53 + * @LastEditors: StavinLi 495727881@qq.com + * @LastEditTime: 2023-05-24 15:20:24 + * @FilePath: /Workflow-Vue3/src/components/nodeWrap.vue +--> +<template> + <div class="node-wrap" v-if="nodeConfig.type < 3"> + <div class="node-wrap-box" :class="(nodeConfig.type == 0 ? 'start-node ' : '') +(isTried && nodeConfig.error ? 'active error' : '')"> + <div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`"> + <span v-if="nodeConfig.type == 0">{{ nodeConfig.nodeName }}</span> + <template v-else> + <span class="iconfont">{{nodeConfig.type == 1?'':''}}</span> + <input + v-if="isInput" + type="text" + class="ant-input editable-title-input" + @blur="blurEvent()" + @focus="$event.currentTarget.select()" + v-focus + v-model="nodeConfig.nodeName" + :placeholder="defaultText" + /> + <span v-else class="editable-title" @click="clickEvent()">{{ nodeConfig.nodeName }}</span> + <i class="anticon anticon-close close" @click="delNode"></i> + </template> + </div> + <div class="content" @click="setPerson"> + <div class="text"> + <span class="placeholder" v-if="!showText">请选择{{defaultText}}</span> + {{showText}} + </div> + <i class="anticon anticon-right arrow"></i> + </div> + <div class="error_tip" v-if="isTried && nodeConfig.error"> + <i class="anticon anticon-exclamation-circle"></i> + </div> + </div> + <addNode v-model:childNodeP="nodeConfig.childNode" /> + </div> + <div class="branch-wrap" v-if="nodeConfig.type == 4"> + <div class="branch-box-wrap"> + <div class="branch-box"> + <button class="add-branch" @click="addTerm">添加条件</button> + <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index"> + <div class="condition-node"> + <div class="condition-node-box"> + <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''"> + <div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)"><</div> + <div class="title-wrapper"> + <input + v-if="isInputList[index]" + type="text" + class="ant-input editable-title-input" + @blur="blurEvent(index)" + @focus="$event.currentTarget.select()" + v-model="item.nodeName" + /> + <span v-else class="editable-title" @click="clickEvent(index)">{{ item.nodeName }}</span> + <span class="priority-title" @click="setPerson(item.priorityLevel)">优先级{{ item.priorityLevel }}</span> + <i class="anticon anticon-close close" @click="delTerm(index)"></i> + </div> + <div class="sort-right" v-if="index != nodeConfig.conditionNodes.length - 1" @click="arrTransfer(index)">></div> + <div class="content" @click="setPerson(item.priorityLevel)">{{ conditionStr(nodeConfig, index) }}</div> + <div class="error_tip" v-if="isTried && item.error"> + <i class="anticon anticon-exclamation-circle"></i> + </div> + </div> + <addNode v-model:childNodeP="item.childNode" /> + </div> + </div> + <nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" /> + <template v-if="index == 0"> + <div class="top-left-cover-line"></div> + <div class="bottom-left-cover-line"></div> + </template> + <template v-if="index == nodeConfig.conditionNodes.length - 1"> + <div class="top-right-cover-line"></div> + <div class="bottom-right-cover-line"></div> + </template> + </div> + </div> + <addNode v-model:childNodeP="nodeConfig.childNode" /> + </div> + </div> + <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" /> +</template> +<script setup> +import addNode from './addNode.vue' +import { onMounted, ref, watch, getCurrentInstance, computed } from 'vue' +import { + arrToStr, + conditionStr, + setApproverStr, + copyerStr, + bgColors, + placeholderList +} from './util' +import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow' +let _uid = getCurrentInstance().uid + +let props = defineProps({ + nodeConfig: { + type: Object, + default: () => ({}) + }, + flowPermission: { + type: Object, + // eslint-disable-next-line vue/require-valid-default-prop + default: () => [] + } +}) + +let defaultText = computed(() => { + return placeholderList[props.nodeConfig.type] +}) +let showText = computed(() => { + if (props.nodeConfig.type == 0) return arrToStr(props.flowPermission) || '所有人' + if (props.nodeConfig.type == 1) return setApproverStr(props.nodeConfig) + return copyerStr(props.nodeConfig) +}) + +let isInputList = ref([]) +let isInput = ref(false) +const resetConditionNodesErr = () => { + for (var i = 0; i < props.nodeConfig.conditionNodes.length; i++) { + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.conditionNodes[i].error = + conditionStr(props.nodeConfig, i) == '请设置条件' && + i != props.nodeConfig.conditionNodes.length - 1 + } +} +onMounted(() => { + if (props.nodeConfig.type == 1) { + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.error = !setApproverStr(props.nodeConfig) + } else if (props.nodeConfig.type == 2) { + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.error = !copyerStr(props.nodeConfig) + } else if (props.nodeConfig.type == 4) { + resetConditionNodesErr() + } +}) +let emits = defineEmits(['update:flowPermission', 'update:nodeConfig']) +let store = useWorkFlowStoreWithOut() +let { + setPromoter, + setApprover, + setCopyer, + setCondition, + setFlowPermission, + setApproverConfig, + setCopyerConfig, + setConditionsConfig +} = store +let isTried = computed(() => store.isTried) +let flowPermission1 = computed(() => store.flowPermission1) +let approverConfig1 = computed(() => store.approverConfig1) +let copyerConfig1 = computed(() => store.copyerConfig1) +let conditionsConfig1 = computed(() => store.conditionsConfig1) +watch(flowPermission1, (flow) => { + if (flow.flag && flow.id === _uid) { + emits('update:flowPermission', flow.value) + } +}) +watch(approverConfig1, (approver) => { + if (approver.flag && approver.id === _uid) { + emits('update:nodeConfig', approver.value) + } +}) +watch(copyerConfig1, (copyer) => { + if (copyer.flag && copyer.id === _uid) { + emits('update:nodeConfig', copyer.value) + } +}) +watch(conditionsConfig1, (condition) => { + if (condition.flag && condition.id === _uid) { + emits('update:nodeConfig', condition.value) + } +}) + +const clickEvent = (index) => { + if (index || index === 0) { + isInputList.value[index] = true + } else { + isInput.value = true + } +} +const blurEvent = (index) => { + if (index || index === 0) { + isInputList.value[index] = false + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.conditionNodes[index].nodeName = + props.nodeConfig.conditionNodes[index].nodeName || '条件' + } else { + isInput.value = false + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.nodeName = props.nodeConfig.nodeName || defaultText + } +} +const delNode = () => { + emits('update:nodeConfig', props.nodeConfig.childNode) +} +const addTerm = () => { + let len = props.nodeConfig.conditionNodes.length + 1 + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.conditionNodes.push({ + nodeName: '条件' + len, + type: 3, + priorityLevel: len, + conditionList: [], + nodeUserList: [], + childNode: null + }) + resetConditionNodesErr() + emits('update:nodeConfig', props.nodeConfig) +} +const delTerm = (index) => { + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.conditionNodes.splice(index, 1) + props.nodeConfig.conditionNodes.map((item, index) => { + item.priorityLevel = index + 1 + item.nodeName = `条件${index + 1}` + }) + resetConditionNodesErr() + emits('update:nodeConfig', props.nodeConfig) + if (props.nodeConfig.conditionNodes.length == 1) { + if (props.nodeConfig.childNode) { + if (props.nodeConfig.conditionNodes[0].childNode) { + reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode) + } else { + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode + } + } + emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode) + } +} +const reData = (data, addData) => { + if (!data.childNode) { + data.childNode = addData + } else { + reData(data.childNode, addData) + } +} +const setPerson = (priorityLevel) => { + var { type } = props.nodeConfig + if (type == 0) { + setPromoter(true) + setFlowPermission({ + value: props.flowPermission, + flag: false, + id: _uid + }) + } else if (type == 1) { + setApprover(true) + setApproverConfig({ + value: { + ...JSON.parse(JSON.stringify(props.nodeConfig)), + ...{ settype: props.nodeConfig.settype ? props.nodeConfig.settype : 1 } + }, + flag: false, + id: _uid + }) + } else if (type == 2) { + setCopyer(true) + setCopyerConfig({ + value: JSON.parse(JSON.stringify(props.nodeConfig)), + flag: false, + id: _uid + }) + } else { + setCondition(true) + setConditionsConfig({ + value: JSON.parse(JSON.stringify(props.nodeConfig)), + priorityLevel, + flag: false, + id: _uid + }) + } +} +const arrTransfer = (index, type = 1) => { + //向左-1,向右1 + // eslint-disable-next-line vue/no-mutating-props + props.nodeConfig.conditionNodes[index] = props.nodeConfig.conditionNodes.splice( + index + type, + 1, + props.nodeConfig.conditionNodes[index] + )[0] + props.nodeConfig.conditionNodes.map((item, index) => { + item.priorityLevel = index + 1 + }) + resetConditionNodesErr() + emits('update:nodeConfig', props.nodeConfig) +} +</script> diff --git a/src/components/SimpleProcessDesigner/src/util.ts b/src/components/SimpleProcessDesigner/src/util.ts new file mode 100644 index 00000000..f4acd76c --- /dev/null +++ b/src/components/SimpleProcessDesigner/src/util.ts @@ -0,0 +1,165 @@ +/** + * todo + */ +export const arrToStr = (arr?: [{ name: string }]) => { + if (arr) { + return arr + .map((item) => { + return item.name + }) + .toString() + } +} + +export const setApproverStr = (nodeConfig: any) => { + if (nodeConfig.settype == 1) { + if (nodeConfig.nodeUserList.length == 1) { + return nodeConfig.nodeUserList[0].name + } else if (nodeConfig.nodeUserList.length > 1) { + if (nodeConfig.examineMode == 1) { + return arrToStr(nodeConfig.nodeUserList) + } else if (nodeConfig.examineMode == 2) { + return nodeConfig.nodeUserList.length + '人会签' + } + } + } else if (nodeConfig.settype == 2) { + const level = + nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管' + if (nodeConfig.examineMode == 1) { + return level + } else if (nodeConfig.examineMode == 2) { + return level + '会签' + } + } else if (nodeConfig.settype == 4) { + if (nodeConfig.selectRange == 1) { + return '发起人自选' + } else { + if (nodeConfig.nodeUserList.length > 0) { + if (nodeConfig.selectRange == 2) { + return '发起人自选' + } else { + return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选' + } + } else { + return '' + } + } + } else if (nodeConfig.settype == 5) { + return '发起人自己' + } else if (nodeConfig.settype == 7) { + return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管' + } +} + +export const copyerStr = (nodeConfig: any) => { + if (nodeConfig.nodeUserList.length != 0) { + return arrToStr(nodeConfig.nodeUserList) + } else { + if (nodeConfig.ccSelfSelectFlag == 1) { + return '发起人自选' + } + } +} +export const conditionStr = (nodeConfig, index) => { + const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index] + if (conditionList.length == 0) { + return index == nodeConfig.conditionNodes.length - 1 && + nodeConfig.conditionNodes[0].conditionList.length != 0 + ? '其他条件进入此流程' + : '请设置条件' + } else { + let str = '' + for (let i = 0; i < conditionList.length; i++) { + const { + columnId, + columnType, + showType, + showName, + optType, + zdy1, + opt1, + zdy2, + opt2, + fixedDownBoxValue + } = conditionList[i] + if (columnId == 0) { + if (nodeUserList.length != 0) { + str += '发起人属于:' + str += + nodeUserList + .map((item) => { + return item.name + }) + .join('或') + ' 并且 ' + } + } + if (columnType == 'String' && showType == '3') { + if (zdy1) { + str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 ' + } + } + if (columnType == 'Double') { + if (optType != 6 && zdy1) { + const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType] + str += `${showName} ${optTypeStr} ${zdy1} 并且 ` + } else if (optType == 6 && zdy1 && zdy2) { + str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 ` + } + } + } + return str ? str.substring(0, str.length - 4) : '请设置条件' + } +} + +export const dealStr = (str: string, obj) => { + const arr = [] + const list = str.split(',') + for (const elem in obj) { + list.map((item) => { + if (item == elem) { + arr.push(obj[elem].value) + } + }) + } + return arr.join('或') +} + +export const removeEle = (arr, elem, key = 'id') => { + let includesIndex + arr.map((item, index) => { + if (item[key] == elem[key]) { + includesIndex = index + } + }) + arr.splice(includesIndex, 1) +} + +export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250'] +export const placeholderList = ['发起人', '审核人', '抄送人'] +export const setTypes = [ + { value: 1, label: '指定成员' }, + { value: 2, label: '主管' }, + { value: 4, label: '发起人自选' }, + { value: 5, label: '发起人自己' }, + { value: 7, label: '连续多级主管' } +] + +export const selectModes = [ + { value: 1, label: '选一个人' }, + { value: 2, label: '选多个人' } +] + +export const selectRanges = [ + { value: 1, label: '全公司' }, + { value: 2, label: '指定成员' }, + { value: 3, label: '指定角色' } +] + +export const optTypes = [ + { value: '1', label: '小于' }, + { value: '2', label: '大于' }, + { value: '3', label: '小于等于' }, + { value: '4', label: '等于' }, + { value: '5', label: '大于等于' }, + { value: '6', label: '介于两个数之间' } +] diff --git a/src/components/SimpleProcessDesigner/theme/workflow.css b/src/components/SimpleProcessDesigner/theme/workflow.css new file mode 100644 index 00000000..888b1a82 --- /dev/null +++ b/src/components/SimpleProcessDesigner/theme/workflow.css @@ -0,0 +1,1292 @@ + +.clearfix { + zoom: 1 +} + +.clearfix:after, +.clearfix:before { + content: ""; + display: table +} + +.clearfix:after { + clear: both +} + +@font-face { + font-family: anticon; + font-display: fallback; + src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.eot"); + src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.woff") format("woff"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.ttf") format("truetype"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.svg#iconfont") format("svg") +} + +.anticon { + display: inline-block; + font-style: normal; + vertical-align: baseline; + text-align: center; + text-transform: none; + line-height: 1; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.anticon:before { + display: block; + font-family: anticon!important +} +.anticon-close:before { + content: "\E633" +} +.anticon-right:before { + content: "\E61F" +} +.anticon-exclamation-circle{ + color: rgb(242, 86, 67) +} +.anticon-exclamation-circle:before { + content: "\E62C" +} + +.anticon-left:before { + content: "\E620" +} + +.anticon-close-circle:before { + content: "\E62E" +} + +.ant-btn { + line-height: 1.5; + display: inline-block; + font-weight: 400; + text-align: center; + touch-action: manipulation; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 0 15px; + font-size: 14px; + border-radius: 4px; + height: 32px; + user-select: none; + transition: all .3s cubic-bezier(.645, .045, .355, 1); + position: relative; + color: rgba(0, 0, 0, .65); + background-color: #fff; + border-color: #d9d9d9 +} + +.ant-btn>.anticon { + line-height: 1 +} + +.ant-btn, +.ant-btn:active, +.ant-btn:focus { + outline: 0 +} + +.ant-btn>a:only-child { + color: currentColor +} + +.ant-btn>a:only-child:after { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: transparent +} + +.ant-btn:focus, +.ant-btn:hover { + color: #40a9ff; + background-color: #fff; + border-color: #40a9ff +} + +.ant-btn:focus>a:only-child, +.ant-btn:hover>a:only-child { + color: currentColor +} + +.ant-btn:focus>a:only-child:after, +.ant-btn:hover>a:only-child:after { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: transparent +} + +.ant-btn.active, +.ant-btn:active { + color: #096dd9; + background-color: #fff; + border-color: #096dd9 +} + +.ant-btn.active>a:only-child, +.ant-btn:active>a:only-child { + color: currentColor +} + +.ant-btn.active>a:only-child:after, +.ant-btn:active>a:only-child:after { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: transparent +} + +.ant-btn.active, +.ant-btn:active, +.ant-btn:focus, +.ant-btn:hover { + background: #fff; + text-decoration: none +} + +.ant-btn>i, +.ant-btn>span { + pointer-events: none +} + +.ant-btn:before { + position: absolute; + top: -1px; + left: -1px; + bottom: -1px; + right: -1px; + background: #fff; + opacity: .35; + content: ""; + border-radius: inherit; + z-index: 1; + transition: opacity .2s; + pointer-events: none; + display: none +} + +.ant-btn .anticon { + transition: margin-left .3s cubic-bezier(.645, .045, .355, 1) +} + +.ant-btn:active>span, +.ant-btn:focus>span { + position: relative +} + +.ant-btn>.anticon+span, +.ant-btn>span+.anticon { + margin-left: 8px +} + +.ant-input { + font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif; + font-variant: tabular-nums; + box-sizing: border-box; + margin: 0; + padding: 0; + list-style: none; + position: relative; + display: inline-block; + padding: 4px 11px; + width: 100%; + height: 32px; + font-size: 14px; + line-height: 1.5; + color: rgba(0, 0, 0, .65); + background-color: #fff; + background-image: none; + border: 1px solid #d9d9d9; + border-radius: 4px; + transition: all .3s +} + +.ant-input::-moz-placeholder { + color: #bfbfbf; + opacity: 1 +} + +.ant-input:-ms-input-placeholder { + color: #bfbfbf +} + +.ant-input::-webkit-input-placeholder { + color: #bfbfbf +} + +.ant-input:focus, +.ant-input:hover { + border-color: #40a9ff; + border-right-width: 1px!important +} + +.ant-input:focus { + outline: 0; + box-shadow: 0 0 0 2px rgba(24, 144, 255, .2) +} + +textarea.ant-input { + max-width: 100%; + height: auto; + vertical-align: bottom; + transition: all .3s, height 0s; + min-height: 32px +} + +a, +abbr, +acronym, +address, +applet, +article, +aside, +audio, +b, +big, +blockquote, +body, +canvas, +caption, +center, +cite, +code, +dd, +del, +details, +dfn, +div, +dl, +dt, +em, +fieldset, +figcaption, +figure, +footer, +form, +h1, +h2, +h3, +h4, +h5, +h6, +header, +hgroup, +html, +i, +iframe, +img, +ins, +kbd, +label, +legend, +li, +mark, +menu, +nav, +object, +ol, +p, +pre, +q, +s, +samp, +section, +small, +span, +strike, +strong, +sub, +summary, +sup, +table, +tbody, +td, +tfoot, +th, +thead, +time, +tr, +tt, +u, +ul, +var, +video { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline +} + +*, +:after, +:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} + +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100% +} + +body, +html { + font-size: 14px +} + +body { + font-family: Microsoft Yahei, Lucida Grande, Lucida Sans Unicode, Helvetica, Arial, Verdana, sans-serif; + line-height: 1.6; + background-color: #fff; + position: static!important; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0) +} + +ol, +ul { + list-style-type: none +} + +b, +strong { + font-weight: 700 +} + +img { + border: 0 +} + +button, +input, +select, +textarea { + font-family: inherit; + font-size: 100%; + margin: 0 +} + +textarea { + overflow: auto; + vertical-align: top; + -webkit-appearance: none +} + +button, +input { + line-height: normal +} + +button, +select { + text-transform: none +} + +button, +html input[type=button], +input[type=reset], +input[type=submit] { + -webkit-appearance: button; + cursor: pointer +} + +input[type=search] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box +} + +input[type=search]::-webkit-search-cancel-button, +input[type=search]::-webkit-search-decoration { + -webkit-appearance: none +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0 +} + +table { + width: 100%; + border-spacing: 0; + border-collapse: collapse +} + +table, +td, +th { + border: 0 +} + +td, +th { + padding: 0; + vertical-align: top +} + +th { + font-weight: 700; + text-align: left +} + +thead th { + white-space: nowrap +} + +a { + text-decoration: none; + cursor: pointer; + color: #3296fa +} + +a:active, +a:hover { + outline: 0; + color: #3296fa +} + +small { + font-size: 80% +} + +body, +html { + font-size: 12px!important; + color: #191f25!important; + background: #f6f6f6!important +} + +.wrap { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + height: 100% +} + +@font-face { + font-family: IconFont; + src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot"); + src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg") +} + +.iconfont { + font-family: IconFont!important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -webkit-text-stroke-width: .2px; + -moz-osx-font-smoothing: grayscale +} + +.fd-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 997; + width: 100%; + height: 60px; + font-size: 14px; + color: #fff; + background: #3296fa; + display: flex; + align-items: center +} + +.fd-nav>* { + flex: 1; + width: 100% +} + +.fd-nav .fd-nav-left { + display: -webkit-box; + display: flex; + align-items: center +} + +.fd-nav .fd-nav-center { + flex: none; + width: 600px; + text-align: center +} + +.fd-nav .fd-nav-right { + display: flex; + align-items: center; + justify-content: flex-end; + text-align: right +} + +.fd-nav .fd-nav-back { + display: inline-block; + width: 60px; + height: 60px; + font-size: 22px; + border-right: 1px solid #1583f2; + text-align: center; + cursor: pointer +} + +.fd-nav .fd-nav-back:hover { + background: #5af +} + +.fd-nav .fd-nav-back:active { + background: #1583f2 +} + +.fd-nav .fd-nav-back .anticon { + line-height: 60px +} + +.fd-nav .fd-nav-title { + width: 0; + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 0 15px +} + +.fd-nav a { + color: #fff; + margin-left: 12px +} + +.fd-nav .button-publish { + min-width: 80px; + margin-left: 4px; + margin-right: 15px; + color: #3296fa; + border-color: #fff +} + +.fd-nav .button-publish.ant-btn:focus, +.fd-nav .button-publish.ant-btn:hover { + color: #3296fa; + border-color: #fff; + box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .3) +} + +.fd-nav .button-publish.ant-btn:active { + color: #3296fa; + background: #d6eaff; + box-shadow: none +} + +.fd-nav .button-preview { + min-width: 80px; + margin-left: 16px; + margin-right: 4px; + color: #fff; + border-color: #fff; + background: transparent +} + +.fd-nav .button-preview.ant-btn:focus, +.fd-nav .button-preview.ant-btn:hover { + color: #fff; + border-color: #fff; + background: #59acfc +} + +.fd-nav .button-preview.ant-btn:active { + color: #fff; + border-color: #fff; + background: #2186ef +} + +.fd-nav-content { + position: fixed; + top: 60px; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + overflow-x: hidden; + overflow-y: auto; + padding-bottom: 30px +} + +.error-modal-desc { + font-size: 13px; + color: rgba(25, 31, 37, .56); + line-height: 22px; + margin-bottom: 14px +} + +.error-modal-list { + height: 200px; + overflow-y: auto; + margin-right: -25px; + padding-right: 25px +} + +.error-modal-item { + padding: 10px 20px; + line-height: 21px; + background: #f6f6f6; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + border-radius: 4px +} + +.error-modal-item-label { + flex: none; + font-size: 15px; + color: rgba(25, 31, 37, .56); + padding-right: 10px +} + +.error-modal-item-content { + text-align: right; + flex: 1; + font-size: 13px; + color: #191f25 +} + +#body.blur { + -webkit-filter: blur(3px); + filter: blur(3px) +} + +.zoom { + display: flex; + position: fixed; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + height: 40px; + width: 125px; + right: 40px; + margin-top: 30px; + z-index: 10 +} + +.zoom .zoom-in, +.zoom .zoom-out { + width: 30px; + height: 30px; + background: #fff; + color: #c1c1cd; + cursor: pointer; + background-size: 100%; + background-repeat: no-repeat +} + +.zoom .zoom-out { + background-image: url(https://gw.alicdn.com/tfs/TB1s0qhBHGYBuNjy0FoXXciBFXa-90-90.png) +} + +.zoom .zoom-out.disabled { + opacity: .5 +} + +.zoom .zoom-in { + background-image: url(https://gw.alicdn.com/tfs/TB1UIgJBTtYBeNjy1XdXXXXyVXa-90-90.png) +} + +.zoom .zoom-in.disabled { + opacity: .5 +} + +.auto-judge:hover .editable-title, +.node-wrap-box:hover .editable-title { + border-bottom: 1px dashed #fff +} + +.auto-judge:hover .editable-title.editing, +.node-wrap-box:hover .editable-title.editing { + text-decoration: none; + border: 1px solid #d9d9d9 +} + +.auto-judge:hover .editable-title { + border-color: #15bc83 +} + +.editable-title { + line-height: 15px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + border-bottom: 1px dashed transparent +} + +.editable-title:before { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 40px +} + +.editable-title:hover { + border-bottom: 1px dashed #fff +} + +.editable-title-input { + flex: none; + height: 18px; + padding-left: 4px; + text-indent: 0; + font-size: 12px; + line-height: 18px; + z-index: 1 +} + +.editable-title-input:hover { + text-decoration: none +} + +.ant-btn { + position: relative +} + +.node-wrap-box { + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + position: relative; + width: 220px; + min-height: 72px; + -ms-flex-negative: 0; + flex-shrink: 0; + background: #fff; + border-radius: 4px; + cursor: pointer +} + +.node-wrap-box:after { + pointer-events: none; + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 2; + border-radius: 4px; + border: 1px solid transparent; + transition: all .1s cubic-bezier(.645, .045, .355, 1); + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1) +} + +.node-wrap-box.active:after, +.node-wrap-box:active:after, +.node-wrap-box:hover:after { + border: 1px solid #3296fa; + box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3) +} + +.node-wrap-box.active .close, +.node-wrap-box:active .close, +.node-wrap-box:hover .close { + display: block +} + +.node-wrap-box.error:after { + border: 1px solid #f25643; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1) +} + +.node-wrap-box .title { + position: relative; + display: flex; + align-items: center; + padding-left: 16px; + padding-right: 30px; + width: 100%; + height: 24px; + line-height: 24px; + font-size: 12px; + color: #fff; + text-align: left; + background: #576a95; + border-radius: 4px 4px 0 0 +} + +.node-wrap-box .title .iconfont { + font-size: 12px; + margin-right: 5px +} + +.node-wrap-box .placeholder { + color: #bfbfbf +} + +.node-wrap-box .close { + display: none; + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + font-size: 14px; + color: #fff; + border-radius: 50%; + text-align: center; + line-height: 20px +} + +.node-wrap-box .content { + position: relative; + font-size: 14px; + padding: 16px; + padding-right: 30px +} + +.node-wrap-box .content .text { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical +} + +.node-wrap-box .content .arrow { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 14px; + font-size: 14px; + color: #979797 +} + +.start-node.node-wrap-box .content .text { + display: block; + white-space: nowrap +} + +.node-wrap-box:before { + content: ""; + position: absolute; + top: -12px; + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); + width: 0; + height: 4px; + border-style: solid; + border-width: 8px 6px 4px; + border-color: #cacaca transparent transparent; + background: #f5f5f7 +} + +.node-wrap-box.start-node:before { + content: none +} + +.top-left-cover-line { + left: -1px +} + +.top-left-cover-line, +.top-right-cover-line { + position: absolute; + height: 8px; + width: 50%; + background-color: #f5f5f7; + top: -4px +} + +.top-right-cover-line { + right: -1px +} + +.bottom-left-cover-line { + left: -1px +} + +.bottom-left-cover-line, +.bottom-right-cover-line { + position: absolute; + height: 8px; + width: 50%; + background-color: #f5f5f7; + bottom: -4px +} + +.bottom-right-cover-line { + right: -1px +} + +.dingflow-design { + width: 100%; + background-color: #f5f5f7; + overflow: auto; + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: 0 +} + +.dingflow-design .box-scale { + transform: scale(1); + display: inline-block; + position: relative; + width: 100%; + padding: 54.5px 0; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + min-width: -webkit-min-content; + min-width: -moz-min-content; + min-width: min-content; + background-color: #f5f5f7; + transform-origin: 50% 0px 0px; +} + +.dingflow-design .node-wrap { + flex-direction: column; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + padding: 0 50px; + position: relative +} + +.dingflow-design .branch-wrap, +.dingflow-design .node-wrap { + display: inline-flex; + width: 100% +} + +.dingflow-design .branch-box-wrap { + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + min-height: 270px; + width: 100%; + -ms-flex-negative: 0; + flex-shrink: 0 +} + +.dingflow-design .branch-box { + display: flex; + overflow: visible; + min-height: 180px; + height: auto; + border-bottom: 2px solid #ccc; + border-top: 2px solid #ccc; + position: relative; + margin-top: 15px +} + +.dingflow-design .branch-box .col-box { + background: #f5f5f7 +} + +.dingflow-design .branch-box .col-box:before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 0; + margin: auto; + width: 2px; + height: 100%; + background-color: #cacaca +} + +.dingflow-design .add-branch { + border: none; + outline: none; + user-select: none; + justify-content: center; + font-size: 12px; + padding: 0 10px; + height: 30px; + line-height: 30px; + border-radius: 15px; + color: #3296fa; + background: #fff; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1); + position: absolute; + top: -16px; + left: 50%; + transform: translateX(-50%); + transform-origin: center center; + cursor: pointer; + z-index: 1; + display: inline-flex; + align-items: center; + -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1); + transition: all .3s cubic-bezier(.645, .045, .355, 1) +} + +.dingflow-design .add-branch:hover { + transform: translateX(-50%) scale(1.1); + box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1) +} + +.dingflow-design .add-branch:active { + transform: translateX(-50%); + box-shadow: none +} + +.dingflow-design .col-box { + display: inline-flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + flex-direction: column; + -webkit-box-align: center; + align-items: center; + position: relative +} + +.dingflow-design .condition-node { + min-height: 220px +} + +.dingflow-design .condition-node, +.dingflow-design .condition-node-box { + display: inline-flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + flex-direction: column; + -webkit-box-flex: 1 +} + +.dingflow-design .condition-node-box { + padding-top: 30px; + padding-right: 50px; + padding-left: 50px; + -webkit-box-pack: center; + justify-content: center; + -webkit-box-align: center; + align-items: center; + flex-grow: 1; + position: relative +} + +.dingflow-design .condition-node-box:before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: 2px; + height: 100%; + background-color: #cacaca +} + +.dingflow-design .auto-judge { + position: relative; + width: 220px; + min-height: 72px; + background: #fff; + border-radius: 4px; + padding: 14px 19px; + cursor: pointer +} + +.dingflow-design .auto-judge:after { + pointer-events: none; + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 2; + border-radius: 4px; + border: 1px solid transparent; + transition: all .1s cubic-bezier(.645, .045, .355, 1); + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1) +} + +.dingflow-design .auto-judge.active:after, +.dingflow-design .auto-judge:active:after, +.dingflow-design .auto-judge:hover:after { + border: 1px solid #3296fa; + box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3) +} + +.dingflow-design .auto-judge.active .close, +.dingflow-design .auto-judge:active .close, +.dingflow-design .auto-judge:hover .close { + display: block +} + +.dingflow-design .auto-judge.error:after { + border: 1px solid #f25643; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1) +} + +.dingflow-design .auto-judge .title-wrapper { + position: relative; + font-size: 12px; + color: #15bc83; + text-align: left; + line-height: 16px +} + +.dingflow-design .auto-judge .title-wrapper .editable-title { + display: inline-block; + max-width: 120px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis +} + +.dingflow-design .auto-judge .title-wrapper .priority-title { + display: inline-block; + float: right; + margin-right: 10px; + color: rgba(25, 31, 37, .56) +} + +.dingflow-design .auto-judge .placeholder { + color: #bfbfbf +} + +.dingflow-design .auto-judge .close { + display: none; + position: absolute; + right: -10px; + top: -10px; + width: 20px; + height: 20px; + font-size: 14px; + color: rgba(0, 0, 0, .25); + border-radius: 50%; + text-align: center; + line-height: 20px; + z-index: 2 +} + +.dingflow-design .auto-judge .content { + font-size: 14px; + color: #191f25; + text-align: left; + margin-top: 6px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical +} + +.dingflow-design .auto-judge .sort-left, +.dingflow-design .auto-judge .sort-right { + position: absolute; + top: 0; + bottom: 0; + display: none; + z-index: 1 +} + +.dingflow-design .auto-judge .sort-left { + left: 0; + border-right: 1px solid #f6f6f6 +} + +.dingflow-design .auto-judge .sort-right { + right: 0; + border-left: 1px solid #f6f6f6 +} + +.dingflow-design .auto-judge:hover .sort-left, +.dingflow-design .auto-judge:hover .sort-right { + display: flex; + align-items: center +} + +.dingflow-design .auto-judge .sort-left:hover, +.dingflow-design .auto-judge .sort-right:hover { + background: #efefef +} + +.dingflow-design .end-node { + border-radius: 50%; + font-size: 14px; + color: rgba(25, 31, 37, .4); + text-align: left +} + +.dingflow-design .end-node .end-node-circle { + width: 10px; + height: 10px; + margin: auto; + border-radius: 50%; + background: #dbdcdc +} + +.dingflow-design .end-node .end-node-text { + margin-top: 5px; + text-align: center +} + +.approval-setting { + border-radius: 2px; + margin: 20px 0; + position: relative; + background: #fff +} + +.ant-btn { + position: relative +} + + diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue index 3fe21944..6cbe11fa 100644 --- a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue +++ b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue @@ -436,7 +436,7 @@ const initBpmnModeler = () => { // bpmnModeler.createDiagram() - console.log(bpmnModeler, 'bpmnModeler111111') + // console.log(bpmnModeler, 'bpmnModeler111111') emit('init-finished', bpmnModeler) initModelListeners() } @@ -666,10 +666,10 @@ const previewProcessJson = () => { } /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */ const processSave = async () => { - console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler') + // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler') const { err, xml } = await bpmnModeler.saveXML() - console.log(err, 'errerrerrerrerr') - console.log(xml, 'xmlxmlxmlxmlxml') + // console.log(err, 'errerrerrerrerr') + // console.log(xml, 'xmlxmlxmlxmlxml') // 读取异常时抛出异常 if (err) { // this.$modal.msgError('保存模型失败,请重试!') diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue index a7958adb..485b9795 100644 --- a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue +++ b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue @@ -115,19 +115,19 @@ const highlightDiagram = async () => { if (!task) { return } - //进行中的任务已经高亮过了,则不高亮后面的任务了 + // 进行中的任务已经高亮过了,则不高亮后面的任务了 if (findProcessTask) { removeTaskDefinitionKeyList.push(n.id) return } // 高亮任务 - canvas.addMarker(n.id, getResultCss(task.result)) + canvas.addMarker(n.id, getResultCss(task.status)) //标记是否高亮了进行中任务 - if (task.result === 1) { + if (task.status === 1) { findProcessTask = true } // 如果非通过,就不走后面的线条了 - if (task.result !== 2) { + if (task.status !== 2) { return } // 处理 outgoing 出线 @@ -194,6 +194,7 @@ const highlightDiagram = async () => { }) } else if (n.$type === 'bpmn:StartEvent') { // 开始节点 + canvas.addMarker(n.id, 'highlight') n.outgoing?.forEach((nn) => { // outgoing 例如说【bpmn:SequenceFlow】连线 // 获得连线是否有指向目标。如果有,则进行高亮 @@ -205,10 +206,10 @@ const highlightDiagram = async () => { }) } else if (n.$type === 'bpmn:EndEvent') { // 结束节点 - if (!processInstance.value || processInstance.value.result === 1) { + if (!processInstance.value || processInstance.value.status === 1) { return } - canvas.addMarker(n.id, getResultCss(processInstance.value.result)) + canvas.addMarker(n.id, getResultCss(processInstance.value.status)) } else if (n.$type === 'bpmn:ServiceTask') { //服务任务 if (activity.startTime > 0 && activity.endTime === 0) { @@ -223,39 +224,49 @@ const highlightDiagram = async () => { canvas.addMarker(out.id, getResultCss(2)) }) } + } else if (n.$type === 'bpmn:SequenceFlow') { + let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id) + if (targetActivity) { + canvas.addMarker(n.id, getActivityHighlightCss(targetActivity)) + } } }) if (!isEmpty(removeTaskDefinitionKeyList)) { taskList.value = taskList.value.filter( - (item) => !removeTaskDefinitionKeyList.includes(item.definitionKey) + (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey) ) } } + const getActivityHighlightCss = (activity) => { return activity.endTime ? 'highlight' : 'highlight-todo' } -const getResultCss = (result) => { - if (result === 1) { + +const getResultCss = (status) => { + if (status === 1) { // 审批中 return 'highlight-todo' - } else if (result === 2) { + } else if (status === 2) { // 已通过 return 'highlight' - } else if (result === 3) { + } else if (status === 3) { // 不通过 return 'highlight-reject' - } else if (result === 4) { + } else if (status === 4) { // 已取消 return 'highlight-cancel' - } else if (result === 5) { + } else if (status === 5) { // 退回 return 'highlight-return' - } else if (result === 6) { + } else if (status === 6) { // 委派 - return 'highlight-return' - } else if (result === 7 || result === 8 || result === 9) { - // 待后加签任务完成/待前加签任务完成/待前置任务完成 - return 'highlight-return' + return 'highlight-todo' + } else if (status === 7) { + // 审批通过中 + return 'highlight-todo' + } else if (status === 0) { + // 待审批 + return 'highlight-todo' } return '' } @@ -296,10 +307,10 @@ const elementHover = (element) => { !elementOverlayIds.value && (elementOverlayIds.value = {}) !overlays.value && (overlays.value = bpmnModeler.get('overlays')) // 展示信息 - console.log(activityLists.value, 'activityLists.value') - console.log(element.value, 'element.value') + // 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') + // console.log(activity, 'activityactivityactivityactivity') if (!activity) { return } @@ -313,15 +324,14 @@ const elementHover = (element) => { <p>部门:${processInstance.value.startUser.deptName}</p> <p>创建时间:${formatDate(processInstance.value.createTime)}` } else if (element.value.type === 'bpmn:UserTask') { - // debugger let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId if (!task) { return } - let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT) + let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS) let dataResult = '' optionData.forEach((element) => { - if (element.value == task.result) { + if (element.value == task.status) { dataResult = element.label } }) @@ -333,7 +343,7 @@ const elementHover = (element) => { // <p>部门:${task.assigneeUser.deptName}</p> // <p>结果:${getIntDictOptions( // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, - // task.result + // task.status // )}</p> // <p>创建时间:${formatDate(task.createTime)}</p>` if (task.endTime) { @@ -351,29 +361,30 @@ const elementHover = (element) => { } console.log(html) } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) { - let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT) + let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS) let dataResult = '' optionData.forEach((element) => { - if (element.value == processInstance.value.result) { + if (element.value == processInstance.value.status) { dataResult = element.label } }) html = `<p>结果:${dataResult}</p>` // html = `<p>结果:${getIntDictOptions( // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, - // processInstance.value.result + // processInstance.value.status // )}</p>` if (processInstance.value.endTime) { html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>` } } - console.log(html, 'html111111111111111') + // 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 const elementOut = (element) => { toRaw(overlays.value).remove({ element }) @@ -389,6 +400,7 @@ onMounted(() => { // 初始模型的监听器 initModelListeners() }) + onBeforeUnmount(() => { // this.$once('hook:beforeDestroy', () => { // }) @@ -427,7 +439,7 @@ watch( ) </script> -<style> +<style lang="scss"> /** 处理中 */ .highlight-todo.djs-connection > .djs-visual > path { stroke: #1890ff !important; @@ -501,6 +513,10 @@ watch( stroke: green !important; } +.djs-element.highlight > .djs-visual > path { + stroke: green !important; +} + /** 不通过 */ .highlight-reject.djs-shape .djs-visual > :nth-child(1) { fill: red !important; @@ -520,6 +536,7 @@ watch( .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) { diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json index db5e4901..94ba8f6c 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json @@ -332,6 +332,16 @@ "name": "multiinstance_condition", "isAttr": true, "type": "String" + }, + { + "name": "candidateStrategy", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateParam", + "isAttr": true, + "type": "String" } ] }, diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json index 79b86bca..8322561e 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json @@ -319,6 +319,16 @@ "name": "priority", "isAttr": true, "type": "String" + }, + { + "name": "candidateStrategy", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateParam", + "isAttr": true, + "type": "String" } ] }, diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json index 7fe7ad14..4ea632a0 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json @@ -319,6 +319,16 @@ "name": "priority", "isAttr": true, "type": "String" + }, + { + "name": "candidateStrategy", + "isAttr": true, + "type": "String" + }, + { + "name": "candidateParam", + "isAttr": true, + "type": "String" } ] }, diff --git a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue index 377592f4..86a1cf74 100644 --- a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue +++ b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue @@ -24,15 +24,10 @@ </el-collapse-item> <el-collapse-item name="condition" v-if="formVisible" key="form"> <template #title><Icon icon="ep:list" />表单</template> - <!-- <element-form :id="elementId" :type="elementType" /> --> - 友情提示:使用 - <router-link :to="{ path: '/bpm/manager/form' }" - ><el-link type="danger">流程表单</el-link> - </router-link> - 替代,提供更好的表单设计功能 + <element-form :id="elementId" :type="elementType" /> </el-collapse-item> <el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task"> - <template #title><Icon icon="ep:checked" />任务</template> + <template #title><Icon icon="ep:checked" />任务(审批人)</template> <element-task :id="elementId" :type="elementType" /> </el-collapse-item> <el-collapse-item @@ -40,7 +35,7 @@ v-if="elementType.indexOf('Task') !== -1" key="multiInstance" > - <template #title><Icon icon="ep:help-filled" />多实例</template> + <template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template> <element-multi-instance :business-object="elementBusinessObject" :type="elementType" /> </el-collapse-item> <el-collapse-item name="listeners" key="listeners"> diff --git a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue index 639c1cb2..70ad4f8b 100644 --- a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue +++ b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue @@ -3,13 +3,6 @@ <el-form label-width="90px" :model="needProps" :rules="rules"> <div v-if="needProps.type == 'bpmn:Process'"> <!-- 如果是 Process 信息的时候,使用自定义表单 --> - <el-link - href="https://doc.iocoder.cn/bpm/#_3-%E6%B5%81%E7%A8%8B%E5%9B%BE%E7%A4%BA%E4%BE%8B" - type="danger" - target="_blank" - > - 如何实现实现会签、或签? - </el-link> <el-form-item label="流程标识" prop="id"> <el-input v-model="needProps.id" @@ -68,13 +61,13 @@ const resetBaseInfo = () => { console.log(bpmnElement.value, 'bpmnElement') bpmnElement.value = bpmnInstances()?.bpmnElement - console.log(bpmnElement.value, 'resetBaseInfo11111111111') + // console.log(bpmnElement.value, 'resetBaseInfo11111111111') elementBaseInfo.value = bpmnElement.value.businessObject needProps.value['type'] = bpmnElement.value.businessObject.$type // elementBaseInfo.value['typess'] = bpmnElement.value.businessObject.$type // elementBaseInfo.value = JSON.parse(JSON.stringify(bpmnElement.value.businessObject)) - console.log(elementBaseInfo.value, 'elementBaseInfo22222222222') + // console.log(elementBaseInfo.value, 'elementBaseInfo22222222222') } const handleKeyUpdate = (value) => { // 校验 value 的值,只有 XML NCName 通过的情况下,才进行赋值。否则,会导致流程图报错,无法绘制的问题 @@ -121,11 +114,11 @@ const updateBaseInfo = (key) => { // id: elementBaseInfo.value[key] // // di: { id: `${elementBaseInfo.value[key]}_di` } // } - console.log(elementBaseInfo, 'elementBaseInfo11111111111') + // console.log(elementBaseInfo, 'elementBaseInfo11111111111') needProps.value = { ...elementBaseInfo.value, ...needProps.value } if (key === 'id') { - console.log('jinru') + // console.log('jinru') console.log(window, 'window') console.log(bpmnElement.value, 'bpmnElement') console.log(toRaw(bpmnElement.value), 'bpmnElement') @@ -138,20 +131,11 @@ const updateBaseInfo = (key) => { bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), attrObj) } } -onMounted(() => { - // 针对上传的 bpmn 流程图时,需要延迟 1 秒的时间,保证 key 和 name 的更新 - setTimeout(() => { - console.log(props.model, 'props.model') - handleKeyUpdate(props.model.key) - handleNameUpdate(props.model.name) - console.log(props, 'propsssssssssssssssssssss') - }, 1000) -}) watch( () => props.businessObject, (val) => { - console.log(val, 'val11111111111111111111') + // console.log(val, 'val11111111111111111111') if (val) { // nextTick(() => { resetBaseInfo() @@ -159,6 +143,18 @@ watch( } } ) + +watch( + () => props.model?.key, + (val) => { + // 针对上传的 bpmn 流程图时,保证 key 和 name 的更新 + if (val) { + handleKeyUpdate(props.model.key) + handleNameUpdate(props.model.name) + } + } +) + // watch( // () => ({ ...props }), // (oldVal, newVal) => { diff --git a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue index da1d1ae9..33f0bc09 100644 --- a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue +++ b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue @@ -1,228 +1,233 @@ <template> <div class="panel-tab__content"> <el-form label-width="80px"> - <el-form-item label="表单标识"> - <el-input v-model="formKey" clearable @change="updateElementFormKey" /> - </el-form-item> - <el-form-item label="业务标识"> - <el-select v-model="businessKey" @change="updateElementBusinessKey"> - <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" /> - <el-option label="无" value="" /> + <el-form-item label="流程表单"> + <!-- <el-input v-model="formKey" clearable @change="updateElementFormKey" />--> + <el-select v-model="formKey" clearable @change="updateElementFormKey"> + <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> </el-select> </el-form-item> + <!-- <el-form-item label="业务标识">--> + <!-- <el-select v-model="businessKey" @change="updateElementBusinessKey">--> + <!-- <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />--> + <!-- <el-option label="无" value="" />--> + <!-- </el-select>--> + <!-- </el-form-item>--> </el-form> <!--字段列表--> - <div class="element-property list-property"> - <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider> - <el-table :data="fieldList" max-height="240" fit border> - <el-table-column label="序号" type="index" width="50px" /> - <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip /> - <el-table-column - label="字段类型" - prop="type" - min-width="80px" - :formatter="(row) => fieldType[row.type] || row.type" - show-overflow-tooltip - /> - <el-table-column - label="默认值" - prop="defaultValue" - min-width="80px" - show-overflow-tooltip - /> - <el-table-column label="操作" width="90px"> - <template #default="scope"> - <el-button type="primary" link @click="openFieldForm(scope, scope.$index)" - >编辑</el-button - > - <el-divider direction="vertical" /> - <el-button - type="primary" - link - style="color: #ff4d4f" - @click="removeField(scope, scope.$index)" - >移除</el-button - > - </template> - </el-table-column> - </el-table> - </div> - <div class="element-drawer__button"> - <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" /> - </div> + <!-- <div class="element-property list-property">--> + <!-- <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>--> + <!-- <el-table :data="fieldList" max-height="240" fit border>--> + <!-- <el-table-column label="序号" type="index" width="50px" />--> + <!-- <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />--> + <!-- <el-table-column--> + <!-- label="字段类型"--> + <!-- prop="type"--> + <!-- min-width="80px"--> + <!-- :formatter="(row) => fieldType[row.type] || row.type"--> + <!-- show-overflow-tooltip--> + <!-- />--> + <!-- <el-table-column--> + <!-- label="默认值"--> + <!-- prop="defaultValue"--> + <!-- min-width="80px"--> + <!-- show-overflow-tooltip--> + <!-- />--> + <!-- <el-table-column label="操作" width="90px">--> + <!-- <template #default="scope">--> + <!-- <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"--> + <!-- >编辑</el-button--> + <!-- >--> + <!-- <el-divider direction="vertical" />--> + <!-- <el-button--> + <!-- type="primary"--> + <!-- link--> + <!-- style="color: #ff4d4f"--> + <!-- @click="removeField(scope, scope.$index)"--> + <!-- >移除</el-button--> + <!-- >--> + <!-- </template>--> + <!-- </el-table-column>--> + <!-- </el-table>--> + <!-- </div>--> + <!-- <div class="element-drawer__button">--> + <!-- <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />--> + <!-- </div>--> <!--字段配置侧边栏--> - <el-drawer - v-model="fieldModelVisible" - title="字段配置" - :size="`${width}px`" - append-to-body - destroy-on-close - > - <el-form :model="formFieldForm" label-width="90px"> - <el-form-item label="字段ID"> - <el-input v-model="formFieldForm.id" clearable /> - </el-form-item> - <el-form-item label="类型"> - <el-select - v-model="formFieldForm.typeType" - placeholder="请选择字段类型" - clearable - @change="changeFieldTypeType" - > - <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" /> - </el-select> - </el-form-item> - <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'"> - <el-input v-model="formFieldForm.type" clearable /> - </el-form-item> - <el-form-item label="名称"> - <el-input v-model="formFieldForm.label" clearable /> - </el-form-item> - <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'"> - <el-input v-model="formFieldForm.datePattern" clearable /> - </el-form-item> - <el-form-item label="默认值"> - <el-input v-model="formFieldForm.defaultValue" clearable /> - </el-form-item> - </el-form> + <!-- <el-drawer--> + <!-- v-model="fieldModelVisible"--> + <!-- title="字段配置"--> + <!-- :size="`${width}px`"--> + <!-- append-to-body--> + <!-- destroy-on-close--> + <!-- >--> + <!-- <el-form :model="formFieldForm" label-width="90px">--> + <!-- <el-form-item label="字段ID">--> + <!-- <el-input v-model="formFieldForm.id" clearable />--> + <!-- </el-form-item>--> + <!-- <el-form-item label="类型">--> + <!-- <el-select--> + <!-- v-model="formFieldForm.typeType"--> + <!-- placeholder="请选择字段类型"--> + <!-- clearable--> + <!-- @change="changeFieldTypeType"--> + <!-- >--> + <!-- <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />--> + <!-- </el-select>--> + <!-- </el-form-item>--> + <!-- <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">--> + <!-- <el-input v-model="formFieldForm.type" clearable />--> + <!-- </el-form-item>--> + <!-- <el-form-item label="名称">--> + <!-- <el-input v-model="formFieldForm.label" clearable />--> + <!-- </el-form-item>--> + <!-- <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">--> + <!-- <el-input v-model="formFieldForm.datePattern" clearable />--> + <!-- </el-form-item>--> + <!-- <el-form-item label="默认值">--> + <!-- <el-input v-model="formFieldForm.defaultValue" clearable />--> + <!-- </el-form-item>--> + <!-- </el-form>--> - <!-- 枚举值设置 --> - <template v-if="formFieldForm.type === 'enum'"> - <el-divider key="enum-divider" /> - <p class="listener-filed__title" key="enum-title"> - <span><Icon icon="ep:menu" />枚举值列表:</span> - <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')" - >添加枚举值</el-button - > - </p> - <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border> - <el-table-column label="序号" width="50px" type="index" /> - <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip /> - <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip /> - <el-table-column label="操作" width="90px"> - <template #default="scope"> - <el-button - type="primary" - link - @click="openFieldOptionForm(scope, scope.$index, 'enum')" - >编辑</el-button - > - <el-divider direction="vertical" /> - <el-button - type="primary" - link - style="color: #ff4d4f" - @click="removeFieldOptionItem(scope, scope.$index, 'enum')" - >移除</el-button - > - </template> - </el-table-column> - </el-table> - </template> + <!-- <!– 枚举值设置 –>--> + <!-- <template v-if="formFieldForm.type === 'enum'">--> + <!-- <el-divider key="enum-divider" />--> + <!-- <p class="listener-filed__title" key="enum-title">--> + <!-- <span><Icon icon="ep:menu" />枚举值列表:</span>--> + <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"--> + <!-- >添加枚举值</el-button--> + <!-- >--> + <!-- </p>--> + <!-- <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>--> + <!-- <el-table-column label="序号" width="50px" type="index" />--> + <!-- <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />--> + <!-- <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />--> + <!-- <el-table-column label="操作" width="90px">--> + <!-- <template #default="scope">--> + <!-- <el-button--> + <!-- type="primary"--> + <!-- link--> + <!-- @click="openFieldOptionForm(scope, scope.$index, 'enum')"--> + <!-- >编辑</el-button--> + <!-- >--> + <!-- <el-divider direction="vertical" />--> + <!-- <el-button--> + <!-- type="primary"--> + <!-- link--> + <!-- style="color: #ff4d4f"--> + <!-- @click="removeFieldOptionItem(scope, scope.$index, 'enum')"--> + <!-- >移除</el-button--> + <!-- >--> + <!-- </template>--> + <!-- </el-table-column>--> + <!-- </el-table>--> + <!-- </template>--> - <!-- 校验规则 --> - <el-divider key="validation-divider" /> - <p class="listener-filed__title" key="validation-title"> - <span><Icon icon="ep:menu" />约束条件列表:</span> - <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')" - >添加约束</el-button - > - </p> - <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border> - <el-table-column label="序号" width="50px" type="index" /> - <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip /> - <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip /> - <el-table-column label="操作" width="90px"> - <template #default="scope"> - <el-button - type="primary" - link - @click="openFieldOptionForm(scope, scope.$index, 'constraint')" - >编辑</el-button - > - <el-divider direction="vertical" /> - <el-button - type="primary" - link - style="color: #ff4d4f" - @click="removeFieldOptionItem(scope, scope.$index, 'constraint')" - >移除</el-button - > - </template> - </el-table-column> - </el-table> + <!-- <!– 校验规则 –>--> + <!-- <el-divider key="validation-divider" />--> + <!-- <p class="listener-filed__title" key="validation-title">--> + <!-- <span><Icon icon="ep:menu" />约束条件列表:</span>--> + <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"--> + <!-- >添加约束</el-button--> + <!-- >--> + <!-- </p>--> + <!-- <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>--> + <!-- <el-table-column label="序号" width="50px" type="index" />--> + <!-- <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />--> + <!-- <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />--> + <!-- <el-table-column label="操作" width="90px">--> + <!-- <template #default="scope">--> + <!-- <el-button--> + <!-- type="primary"--> + <!-- link--> + <!-- @click="openFieldOptionForm(scope, scope.$index, 'constraint')"--> + <!-- >编辑</el-button--> + <!-- >--> + <!-- <el-divider direction="vertical" />--> + <!-- <el-button--> + <!-- type="primary"--> + <!-- link--> + <!-- style="color: #ff4d4f"--> + <!-- @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"--> + <!-- >移除</el-button--> + <!-- >--> + <!-- </template>--> + <!-- </el-table-column>--> + <!-- </el-table>--> - <!-- 表单属性 --> - <el-divider key="property-divider" /> - <p class="listener-filed__title" key="property-title"> - <span><Icon icon="ep:menu" />字段属性列表:</span> - <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')" - >添加属性</el-button - > - </p> - <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border> - <el-table-column label="序号" width="50px" type="index" /> - <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip /> - <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip /> - <el-table-column label="操作" width="90px"> - <template #default="scope"> - <el-button - type="primary" - link - @click="openFieldOptionForm(scope, scope.$index, 'property')" - >编辑</el-button - > - <el-divider direction="vertical" /> - <el-button - type="primary" - link - style="color: #ff4d4f" - @click="removeFieldOptionItem(scope, scope.$index, 'property')" - >移除</el-button - > - </template> - </el-table-column> - </el-table> + <!-- <!– 表单属性 –>--> + <!-- <el-divider key="property-divider" />--> + <!-- <p class="listener-filed__title" key="property-title">--> + <!-- <span><Icon icon="ep:menu" />字段属性列表:</span>--> + <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"--> + <!-- >添加属性</el-button--> + <!-- >--> + <!-- </p>--> + <!-- <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>--> + <!-- <el-table-column label="序号" width="50px" type="index" />--> + <!-- <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />--> + <!-- <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />--> + <!-- <el-table-column label="操作" width="90px">--> + <!-- <template #default="scope">--> + <!-- <el-button--> + <!-- type="primary"--> + <!-- link--> + <!-- @click="openFieldOptionForm(scope, scope.$index, 'property')"--> + <!-- >编辑</el-button--> + <!-- >--> + <!-- <el-divider direction="vertical" />--> + <!-- <el-button--> + <!-- type="primary"--> + <!-- link--> + <!-- style="color: #ff4d4f"--> + <!-- @click="removeFieldOptionItem(scope, scope.$index, 'property')"--> + <!-- >移除</el-button--> + <!-- >--> + <!-- </template>--> + <!-- </el-table-column>--> + <!-- </el-table>--> - <!-- 底部按钮 --> - <div class="element-drawer__button"> - <el-button>取 消</el-button> - <el-button type="primary" @click="saveField">保 存</el-button> - </div> - </el-drawer> + <!-- <!– 底部按钮 –>--> + <!-- <div class="element-drawer__button">--> + <!-- <el-button>取 消</el-button>--> + <!-- <el-button type="primary" @click="saveField">保 存</el-button>--> + <!-- </div>--> + <!-- </el-drawer>--> - <el-dialog - v-model="fieldOptionModelVisible" - :title="optionModelTitle" - width="600px" - append-to-body - destroy-on-close - > - <el-form :model="fieldOptionForm" label-width="96px"> - <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id"> - <el-input v-model="fieldOptionForm.id" clearable /> - </el-form-item> - <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name"> - <el-input v-model="fieldOptionForm.name" clearable /> - </el-form-item> - <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config"> - <el-input v-model="fieldOptionForm.config" clearable /> - </el-form-item> - <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value"> - <el-input v-model="fieldOptionForm.value" clearable /> - </el-form-item> - </el-form> - <template #footer> - <el-button @click="fieldOptionModelVisible = false">取 消</el-button> - <el-button type="primary" @click="saveFieldOption">确 定</el-button> - </template> - </el-dialog> + <!-- <el-dialog--> + <!-- v-model="fieldOptionModelVisible"--> + <!-- :title="optionModelTitle"--> + <!-- width="600px"--> + <!-- append-to-body--> + <!-- destroy-on-close--> + <!-- >--> + <!-- <el-form :model="fieldOptionForm" label-width="96px">--> + <!-- <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">--> + <!-- <el-input v-model="fieldOptionForm.id" clearable />--> + <!-- </el-form-item>--> + <!-- <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">--> + <!-- <el-input v-model="fieldOptionForm.name" clearable />--> + <!-- </el-form-item>--> + <!-- <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">--> + <!-- <el-input v-model="fieldOptionForm.config" clearable />--> + <!-- </el-form-item>--> + <!-- <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">--> + <!-- <el-input v-model="fieldOptionForm.value" clearable />--> + <!-- </el-form-item>--> + <!-- </el-form>--> + <!-- <template #footer>--> + <!-- <el-button @click="fieldOptionModelVisible = false">取 消</el-button>--> + <!-- <el-button type="primary" @click="saveFieldOption">确 定</el-button>--> + <!-- </template>--> + <!-- </el-dialog>--> </div> </template> <script lang="ts" setup> +import * as FormApi from '@/api/bpm/form' + defineOptions({ name: 'ElementForm' }) const props = defineProps({ @@ -263,6 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances const resetFormList = () => { bpmnELement.value = bpmnInstances().bpmnElement formKey.value = bpmnELement.value.businessObject.formKey + if (formKey.value?.length > 0) { + formKey.value = parseInt(formKey.value) + } // 获取元素扩展属性 或者 创建扩展属性 elExtensionElements.value = bpmnELement.value.businessObject.get('extensionElements') || @@ -421,7 +429,7 @@ const saveField = () => { // 移除某个 字段的 配置项 const removeFieldOptionItem = (option, index, type) => { - console.log(option, 'option') + // console.log(option, 'option') if (type === 'property') { fieldPropertiesList.value.splice(index, 1) return @@ -451,6 +459,11 @@ const updateElementExtensions = () => { }) } +const formList = ref([]) // 流程表单的下拉框的数据 +onMounted(async () => { + formList.value = await FormApi.getFormSimpleList() +}) + watch( () => props.id, (val) => { diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue index 45ee8f93..de5445c8 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue @@ -26,8 +26,16 @@ type="primary" preIcon="ep:plus" title="添加监听器" + size="small" @click="openListenerForm(null)" /> + <XButton + type="success" + preIcon="ep:select" + title="选择监听器" + size="small" + @click="openProcessListenerDialog" + /> </div> <!-- 监听器 编辑/创建 部分 --> @@ -240,11 +248,21 @@ </template> </el-dialog> </div> + + <!-- 选择弹窗 --> + <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" /> </template> <script lang="ts" setup> import { ElMessageBox } from 'element-plus' import { createListenerObject, updateElementExtensions } from '../../utils' -import { initListenerType, initListenerForm, listenerType, fieldType } from './utilSelf' +import { + initListenerType, + initListenerForm, + listenerType, + fieldType, + initListenerForm2 +} from './utilSelf' +import ProcessListenerDialog from './ProcessListenerDialog.vue' defineOptions({ name: 'ElementListeners' }) @@ -284,6 +302,7 @@ const resetListenersList = () => { } // 打开 监听器详情 侧边栏 const openListenerForm = (listener, index?) => { + // debugger if (listener) { listenerForm.value = initListenerForm(listener) editingListenerIndex.value = index @@ -321,6 +340,7 @@ const openListenerFieldForm = (field, index?) => { } // 保存监听器注入字段 const saveListenerFiled = async () => { + // debugger let validateStatus = await listenerFieldFormRef.value.validate() if (!validateStatus) return // 验证不通过直接返回 if (editingListenerFieldIndex.value === -1) { @@ -337,6 +357,7 @@ const saveListenerFiled = async () => { } // 移除监听器字段 const removeListenerField = (index) => { + // debugger ElMessageBox.confirm('确认移除该字段吗?', '提示', { confirmButtonText: '确 认', cancelButtonText: '取 消' @@ -349,6 +370,7 @@ const removeListenerField = (index) => { } // 移除监听器 const removeListener = (index) => { + debugger ElMessageBox.confirm('确认移除该监听器吗?', '提示', { confirmButtonText: '确 认', cancelButtonText: '取 消' @@ -365,6 +387,7 @@ const removeListener = (index) => { } // 保存监听器配置 const saveListenerConfig = async () => { + // debugger let validateStatus = await listenerFormRef.value.validate() if (!validateStatus) return // 验证不通过直接返回 const listenerObject = createListenerObject(listenerForm.value, false, prefix) @@ -389,6 +412,28 @@ const saveListenerConfig = async () => { listenerForm.value = {} } +// 打开监听器弹窗 +const processListenerDialogRef = ref() +const openProcessListenerDialog = async () => { + processListenerDialogRef.value.open('execution') +} +const selectProcessListener = (listener) => { + const listenerForm = initListenerForm2(listener) + const listenerObject = createListenerObject(listenerForm, false, prefix) + bpmnElementListeners.value.push(listenerObject) + elementListenersList.value.push(listenerForm) + + // 保存其他配置 + otherExtensionList.value = + bpmnElement.value.businessObject?.extensionElements?.values?.filter( + (ex) => ex.$type !== `${prefix}:ExecutionListener` + ) ?? [] + updateElementExtensions( + bpmnElement.value, + otherExtensionList.value.concat(bpmnElementListeners.value) + ) +} + watch( () => props.id, (val) => { diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue new file mode 100644 index 00000000..01f81242 --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue @@ -0,0 +1,83 @@ +<!-- 执行器选择 --> +<template> + <Dialog title="请选择监听器" v-model="dialogVisible" width="1024px"> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="名字" align="center" prop="name" /> + <el-table-column label="类型" align="center" prop="type"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" /> + </template> + </el-table-column> + <el-table-column label="事件" align="center" prop="event" /> + <el-table-column label="值类型" align="center" prop="valueType"> + <template #default="scope"> + <dict-tag + :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE" + :value="scope.row.valueType" + /> + </template> + </el-table-column> + <el-table-column label="值" align="center" prop="value" /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button link type="primary" @click="select(scope.row)"> 选择 </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + </Dialog> +</template> +<script setup lang="ts"> +import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener' +import { DICT_TYPE } from '@/utils/dict' +import { CommonStatusEnum } from '@/utils/constants' + +/** BPM 流程 表单 */ +defineOptions({ name: 'ProcessListenerDialog' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const loading = ref(true) // 列表的加载中 +const list = ref<ProcessListenerVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + type: undefined, + status: CommonStatusEnum.ENABLE +}) + +/** 打开弹窗 */ +const open = async (type: string) => { + dialogVisible.value = true + loading.value = true + try { + queryParams.pageNo = 1 + queryParams.type = type + const data = await ProcessListenerApi.getProcessListenerPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const select = async (row) => { + dialogVisible.value = false + // 发送操作成功的事件 + emit('select', row) +} +</script> diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue index 9464883c..76e0c809 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue @@ -39,6 +39,13 @@ title="添加监听器" @click="openListenerForm(null)" /> + <XButton + type="success" + preIcon="ep:select" + title="选择监听器" + size="small" + @click="openProcessListenerDialog" + /> </div> <!-- 监听器 编辑/创建 部分 --> @@ -286,11 +293,22 @@ </template> </el-dialog> </div> + + <!-- 选择弹窗 --> + <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" /> </template> <script lang="ts" setup> import { ElMessageBox } from 'element-plus' import { createListenerObject, updateElementExtensions } from '../../utils' -import { initListenerForm, initListenerType, eventType, listenerType, fieldType } from './utilSelf' +import { + initListenerForm, + initListenerType, + eventType, + listenerType, + fieldType, + initListenerForm2 +} from './utilSelf' +import ProcessListenerDialog from '@/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue' defineOptions({ name: 'UserTaskListeners' }) @@ -437,6 +455,28 @@ const removeListenerField = (field, index) => { .catch(() => console.info('操作取消')) } +// 打开监听器弹窗 +const processListenerDialogRef = ref() +const openProcessListenerDialog = async () => { + processListenerDialogRef.value.open('task') +} +const selectProcessListener = (listener) => { + const listenerForm = initListenerForm2(listener) + const listenerObject = createListenerObject(listenerForm, true, prefix) + bpmnElementListeners.value.push(listenerObject) + elementListenersList.value.push(listenerForm) + + // 保存其他配置 + otherExtensionList.value = + bpmnElement.value.businessObject?.extensionElements?.values?.filter( + (ex) => ex.$type !== `${prefix}:TaskListener` + ) ?? [] + updateElementExtensions( + bpmnElement.value, + otherExtensionList.value.concat(bpmnElementListeners.value) + ) +} + watch( () => props.id, (val) => { diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts index 5f46abd0..b4eb1d27 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts @@ -40,6 +40,33 @@ export function initListenerType(listener) { } } +/** 将 ProcessListenerDO 转换成 initListenerForm 想同的 Form 对象 */ +export function initListenerForm2(processListener) { + if (processListener.valueType === 'class') { + return { + listenerType: 'classListener', + class: processListener.value, + event: processListener.event, + fields: [] + } + } else if (processListener.valueType === 'expression') { + return { + listenerType: 'expressionListener', + expression: processListener.value, + event: processListener.event, + fields: [] + } + } else if (processListener.valueType === 'delegateExpression') { + return { + listenerType: 'delegateExpressionListener', + delegateExpression: processListener.value, + event: processListener.event, + fields: [] + } + } + throw new Error('未知的监听器类型') +} + export const listenerType = { classListener: 'Java 类', expressionListener: '表达式', diff --git a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue index 28db5aa7..c0ec1cad 100644 --- a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue +++ b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue @@ -1,11 +1,15 @@ <template> <div class="panel-tab__content"> <el-form label-width="90px"> - <el-form-item label="回路特性"> + <el-form-item label="快捷配置"> + <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button> + <el-button size="small" @click="changeConfig('会签')">会签</el-button> + <el-button size="small" @click="changeConfig('或签')">或签</el-button> + </el-form-item> + <el-form-item label="会签类型"> <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType"> <el-option label="并行多重事件" value="ParallelMultiInstance" /> <el-option label="时序多重事件" value="SequentialMultiInstance" /> - <el-option label="循环事件" value="StandardLoop" /> <el-option label="无" value="Null" /> </el-select> </el-form-item> @@ -15,7 +19,7 @@ loopCharacteristics === 'SequentialMultiInstance' " > - <el-form-item label="循环基数" key="loopCardinality"> + <el-form-item label="循环数量" key="loopCardinality"> <el-input v-model="loopInstanceForm.loopCardinality" clearable @@ -25,7 +29,8 @@ <el-form-item label="集合" key="collection" v-show="false"> <el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" /> </el-form-item> - <el-form-item label="元素变量" key="elementVariable"> + <!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none --> + <el-form-item label="元素变量" key="elementVariable" style="display: none"> <el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" /> </el-form-item> <el-form-item label="完成条件" key="completionCondition"> @@ -35,7 +40,8 @@ @change="updateLoopCondition" /> </el-form-item> - <el-form-item label="异步状态" key="async"> + <!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none --> + <el-form-item label="异步状态" key="async" style="display: none"> <el-checkbox v-model="loopInstanceForm.asyncBefore" label="异步前" @@ -124,6 +130,7 @@ const getElementLoop = (businessObject) => { businessObject.loopCharacteristics.extensionElements.values[0].body } } + const changeLoopCharacteristicsType = (type) => { // this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 切换类型取消原表单配置 // 取消多实例配置 @@ -160,6 +167,7 @@ const changeLoopCharacteristicsType = (type) => { loopCharacteristics: toRaw(multiLoopInstance.value) }) } + // 循环基数 const updateLoopCardinality = (cardinality) => { let loopCardinality = null @@ -176,6 +184,7 @@ const updateLoopCardinality = (cardinality) => { } ) } + // 完成条件 const updateLoopCondition = (condition) => { let completionCondition = null @@ -192,6 +201,7 @@ const updateLoopCondition = (condition) => { } ) } + // 重试周期 const updateLoopTimeCycle = (timeCycle) => { const extensionElements = bpmnInstances().moddle.create('bpmn:ExtensionElements', { @@ -209,6 +219,7 @@ const updateLoopTimeCycle = (timeCycle) => { } ) } + // 直接更新的基础信息 const updateLoopBase = () => { bpmnInstances().modeling.updateModdleProperties( @@ -220,6 +231,7 @@ const updateLoopBase = () => { } ) } + // 各异步状态 const updateLoopAsync = (key) => { const { asyncBefore, asyncAfter } = loopInstanceForm.value @@ -238,6 +250,20 @@ const updateLoopAsync = (key) => { ) } +const changeConfig = (config) => { + if (config === '依次审批') { + changeLoopCharacteristicsType('SequentialMultiInstance') + updateLoopCardinality('1') + updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }') + } else if (config === '会签') { + changeLoopCharacteristicsType('ParallelMultiInstance') + updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }') + } else if (config === '或签') { + changeLoopCharacteristicsType('ParallelMultiInstance') + updateLoopCondition('${ nrOfCompletedInstances > 0 }') + } +} + onBeforeUnmount(() => { multiLoopInstance.value = null bpmnElement.value = null diff --git a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue index 33a12a74..e808af39 100644 --- a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue +++ b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue @@ -1,7 +1,8 @@ <template> <div class="panel-tab__content"> <el-form size="small" label-width="90px"> - <el-form-item label="异步延续"> + <!-- add by 芋艿:由于「异步延续」暂时用不到,所以这里 display 为 none --> + <el-form-item label="异步延续" style="display: none"> <el-checkbox v-model="taskConfigForm.asyncBefore" label="异步前" diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue new file mode 100644 index 00000000..b478bb2f --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue @@ -0,0 +1,68 @@ +<!-- 表达式选择 --> +<template> + <Dialog title="请选择表达式" v-model="dialogVisible" width="1024px"> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="名字" align="center" prop="name" /> + <el-table-column label="表达式" align="center" prop="expression" /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button link type="primary" @click="select(scope.row)"> 选择 </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + </Dialog> +</template> +<script setup lang="ts"> +import { CommonStatusEnum } from '@/utils/constants' +import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression' + +/** BPM 流程 表单 */ +defineOptions({ name: 'ProcessExpressionDialog' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const loading = ref(true) // 列表的加载中 +const list = ref<ProcessExpressionVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + type: undefined, + status: CommonStatusEnum.ENABLE +}) + +/** 打开弹窗 */ +const open = async (type: string) => { + dialogVisible.value = true + loading.value = true + try { + queryParams.pageNo = 1 + queryParams.type = type + const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const select = async (row) => { + dialogVisible.value = false + // 发送操作成功的事件 + emit('select', row) +} +</script> diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue index 7b793dbc..0dffeb0f 100644 --- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue +++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue @@ -1,85 +1,204 @@ <template> - <div style="margin-top: 16px"> - <!-- <el-form-item label="处理用户">--> - <!-- <el-select v-model="userTaskForm.assignee" @change="updateElementTask('assignee')">--> - <!-- <el-option v-for="ak in mockData" :key="'ass-' + ak" :label="`用户${ak}`" :value="`user${ak}`" />--> - <!-- </el-select>--> - <!-- </el-form-item>--> - <!-- <el-form-item label="候选用户">--> - <!-- <el-select v-model="userTaskForm.candidateUsers" multiple collapse-tags @change="updateElementTask('candidateUsers')">--> - <!-- <el-option v-for="uk in mockData" :key="'user-' + uk" :label="`用户${uk}`" :value="`user${uk}`" />--> - <!-- </el-select>--> - <!-- </el-form-item>--> - <!-- <el-form-item label="候选分组">--> - <!-- <el-select v-model="userTaskForm.candidateGroups" multiple collapse-tags @change="updateElementTask('candidateGroups')">--> - <!-- <el-option v-for="gk in mockData" :key="'ass-' + gk" :label="`分组${gk}`" :value="`group${gk}`" />--> - <!-- </el-select>--> - <!-- </el-form-item>--> - <el-form-item label="到期时间"> - <el-input v-model="userTaskForm.dueDate" clearable @change="updateElementTask('dueDate')" /> - </el-form-item> - <el-form-item label="跟踪时间"> - <el-input - v-model="userTaskForm.followUpDate" + <el-form label-width="100px"> + <el-form-item label="规则类型" prop="candidateStrategy"> + <el-select + v-model="userTaskForm.candidateStrategy" clearable - @change="updateElementTask('followUpDate')" + style="width: 100%" + @change="changeCandidateStrategy" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="userTaskForm.candidateStrategy == 10" + label="指定角色" + prop="candidateParam" + > + <el-select + v-model="userTaskForm.candidateParam" + clearable + multiple + style="width: 100%" + @change="updateElementTask" + > + <el-option v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" /> + </el-select> + </el-form-item> + <el-form-item + v-if="userTaskForm.candidateStrategy == 20 || userTaskForm.candidateStrategy == 21" + label="指定部门" + prop="candidateParam" + span="24" + > + <el-tree-select + ref="treeRef" + v-model="userTaskForm.candidateParam" + :data="deptTreeOptions" + :props="defaultProps" + empty-text="加载中,请稍后" + multiple + node-key="id" + show-checkbox + @change="updateElementTask" /> </el-form-item> - <el-form-item label="优先级"> - <el-input v-model="userTaskForm.priority" clearable @change="updateElementTask('priority')" /> + <el-form-item + v-if="userTaskForm.candidateStrategy == 22" + label="指定岗位" + prop="candidateParam" + span="24" + > + <el-select + v-model="userTaskForm.candidateParam" + clearable + multiple + style="width: 100%" + @change="updateElementTask" + > + <el-option v-for="item in postOptions" :key="item.id" :label="item.name" :value="item.id" /> + </el-select> </el-form-item> - 友情提示:任务的分配规则,使用 - <router-link target="_blank" :to="{ path: '/bpm/manager/model' }" - ><el-link type="danger">流程模型</el-link> - </router-link> - 下的【分配规则】替代,提供指定角色、部门负责人、部门成员、岗位、工作组、自定义脚本等 7 - 种维护的任务分配维度,更加灵活! - </div> + <el-form-item + v-if="userTaskForm.candidateStrategy == 30" + label="指定用户" + prop="candidateParam" + span="24" + > + <el-select + v-model="userTaskForm.candidateParam" + clearable + multiple + style="width: 100%" + @change="updateElementTask" + > + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="userTaskForm.candidateStrategy === 40" + label="指定用户组" + prop="candidateParam" + > + <el-select + v-model="userTaskForm.candidateParam" + clearable + multiple + style="width: 100%" + @change="updateElementTask" + > + <el-option + v-for="item in userGroupOptions" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="userTaskForm.candidateStrategy === 60" + label="流程表达式" + prop="candidateParam" + > + <el-input + type="textarea" + v-model="userTaskForm.candidateParam[0]" + clearable + style="width: 72%" + @change="updateElementTask" + /> + <el-button class="ml-5px" size="small" type="success" @click="openProcessExpressionDialog" + >选择表达式</el-button + > + <!-- 选择弹窗 --> + <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" /> + </el-form-item> + </el-form> </template> <script lang="ts" setup> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { defaultProps, handleTree } from '@/utils/tree' +import * as RoleApi from '@/api/system/role' +import * as DeptApi from '@/api/system/dept' +import * as PostApi from '@/api/system/post' +import * as UserApi from '@/api/system/user' +import * as UserGroupApi from '@/api/bpm/userGroup' +import ProcessExpressionDialog from './ProcessExpressionDialog.vue' + defineOptions({ name: 'UserTask' }) const props = defineProps({ id: String, type: String }) -const defaultTaskForm = ref({ - assignee: '', - candidateUsers: [], - candidateGroups: [], - dueDate: '', - followUpDate: '', - priority: '' +const userTaskForm = ref({ + candidateStrategy: undefined, // 分配规则 + candidateParam: [] // 分配选项 }) -const userTaskForm = ref<any>({}) -// const mockData=ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) const bpmnElement = ref() const bpmnInstances = () => (window as any)?.bpmnInstances +const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 +const deptTreeOptions = ref() // 部门树 +const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表 +const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 +const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 + const resetTaskForm = () => { - for (let key in defaultTaskForm.value) { - let value - if (key === 'candidateUsers' || key === 'candidateGroups') { - value = bpmnElement.value?.businessObject[key] - ? bpmnElement.value.businessObject[key].split(',') - : [] + const businessObject = bpmnElement.value.businessObject + if (!businessObject) { + return + } + if (businessObject.candidateStrategy != undefined) { + userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any + } else { + userTaskForm.value.candidateStrategy = undefined + } + if (businessObject.candidateParam && businessObject.candidateParam.length > 0) { + if (userTaskForm.value.candidateStrategy === 60) { + // 特殊:流程表达式,只有一个 input 输入框 + userTaskForm.value.candidateParam = [businessObject.candidateParam] } else { - value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key] + userTaskForm.value.candidateParam = businessObject.candidateParam + .split(',') + .map((item) => +item) } - userTaskForm.value[key] = value + } else { + userTaskForm.value.candidateParam = [] } } -const updateElementTask = (key) => { - const taskAttr = Object.create(null) - if (key === 'candidateUsers' || key === 'candidateGroups') { - taskAttr[key] = - userTaskForm.value[key] && userTaskForm.value[key].length - ? userTaskForm.value[key].join() - : null - } else { - taskAttr[key] = userTaskForm.value[key] || null - } - bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr) + +/** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */ +const changeCandidateStrategy = () => { + userTaskForm.value.candidateParam = [] + updateElementTask() +} + +/** 选中某个 options 时候,更新 bpmn 图 */ +const updateElementTask = () => { + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + candidateStrategy: userTaskForm.value.candidateStrategy, + candidateParam: userTaskForm.value.candidateParam.join(',') + }) +} + +// 打开监听器弹窗 +const processExpressionDialogRef = ref() +const openProcessExpressionDialog = async () => { + processExpressionDialogRef.value.open() +} +const selectProcessExpression = (expression) => { + userTaskForm.value.candidateParam = [expression.expression] } watch( @@ -92,6 +211,21 @@ watch( }, { immediate: true } ) + +onMounted(async () => { + // 获得角色列表 + roleOptions.value = await RoleApi.getSimpleRoleList() + // 获得部门列表 + const deptOptions = await DeptApi.getSimpleDeptList() + deptTreeOptions.value = handleTree(deptOptions, 'id') + // 获得岗位列表 + postOptions.value = await PostApi.getSimplePostList() + // 获得用户列表 + userOptions.value = await UserApi.getSimpleUserList() + // 获得用户组列表 + userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList() +}) + onBeforeUnmount(() => { bpmnElement.value = null }) diff --git a/src/components/bpmnProcessDesigner/package/utils.ts b/src/components/bpmnProcessDesigner/package/utils.ts index bb6c5d52..8996788b 100644 --- a/src/components/bpmnProcessDesigner/package/utils.ts +++ b/src/components/bpmnProcessDesigner/package/utils.ts @@ -2,6 +2,7 @@ import { toRaw } from 'vue' const bpmnInstances = () => (window as any)?.bpmnInstances // 创建监听器实例 export function createListenerObject(options, isTask, prefix) { + debugger const listenerObj = Object.create(null) listenerObj.event = options.event isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段 diff --git a/src/config/axios/service.ts b/src/config/axios/service.ts index 19b8c979..25936068 100644 --- a/src/config/axios/service.ts +++ b/src/config/axios/service.ts @@ -13,7 +13,7 @@ import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } f import errorCode from './errorCode' import { resetRouter } from '@/router' -import { useCache } from '@/hooks/web/useCache' +import { deleteUserCache } from '@/hooks/web/useCache' const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE const { result_code, base_url, request_timeout } = config @@ -217,9 +217,8 @@ const handleAuthorized = () => { confirmButtonText: t('login.relogin'), type: 'warning' }).then(() => { - const { wsCache } = useCache() resetRouter() // 重置静态路由表 - wsCache.clear() + deleteUserCache() // 删除用户缓存 removeToken() isRelogin.show = false // 干掉token后再走一次路由让它过router.beforeEach的校验 diff --git a/src/hooks/web/useCache.ts b/src/hooks/web/useCache.ts index 6d2a9318..4f39f307 100644 --- a/src/hooks/web/useCache.ts +++ b/src/hooks/web/useCache.ts @@ -7,13 +7,18 @@ import WebStorageCache from 'web-storage-cache' type CacheType = 'localStorage' | 'sessionStorage' export const CACHE_KEY = { - IS_DARK: 'isDark', + // 用户相关 + ROLE_ROUTERS: 'roleRouters', USER: 'user', + // 系统设置 + IS_DARK: 'isDark', LANG: 'lang', THEME: 'theme', LAYOUT: 'layout', - ROLE_ROUTERS: 'roleRouters', - DICT_CACHE: 'dictCache' + DICT_CACHE: 'dictCache', + // 登录表单 + LoginForm: 'loginForm', + TenantId: 'tenantId' } export const useCache = (type: CacheType = 'localStorage') => { @@ -25,3 +30,10 @@ export const useCache = (type: CacheType = 'localStorage') => { wsCache } } + +export const deleteUserCache = () => { + const { wsCache } = useCache() + wsCache.delete(CACHE_KEY.USER) + wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + // 注意,不要清理 LoginForm 登录表单 +} diff --git a/src/layout/components/Collapse/src/Collapse.vue b/src/layout/components/Collapse/src/Collapse.vue index ecb6890f..a8fc7ee8 100644 --- a/src/layout/components/Collapse/src/Collapse.vue +++ b/src/layout/components/Collapse/src/Collapse.vue @@ -24,13 +24,12 @@ const toggleCollapse = () => { </script> <template> - <div :class="prefixCls"> + <div :class="prefixCls" @click="toggleCollapse"> <Icon :color="color" :icon="collapse ? 'ep:expand' : 'ep:fold'" :size="18" class="cursor-pointer" - @click="toggleCollapse" /> </div> </template> diff --git a/src/layout/components/Menu/src/Menu.vue b/src/layout/components/Menu/src/Menu.vue index 9033616f..466cca50 100644 --- a/src/layout/components/Menu/src/Menu.vue +++ b/src/layout/components/Menu/src/Menu.vue @@ -124,16 +124,6 @@ export default defineComponent({ <style lang="scss" scoped> $prefix-cls: #{$namespace}-menu; -.is-active--after { - position: absolute; - top: 0; - right: 0; - width: 4px; - height: 100%; - background-color: var(--el-color-primary); - content: ''; -} - .#{$prefix-cls} { position: relative; transition: width var(--transition-time-02); @@ -159,7 +149,6 @@ $prefix-cls: #{$namespace}-menu; } // 设置选中时的高亮背景和高亮颜色 - .#{$elNamespace}-sub-menu.is-active, .#{$elNamespace}-menu-item.is-active { color: var(--left-menu-text-active-color) !important; background-color: var(--left-menu-bg-active-color) !important; @@ -171,10 +160,6 @@ $prefix-cls: #{$namespace}-menu; .#{$elNamespace}-menu-item.is-active { position: relative; - - &::after { - @extend .is-active--after; - } } // 设置子菜单的背景颜色 @@ -194,10 +179,6 @@ $prefix-cls: #{$namespace}-menu; & > .is-active > .#{$elNamespace}-sub-menu__title { position: relative; background-color: var(--left-menu-collapse-bg-active-color) !important; - - &::after { - @extend .is-active--after; - } } } @@ -245,16 +226,6 @@ $prefix-cls: #{$namespace}-menu; <style lang="scss"> $prefix-cls: #{$namespace}-menu-popper; -.is-active--after { - position: absolute; - top: 0; - right: 0; - width: 4px; - height: 100%; - background-color: var(--el-color-primary); - content: ''; -} - .#{$prefix-cls}--vertical, .#{$prefix-cls}--horizontal { // 设置选中时子标题的颜色 @@ -281,10 +252,6 @@ $prefix-cls: #{$namespace}-menu-popper; &:hover { background-color: var(--left-menu-bg-active-color) !important; } - - &::after { - @extend .is-active--after; - } } } </style> diff --git a/src/layout/components/Menu/src/components/useRenderMenuItem.tsx b/src/layout/components/Menu/src/components/useRenderMenuItem.tsx index 17a520a6..301313fe 100644 --- a/src/layout/components/Menu/src/components/useRenderMenuItem.tsx +++ b/src/layout/components/Menu/src/components/useRenderMenuItem.tsx @@ -1,59 +1,50 @@ import { ElSubMenu, ElMenuItem } from 'element-plus' -import type { RouteMeta } from 'vue-router' import { hasOneShowingChild } from '../helper' import { isUrl } from '@/utils/is' import { useRenderMenuTitle } from './useRenderMenuTitle' -import { useDesign } from '@/hooks/web/useDesign' import { pathResolve } from '@/utils/routerHelper' -export const useRenderMenuItem = ( +const { renderMenuTitle } = useRenderMenuTitle() + +export const useRenderMenuItem = () => // allRouters: AppRouteRecordRaw[] = [], - menuMode: 'vertical' | 'horizontal' -) => { - const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { - return routers.map((v) => { - const meta = (v.meta ?? {}) as RouteMeta - if (!meta.hidden) { - const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) - const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/') + { + const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { + return routers + .filter((v) => !v.meta?.hidden) + .map((v) => { + const meta = v.meta ?? {} + const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) + const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/') - const { renderMenuTitle } = useRenderMenuTitle() + if ( + oneShowingChild && + (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && + !meta?.alwaysShow + ) { + return ( + <ElMenuItem + index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath} + > + {{ + default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta) + }} + </ElMenuItem> + ) + } else { + return ( + <ElSubMenu index={fullPath}> + {{ + title: () => renderMenuTitle(meta), + default: () => renderMenuItem(v.children!, fullPath) + }} + </ElSubMenu> + ) + } + }) + } - if ( - oneShowingChild && - (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && - !meta?.alwaysShow - ) { - return ( - <ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}> - {{ - default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta) - }} - </ElMenuItem> - ) - } else { - const { getPrefixCls } = useDesign() - - const preFixCls = getPrefixCls('menu-popper') - return ( - <ElSubMenu - index={fullPath} - popperClass={ - menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal` - } - > - {{ - title: () => renderMenuTitle(meta), - default: () => renderMenuItem(v.children!, fullPath) - }} - </ElSubMenu> - ) - } - } - }) + return { + renderMenuItem + } } - - return { - renderMenuItem - } -} diff --git a/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx b/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx index fc30b900..8941d9d7 100644 --- a/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx +++ b/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx @@ -1,5 +1,6 @@ import type { RouteMeta } from 'vue-router' import { Icon } from '@/components/Icon' +import { useI18n } from '@/hooks/web/useI18n' export const useRenderMenuTitle = () => { const renderMenuTitle = (meta: RouteMeta) => { @@ -9,10 +10,14 @@ export const useRenderMenuTitle = () => { return icon ? ( <> <Icon icon={meta.icon}></Icon> - <span class="v-menu__title">{t(title as string)}</span> + <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap"> + {t(title as string)} + </span> </> ) : ( - <span class="v-menu__title">{t(title as string)}</span> + <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap"> + {t(title as string)} + </span> ) } diff --git a/src/layout/components/TabMenu/src/TabMenu.vue b/src/layout/components/TabMenu/src/TabMenu.vue index c4f63a3f..055a6aff 100644 --- a/src/layout/components/TabMenu/src/TabMenu.vue +++ b/src/layout/components/TabMenu/src/TabMenu.vue @@ -139,7 +139,7 @@ export default defineComponent({ id={`${variables.namespace}-menu`} class={[ prefixCls, - 'relative bg-[var(--left-menu-bg-color)] top-1px z-3000 layout-border__right', + 'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right', { 'w-[var(--tab-menu-max-width)]': !unref(collapse), 'w-[var(--tab-menu-min-width)]': unref(collapse) diff --git a/src/layout/components/UserInfo/src/UserInfo.vue b/src/layout/components/UserInfo/src/UserInfo.vue index 22acfd13..5c5e3732 100644 --- a/src/layout/components/UserInfo/src/UserInfo.vue +++ b/src/layout/components/UserInfo/src/UserInfo.vue @@ -5,6 +5,9 @@ import avatarImg from '@/assets/imgs/avatar.gif' import { useDesign } from '@/hooks/web/useDesign' import { useTagsViewStore } from '@/store/modules/tagsView' import { useUserStore } from '@/store/modules/user' +import LockDialog from './components/LockDialog.vue' +import LockPage from './components/LockPage.vue' +import { useLockStore } from '@/store/modules/lock' defineOptions({ name: 'UserInfo' }) @@ -23,6 +26,14 @@ const prefixCls = getPrefixCls('user-info') const avatar = computed(() => userStore.user.avatar ?? avatarImg) const userName = computed(() => userStore.user.nickname ?? 'Admin') +// 锁定屏幕 +const lockStore = useLockStore() +const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false) +const dialogVisible = ref<boolean>(false) +const lockScreen = () => { + dialogVisible.value = true +} + const loginOut = async () => { try { await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), { @@ -33,8 +44,7 @@ const loginOut = async () => { await userStore.loginOut() tagsViewStore.delAllViews() replace('/login?redirect=/index') - } - catch { } + } catch {} } const toProfile = async () => { push('/user/profile') @@ -62,6 +72,10 @@ const toDocument = () => { <Icon icon="ep:menu" /> <div @click="toDocument">{{ t('common.document') }}</div> </ElDropdownItem> + <ElDropdownItem divided> + <Icon icon="ep:lock" /> + <div @click="lockScreen">{{ t('lock.lockScreen') }}</div> + </ElDropdownItem> <ElDropdownItem divided @click="loginOut"> <Icon icon="ep:switch-button" /> <div>{{ t('common.loginOut') }}</div> @@ -69,4 +83,31 @@ const toDocument = () => { </ElDropdownMenu> </template> </ElDropdown> + + <LockDialog v-if="dialogVisible" v-model="dialogVisible" /> + + <teleport to="body"> + <transition name="fade-bottom" mode="out-in"> + <LockPage v-if="getIsLock" /> + </transition> + </teleport> </template> + +<style scoped lang="scss"> +.fade-bottom-enter-active, +.fade-bottom-leave-active { + transition: + opacity 0.25s, + transform 0.3s; +} + +.fade-bottom-enter-from { + opacity: 0; + transform: translateY(-10%); +} + +.fade-bottom-leave-to { + opacity: 0; + transform: translateY(10%); +} +</style> diff --git a/src/layout/components/UserInfo/src/components/LockDialog.vue b/src/layout/components/UserInfo/src/components/LockDialog.vue new file mode 100644 index 00000000..f4ab7d4f --- /dev/null +++ b/src/layout/components/UserInfo/src/components/LockDialog.vue @@ -0,0 +1,98 @@ +<script setup lang="ts"> +import { useValidator } from '@/hooks/web/useValidator' +import { useDesign } from '@/hooks/web/useDesign' +import { useLockStore } from '@/store/modules/lock' +import avatarImg from '@/assets/imgs/avatar.gif' +import { useUserStore } from '@/store/modules/user' + +const { getPrefixCls } = useDesign() +const prefixCls = getPrefixCls('lock-dialog') + +const { required } = useValidator() + +const { t } = useI18n() + +const lockStore = useLockStore() + +const props = defineProps({ + modelValue: { + type: Boolean + } +}) + +const userStore = useUserStore() +const avatar = computed(() => userStore.user.avatar ?? avatarImg) +const userName = computed(() => userStore.user.nickname ?? 'Admin') + +const emit = defineEmits(['update:modelValue']) + +const dialogVisible = computed({ + get: () => props.modelValue, + set: (val) => { + console.log('set: ', val) + emit('update:modelValue', val) + } +}) + +const dialogTitle = ref(t('lock.lockScreen')) + +const formData = ref({ + password: undefined +}) +const formRules = reactive({ + password: [required()] +}) + +const formRef = ref() // 表单 Ref +const handleLock = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + dialogVisible.value = false + lockStore.setLockInfo({ + ...formData.value, + isLock: true + }) +} +</script> + +<template> + <Dialog + v-model="dialogVisible" + width="500px" + max-height="170px" + :class="prefixCls" + :title="dialogTitle" + > + <div class="flex flex-col items-center"> + <img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" /> + <span class="text-14px my-10px text-[var(--top-header-text-color)]"> + {{ userName }} + </span> + </div> + <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px"> + <el-form-item :label="t('lock.lockPassword')" prop="password"> + <el-input + type="password" + v-model="formData.password" + :placeholder="'请输入' + t('lock.lockPassword')" + clearable + show-password + /> + </el-form-item> + </el-form> + <template #footer> + <ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton> + </template> + </Dialog> +</template> + +<style lang="scss" scoped> +:global(.v-lock-dialog) { + @media (max-width: 767px) { + max-width: calc(100vw - 16px); + } +} +</style> diff --git a/src/layout/components/UserInfo/src/components/LockPage.vue b/src/layout/components/UserInfo/src/components/LockPage.vue new file mode 100644 index 00000000..497dd37b --- /dev/null +++ b/src/layout/components/UserInfo/src/components/LockPage.vue @@ -0,0 +1,270 @@ +<script lang="ts" setup> +import { resetRouter } from '@/router' +import { deleteUserCache } from '@/hooks/web/useCache' +import { useLockStore } from '@/store/modules/lock' +import { useNow } from '@/hooks/web/useNow' +import { useDesign } from '@/hooks/web/useDesign' +import { useTagsViewStore } from '@/store/modules/tagsView' +import { useUserStore } from '@/store/modules/user' +import avatarImg from '@/assets/imgs/avatar.gif' + +const tagsViewStore = useTagsViewStore() + +const { replace } = useRouter() + +const userStore = useUserStore() + +const password = ref('') +const loading = ref(false) +const errMsg = ref(false) +const showDate = ref(true) + +const { getPrefixCls } = useDesign() +const prefixCls = getPrefixCls('lock-page') + +const avatar = computed(() => userStore.user.avatar ?? avatarImg) +const userName = computed(() => userStore.user.nickname ?? 'Admin') + +const lockStore = useLockStore() + +const { hour, month, minute, meridiem, year, day, week } = useNow(true) + +const { t } = useI18n() + +// 解锁 +async function unLock() { + if (!password.value) { + return + } + let pwd = password.value + try { + loading.value = true + const res = await lockStore.unLock(pwd) + errMsg.value = !res + } finally { + loading.value = false + } +} + +// 返回登录 +async function goLogin() { + await userStore.loginOut().catch(() => {}) + // 登出后清理 + deleteUserCache() // 清空用户缓存 + tagsViewStore.delAllViews() + resetRouter() // 重置静态路由表 + lockStore.resetLockInfo() + replace('/login') +} + +function handleShowForm(show = false) { + showDate.value = show +} +</script> + +<template> + <div + :class="prefixCls" + class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center" + > + <div + :class="`${prefixCls}__unlock`" + class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2" + @click="handleShowForm(false)" + v-show="showDate" + > + <Icon icon="ep:lock" /> + <span>{{ t('lock.unlock') }}</span> + </div> + + <div class="flex w-screen h-screen justify-center items-center"> + <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5"> + <span>{{ hour }}</span> + <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate"> + {{ meridiem }} + </span> + </div> + <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `"> + <span> {{ minute }}</span> + </div> + </div> + <transition name="fade-slide"> + <div :class="`${prefixCls}-entry`" v-show="!showDate"> + <div :class="`${prefixCls}-entry-content`"> + <div class="flex flex-col items-center"> + <img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" /> + <span class="text-14px my-10px text-[var(--logo-title-text-color)]"> + {{ userName }} + </span> + </div> + <ElInput + type="password" + :placeholder="t('lock.placeholder')" + class="enter-x" + v-model="password" + /> + <span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg"> + {{ t('lock.message') }} + </span> + <div :class="`${prefixCls}-entry__footer enter-x`"> + <ElButton + type="primary" + size="small" + class="mt-2 mr-2 enter-x" + link + :disabled="loading" + @click="handleShowForm(true)" + > + {{ t('common.back') }} + </ElButton> + <ElButton + type="primary" + size="small" + class="mt-2 mr-2 enter-x" + link + :disabled="loading" + @click="goLogin" + > + {{ t('lock.backToLogin') }} + </ElButton> + <ElButton + type="primary" + class="mt-2" + size="small" + link + @click="unLock()" + :disabled="loading" + > + {{ t('lock.entrySystem') }} + </ElButton> + </div> + </div> + </div> + </transition> + + <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y"> + <div class="text-5xl mb-4 enter-x" v-show="!showDate"> + {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span> + </div> + <div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div> + </div> + </div> +</template> + +<style lang="scss" scoped> +$prefix-cls: '#{$namespace}-lock-page'; + +// Small screen / tablet +$screen-sm: 576px; + +// Medium screen / desktop +$screen-md: 768px; + +// Large screen / wide desktop +$screen-lg: 992px; + +// Extra large screen / full hd +$screen-xl: 1200px; + +// Extra extra large screen / large desktop +$screen-2xl: 1600px; + +$error-color: #ed6f6f; + +.#{$prefix-cls} { + z-index: 3000; + + &__unlock { + transform: translate(-50%, 0); + } + + &__hour, + &__minute { + display: flex; + font-weight: 700; + color: #bababa; + background-color: #141313; + border-radius: 30px; + justify-content: center; + align-items: center; + + @media screen and (max-width: $screen-md) { + span:not(.meridiem) { + font-size: 160px; + } + } + + @media screen and (min-width: $screen-md) { + span:not(.meridiem) { + font-size: 160px; + } + } + + @media screen and (max-width: $screen-sm) { + span:not(.meridiem) { + font-size: 90px; + } + } + @media screen and (min-width: $screen-lg) { + span:not(.meridiem) { + font-size: 220px; + } + } + + @media screen and (min-width: $screen-xl) { + span:not(.meridiem) { + font-size: 260px; + } + } + @media screen and (min-width: $screen-2xl) { + span:not(.meridiem) { + font-size: 320px; + } + } + } + + &-entry { + position: absolute; + top: 0; + left: 0; + display: flex; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(8px); + justify-content: center; + align-items: center; + + &-content { + width: 260px; + } + + &__header { + text-align: center; + + &-img { + width: 70px; + margin: 0 auto; + border-radius: 50%; + } + + &-name { + margin-top: 5px; + font-weight: 500; + color: #bababa; + } + } + + &__err-msg { + display: inline-block; + margin-top: 10px; + color: $error-color; + } + + &__footer { + display: flex; + justify-content: space-between; + } + } +} +</style> diff --git a/src/locales/en.ts b/src/locales/en.ts index 4f4d4895..6562c9b7 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -56,6 +56,16 @@ export default { copySuccess: 'Copy Success', copyError: 'Copy Error' }, + lock: { + lockScreen: 'Lock screen', + lock: 'Lock', + lockPassword: 'Lock screen password', + unlock: 'Click to unlock', + backToLogin: 'Back to login', + entrySystem: 'Entry the system', + placeholder: 'Please enter the lock screen password', + message: 'Lock screen password error' + }, error: { noPermission: `Sorry, you don't have permission to access this page.`, pageError: 'Sorry, the page you visited does not exist.', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 6346a3d3..0721651d 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -56,6 +56,16 @@ export default { copySuccess: '复制成功', copyError: '复制失败' }, + lock: { + lockScreen: '锁定屏幕', + lock: '锁定', + lockPassword: '锁屏密码', + unlock: '点击解锁', + backToLogin: '返回登录', + entrySystem: '进入系统', + placeholder: '请输入锁屏密码', + message: '锁屏密码错误' + }, error: { noPermission: `抱歉,您无权访问此页面。`, pageError: '抱歉,您访问的页面不存在。', diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index f63bee6e..bc62a3c4 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -243,7 +243,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ }, children: [ { - path: '/manager/form/edit', + path: 'manager/form/edit', component: () => import('@/views/bpm/form/editor/index.vue'), name: 'BpmFormEditor', meta: { @@ -255,7 +255,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ } }, { - path: '/manager/model/edit', + path: 'manager/model/edit', component: () => import('@/views/bpm/model/editor/index.vue'), name: 'BpmModelEditor', meta: { @@ -267,7 +267,19 @@ const remainingRouter: AppRouteRecordRaw[] = [ } }, { - path: '/manager/definition', + path: 'manager/simple/workflow/model/edit', + component: () => import('@/views/bpm/simpleWorkflow/index.vue'), + name: 'SimpleWorkflowDesignEditor', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '仿钉钉设计流程', + activeMenu: '/bpm/manager/model' + } + }, + { + path: 'manager/definition', component: () => import('@/views/bpm/definition/index.vue'), name: 'BpmProcessDefinition', meta: { @@ -279,30 +291,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ } }, { - path: '/manager/task-assign-rule', - component: () => import('@/views/bpm/taskAssignRule/index.vue'), - name: 'BpmTaskAssignRuleList', - meta: { - noCache: true, - hidden: true, - canTo: true, - title: '任务分配规则' - } - }, - { - path: '/process-instance/create', - component: () => import('@/views/bpm/processInstance/create/index.vue'), - name: 'BpmProcessInstanceCreate', - meta: { - noCache: true, - hidden: true, - canTo: true, - title: '发起流程', - activeMenu: 'bpm/processInstance/create' - } - }, - { - path: '/process-instance/detail', + path: 'process-instance/detail', component: () => import('@/views/bpm/processInstance/detail/index.vue'), name: 'BpmProcessInstanceDetail', meta: { @@ -310,11 +299,11 @@ const remainingRouter: AppRouteRecordRaw[] = [ hidden: true, canTo: true, title: '流程详情', - activeMenu: 'bpm/processInstance/detail' + activeMenu: '/bpm/task/my' } }, { - path: '/bpm/oa/leave/create', + path: 'oa/leave/create', component: () => import('@/views/bpm/oa/leave/create.vue'), name: 'OALeaveCreate', meta: { @@ -326,7 +315,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ } }, { - path: '/bpm/oa/leave/detail', + path: 'oa/leave/detail', component: () => import('@/views/bpm/oa/leave/detail.vue'), name: 'OALeaveDetail', meta: { diff --git a/src/store/index.ts b/src/store/index.ts index 65964ea8..63f00452 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,7 +1,9 @@ import type { App } from 'vue' import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const store = createPinia() +store.use(piniaPluginPersistedstate) export const setupStore = (app: App<Element>) => { app.use(store) diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts index 1d0c797a..87336181 100644 --- a/src/store/modules/app.ts +++ b/src/store/modules/app.ts @@ -268,7 +268,8 @@ export const useAppStore = defineStore('app', { setFooter(footer: boolean) { this.footer = footer } - } + }, + persist: false }) export const useAppStoreWithOut = () => { diff --git a/src/store/modules/lock.ts b/src/store/modules/lock.ts new file mode 100644 index 00000000..68ae1d7d --- /dev/null +++ b/src/store/modules/lock.ts @@ -0,0 +1,48 @@ +import { defineStore } from 'pinia' +import { store } from '@/store' + +interface lockInfo { + isLock?: boolean + password?: string +} + +interface LockState { + lockInfo: lockInfo +} + +export const useLockStore = defineStore('lock', { + state: (): LockState => { + return { + lockInfo: { + // isLock: false, // 是否锁定屏幕 + // password: '' // 锁屏密码 + } + } + }, + getters: { + getLockInfo(): lockInfo { + return this.lockInfo + } + }, + actions: { + setLockInfo(lockInfo: lockInfo) { + this.lockInfo = lockInfo + }, + resetLockInfo() { + this.lockInfo = {} + }, + unLock(password: string) { + if (this.lockInfo?.password === password) { + this.resetLockInfo() + return true + } else { + return false + } + } + }, + persist: true +}) + +export const useLockStoreWithOut = () => { + return useLockStore(store) +} diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index c729cea0..5e3287a7 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { store } from '../index' +import { store } from '@/store' import { cloneDeep } from 'lodash-es' import remainingRouter from '@/router/modules/remaining' import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper' @@ -59,7 +59,8 @@ export const usePermissionStore = defineStore('permission', { setMenuTabRouters(routers: AppRouteRecordRaw[]): void { this.menuTabRouters = routers } - } + }, + persist: false }) export const usePermissionStoreWithOut = () => { diff --git a/src/store/modules/simpleWorkflow.ts b/src/store/modules/simpleWorkflow.ts new file mode 100644 index 00000000..cf98538d --- /dev/null +++ b/src/store/modules/simpleWorkflow.ts @@ -0,0 +1,55 @@ +import { store } from '../index' +import { defineStore } from 'pinia' + +export const useWorkFlowStore = defineStore('simpleWorkflow', { + state: () => ({ + tableId: '', + isTried: false, + promoterDrawer: false, + flowPermission1: {}, + approverDrawer: false, + approverConfig1: {}, + copyerDrawer: false, + copyerConfig1: {}, + conditionDrawer: false, + conditionsConfig1: { + conditionNodes: [] + } + }), + actions: { + setTableId(payload) { + this.tableId = payload + }, + setIsTried(payload) { + this.isTried = payload + }, + setPromoter(payload) { + this.promoterDrawer = payload + }, + setFlowPermission(payload) { + this.flowPermission1 = payload + }, + setApprover(payload) { + this.approverDrawer = payload + }, + setApproverConfig(payload) { + this.approverConfig1 = payload + }, + setCopyer(payload) { + this.copyerDrawer = payload + }, + setCopyerConfig(payload) { + this.copyerConfig1 = payload + }, + setCondition(payload) { + this.conditionDrawer = payload + }, + setConditionsConfig(payload) { + this.conditionsConfig1 = payload + } + } +}) + +export const useWorkFlowStoreWithOut = () => { + return useWorkFlowStore(store) +} diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts index a60d0e45..25a3a1fd 100644 --- a/src/store/modules/tagsView.ts +++ b/src/store/modules/tagsView.ts @@ -132,7 +132,8 @@ export const useTagsViewStore = defineStore('tagsView', { } } } - } + }, + persist: false }) export const useTagsViewStoreWithOut = () => { diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index cb71a1a4..b3861809 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -1,7 +1,7 @@ -import { store } from '../index' +import { store } from '@/store' import { defineStore } from 'pinia' import { getAccessToken, removeToken } from '@/utils/auth' -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache' import { getInfo, loginOut } from '@/api/login' const { wsCache } = useCache() @@ -14,6 +14,7 @@ interface UserVO { } interface UserInfoVO { + // USER 缓存 permissions: string[] roles: string[] isSetUser: boolean @@ -80,7 +81,7 @@ export const useUserStore = defineStore('admin-user', { async loginOut() { await loginOut() removeToken() - wsCache.clear() + deleteUserCache() // 删除用户缓存 this.resetState() }, resetState() { diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 7da49b08..c68a67a9 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,4 +1,4 @@ -import { useCache } from '@/hooks/web/useCache' +import { useCache, CACHE_KEY } from '@/hooks/web/useCache' import { TokenType } from '@/api/login/types' import { decrypt, encrypt } from '@/utils/jsencrypt' @@ -36,8 +36,6 @@ export const formatToken = (token: string): string => { } // ========== 账号相关 ========== -const LoginFormKey = 'LOGINFORM' - export type LoginFormType = { tenantName: string username: string @@ -46,7 +44,7 @@ export type LoginFormType = { } export const getLoginForm = () => { - const loginForm: LoginFormType = wsCache.get(LoginFormKey) + const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm) if (loginForm) { loginForm.password = decrypt(loginForm.password) as string } @@ -55,38 +53,19 @@ export const getLoginForm = () => { export const setLoginForm = (loginForm: LoginFormType) => { loginForm.password = encrypt(loginForm.password) as string - wsCache.set(LoginFormKey, loginForm, { exp: 30 * 24 * 60 * 60 }) + wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 }) } export const removeLoginForm = () => { - wsCache.delete(LoginFormKey) + wsCache.delete(CACHE_KEY.LoginForm) } // ========== 租户相关 ========== -const TenantIdKey = 'TENANT_ID' -const TenantNameKey = 'TENANT_NAME' - -export const getTenantName = () => { - return wsCache.get(TenantNameKey) -} - -export const setTenantName = (username: string) => { - wsCache.set(TenantNameKey, username, { exp: 30 * 24 * 60 * 60 }) -} - -export const removeTenantName = () => { - wsCache.delete(TenantNameKey) -} - export const getTenantId = () => { - return wsCache.get(TenantIdKey) + return wsCache.get(CACHE_KEY.TenantId) } export const setTenantId = (username: string) => { - wsCache.set(TenantIdKey, username) -} - -export const removeTenantId = () => { - wsCache.delete(TenantIdKey) + wsCache.set(CACHE_KEY.TenantId, username) } diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts new file mode 100644 index 00000000..316b870b --- /dev/null +++ b/src/utils/dateUtil.ts @@ -0,0 +1,18 @@ +/** + * Independent time operation tool to facilitate subsequent switch to dayjs + */ +// TODO 芋艿:【锁屏】可能后面删除掉 +import dayjs from 'dayjs' + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' +const DATE_FORMAT = 'YYYY-MM-DD' + +export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string { + return dayjs(date).format(format) +} + +export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string { + return dayjs(date).format(format) +} + +export const dateUtil = dayjs diff --git a/src/utils/dict.ts b/src/utils/dict.ts index cc1774b3..2284ff13 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -136,13 +136,13 @@ export enum DICT_TYPE { INFRA_FILE_STORAGE = 'infra_file_storage', // ========== BPM 模块 ========== - BPM_MODEL_CATEGORY = 'bpm_model_category', BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', - BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type', + BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy', BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', - BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result', - BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script', + BPM_TASK_STATUS = 'bpm_task_status', BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type', + BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type', + BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type', // ========== PAY 模块 ========== PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型 @@ -157,7 +157,7 @@ export enum DICT_TYPE { MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型 - // ========== MALL - 会员模块 ========== + // ========== Member 会员模块 ========== MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型 MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型 diff --git a/src/utils/formCreate.ts b/src/utils/formCreate.ts index 6d7dbc7f..b9644d6f 100644 --- a/src/utils/formCreate.ts +++ b/src/utils/formCreate.ts @@ -28,7 +28,7 @@ export const decodeFields = (fields: string[]) => { return rule } -// 设置表单的 Conf 和 Fields +// 设置表单的 Conf 和 Fields,适用 FcDesigner 场景 export const setConfAndFields = (designerRef: object, conf: string, fields: string) => { // @ts-ignore designerRef.value.setOption(JSON.parse(conf)) @@ -36,19 +36,22 @@ export const setConfAndFields = (designerRef: object, conf: string, fields: stri designerRef.value.setRule(decodeFields(fields)) } -// 设置表单的 Conf 和 Fields +// 设置表单的 Conf 和 Fields,适用 form-create 场景 export const setConfAndFields2 = ( detailPreview: object, conf: string, fields: string, value?: object ) => { + if (isRef(detailPreview)) { + detailPreview = detailPreview.value + } // @ts-ignore - detailPreview.value.option = JSON.parse(conf) + detailPreview.option = JSON.parse(conf) // @ts-ignore - detailPreview.value.rule = decodeFields(fields) + detailPreview.rule = decodeFields(fields) if (value) { // @ts-ignore - detailPreview.value.value = value + detailPreview.value = value } } diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts index ed434cb0..134a986e 100644 --- a/src/utils/formatTime.ts +++ b/src/utils/formatTime.ts @@ -175,18 +175,18 @@ export function formatPast2(ms: number): string { const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60) const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60) if (day > 0) { - return day + '天' + hour + '小时' + minute + '分钟' + return day + ' 天' + hour + ' 小时 ' + minute + ' 分钟' } if (hour > 0) { - return hour + '小时' + minute + '分钟' + return hour + ' 小时 ' + minute + ' 分钟' } if (minute > 0) { - return minute + '分钟' + return minute + ' 分钟' } if (second > 0) { - return second + '秒' + return second + ' 秒' } else { - return 0 + '秒' + return 0 + ' 秒' } } diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue index e8e618dd..e61ccbe9 100644 --- a/src/views/Login/components/LoginForm.vue +++ b/src/views/Login/components/LoginForm.vue @@ -188,7 +188,7 @@ const loginData = reactive({ username: 'admin', password: 'admin123', captchaVerification: '', - rememberMe: false + rememberMe: true // 默认记录我。如果不需要,可手动修改 } }) @@ -218,14 +218,14 @@ const getTenantId = async () => { } } // 记住我 -const getCookie = () => { +const getLoginFormCache = () => { const loginForm = authUtil.getLoginForm() if (loginForm) { loginData.loginForm = { ...loginData.loginForm, username: loginForm.username ? loginForm.username : loginData.loginForm.username, password: loginForm.password ? loginForm.password : loginData.loginForm.password, - rememberMe: loginForm.rememberMe ? true : false, + rememberMe: loginForm.rememberMe, tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName } } @@ -326,7 +326,7 @@ watch( } ) onMounted(() => { - getCookie() + getLoginFormCache() getTenantByWebsite() }) </script> diff --git a/src/views/bpm/category/CategoryForm.vue b/src/views/bpm/category/CategoryForm.vue new file mode 100644 index 00000000..5b771537 --- /dev/null +++ b/src/views/bpm/category/CategoryForm.vue @@ -0,0 +1,124 @@ +<template> + <Dialog :title="dialogTitle" v-model="dialogVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="100px" + v-loading="formLoading" + > + <el-form-item label="分类名" prop="name"> + <el-input v-model="formData.name" placeholder="请输入分类名" /> + </el-form-item> + <el-form-item label="分类标志" prop="code"> + <el-input v-model="formData.code" placeholder="请输入分类标志" /> + </el-form-item> + <el-form-item label="分类状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="分类排序" prop="sort"> + <el-input-number + v-model="formData.sort" + placeholder="请输入分类排序" + class="!w-1/1" + :precision="0" + /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' + +/** BPM 流程分类 表单 */ +defineOptions({ name: 'CategoryForm' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + name: undefined, + code: undefined, + status: undefined, + sort: undefined +}) +const formRules = reactive({ + name: [{ required: true, message: '分类名不能为空', trigger: 'blur' }], + code: [{ required: true, message: '分类标志不能为空', trigger: 'blur' }], + status: [{ required: true, message: '分类状态不能为空', trigger: 'blur' }], + sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + dialogVisible.value = true + dialogTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await CategoryApi.getCategory(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + await formRef.value.validate() + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as CategoryVO + if (formType.value === 'create') { + await CategoryApi.createCategory(data) + message.success(t('common.createSuccess')) + } else { + await CategoryApi.updateCategory(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + name: undefined, + code: undefined, + status: undefined, + sort: undefined + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/bpm/category/index.vue b/src/views/bpm/category/index.vue new file mode 100644 index 00000000..46fa6cf1 --- /dev/null +++ b/src/views/bpm/category/index.vue @@ -0,0 +1,200 @@ +<template> + <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <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="code"> + <el-input + v-model="queryParams.code" + placeholder="请输入分类标志" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="分类状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择分类状态" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </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:category:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="分类编号" align="center" prop="id" /> + <el-table-column label="分类名" align="center" prop="name" /> + <el-table-column label="分类标志" align="center" prop="code" /> + <el-table-column label="分类描述" align="center" prop="description" /> + <el-table-column label="分类状态" align="center" prop="status"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column label="分类排序" align="center" prop="sort" /> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="180px" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['bpm:category:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['bpm:category:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <CategoryForm ref="formRef" @success="getList" /> +</template> + +<script setup lang="ts"> +import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import download from '@/utils/download' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' +import CategoryForm from './CategoryForm.vue' + +/** BPM 流程分类 列表 */ +defineOptions({ name: 'BpmCategory' }) + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const list = ref<CategoryVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: undefined, + code: undefined, + status: undefined, + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await CategoryApi.getCategoryPage(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 formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await CategoryApi.deleteCategory(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/bpm/definition/index.vue b/src/views/bpm/definition/index.vue index 31ed8413..1e7794b3 100644 --- a/src/views/bpm/definition/index.vue +++ b/src/views/bpm/definition/index.vue @@ -11,11 +11,7 @@ </el-button> </template> </el-table-column> - <el-table-column label="定义分类" align="center" prop="category" width="100"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" /> - </template> - </el-table-column> + <el-table-column label="定义分类" align="center" prop="categoryName" width="100" /> <el-table-column label="表单信息" align="center" prop="formType" width="200"> <template #default="scope"> <el-button @@ -57,18 +53,6 @@ width="300" show-overflow-tooltip /> - <el-table-column label="操作" align="center" width="150" fixed="right"> - <template #default="scope"> - <el-button - link - type="primary" - @click="handleAssignRule(scope.row)" - v-hasPermi="['bpm:task-assign-rule:query']" - > - 分配规则 - </el-button> - </template> - </el-table-column> </el-table> <!-- 分页 --> <Pagination @@ -88,8 +72,8 @@ <Dialog title="流程图" v-model="bpmnDetailVisible" width="800"> <MyProcessViewer key="designer" - v-model="bpmnXML" - :value="bpmnXML as any" + v-model="bpmnXml" + :value="bpmnXml as any" v-bind="bpmnControlForm" :prefix="bpmnControlForm.prefix" /> @@ -97,7 +81,6 @@ </template> <script lang="ts" setup> -import { DICT_TYPE } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package' import * as DefinitionApi from '@/api/bpm/definition' @@ -129,16 +112,6 @@ const getList = async () => { } } -/** 点击任务分配按钮 */ -const handleAssignRule = (row) => { - push({ - name: 'BpmTaskAssignRuleList', - query: { - modelId: row.id - } - }) -} - /** 流程表单的详情按钮操作 */ const formDetailVisible = ref(false) const formDetailPreview = ref({ @@ -160,12 +133,12 @@ const handleFormDetail = async (row) => { /** 流程图的详情按钮操作 */ const bpmnDetailVisible = ref(false) -const bpmnXML = ref(null) +const bpmnXml = ref(null) const bpmnControlForm = ref({ prefix: 'flowable' }) const handleBpmnDetail = async (row) => { - bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id) + bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml bpmnDetailVisible.value = true } diff --git a/src/views/bpm/form/index.vue b/src/views/bpm/form/index.vue index 4cf37777..3d542c80 100644 --- a/src/views/bpm/form/index.vue +++ b/src/views/bpm/form/index.vue @@ -1,5 +1,5 @@ <template> - <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + <doc-alert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" /> <ContentWrap> <!-- 搜索工作栏 --> diff --git a/src/views/bpm/group/UserGroupForm.vue b/src/views/bpm/group/UserGroupForm.vue index 35d833ea..ac0cfcb3 100644 --- a/src/views/bpm/group/UserGroupForm.vue +++ b/src/views/bpm/group/UserGroupForm.vue @@ -13,8 +13,8 @@ <el-form-item label="描述"> <el-input v-model="formData.description" placeholder="请输入描述" type="textarea" /> </el-form-item> - <el-form-item label="成员" prop="memberUserIds"> - <el-select v-model="formData.memberUserIds" multiple placeholder="请选择成员"> + <el-form-item label="成员" prop="userIds"> + <el-select v-model="formData.userIds" multiple placeholder="请选择成员"> <el-option v-for="user in userList" :key="user.id" @@ -60,13 +60,13 @@ const formData = ref({ id: undefined, name: undefined, description: undefined, - memberUserIds: undefined, + userIds: undefined, status: CommonStatusEnum.ENABLE }) const formRules = reactive({ name: [{ required: true, message: '组名不能为空', trigger: 'blur' }], description: [{ required: true, message: '描述不能为空', trigger: 'blur' }], - memberUserIds: [{ required: true, message: '成员不能为空', trigger: 'blur' }], + userIds: [{ required: true, message: '成员不能为空', trigger: 'blur' }], status: [{ required: true, message: '状态不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref @@ -124,7 +124,7 @@ const resetForm = () => { id: undefined, name: undefined, description: undefined, - memberUserIds: undefined, + userIds: undefined, status: CommonStatusEnum.ENABLE } formRef.value?.resetFields() diff --git a/src/views/bpm/group/index.vue b/src/views/bpm/group/index.vue index 98a445d6..62785a92 100644 --- a/src/views/bpm/group/index.vue +++ b/src/views/bpm/group/index.vue @@ -63,7 +63,7 @@ <el-table-column label="描述" align="center" prop="description" /> <el-table-column label="成员" align="center"> <template #default="scope"> - <span v-for="userId in scope.row.memberUserIds" :key="userId" class="pr-5px"> + <span v-for="userId in scope.row.userIds" :key="userId" class="pr-5px"> {{ userList.find((user) => user.id === userId)?.nickname }} </span> </template> diff --git a/src/views/bpm/model/ModelForm.vue b/src/views/bpm/model/ModelForm.vue index 15935e18..ce60edca 100644 --- a/src/views/bpm/model/ModelForm.vue +++ b/src/views/bpm/model/ModelForm.vue @@ -43,13 +43,16 @@ style="width: 100%" > <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)" - :key="dict.value" - :label="dict.label" - :value="dict.value" + v-for="category in categoryList" + :key="category.code" + :label="category.name" + :value="category.code" /> </el-select> </el-form-item> + <el-form-item v-if="formData.id" label="流程图标" prop="icon"> + <UploadImg v-model="formData.icon" :limit="1" height="128px" width="128px" /> + </el-form-item> <el-form-item label="流程描述" prop="description"> <el-input v-model="formData.description" clearable type="textarea" /> </el-form-item> @@ -126,6 +129,7 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { ElMessageBox } from 'element-plus' import * as ModelApi from '@/api/bpm/model' import * as FormApi from '@/api/bpm/form' +import { CategoryApi } from '@/api/bpm/category' defineOptions({ name: 'ModelForm' }) @@ -140,20 +144,23 @@ const formData = ref({ formType: 10, name: '', category: undefined, + icon: undefined, description: '', formId: '', formCustomCreatePath: '', formCustomViewPath: '' }) const formRules = reactive({ - category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }], name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }], key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }], + category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }], + icon: [{ required: true, message: '参数图标不能为空', trigger: 'blur' }], value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }], visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref const formList = ref([]) // 流程表单的下拉框的数据 +const categoryList = ref([]) // 流程分类列表 /** 打开弹窗 */ const open = async (type: string, id?: number) => { @@ -171,7 +178,9 @@ const open = async (type: string, id?: number) => { } } // 获得流程表单的下拉框的数据 - formList.value = await FormApi.getSimpleFormList() + formList.value = await FormApi.getFormSimpleList() + // 查询流程分类列表 + categoryList.value = await CategoryApi.getCategorySimpleList() } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 @@ -190,11 +199,10 @@ const submitForm = async () => { await ModelApi.createModel(data) // 提示,引导用户做后续的操作 await ElMessageBox.alert( - '<strong>新建模型成功!</strong>后续需要执行如下 4 个步骤:' + + '<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' + '<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' + '<div>2. 点击【设计流程】按钮,绘制流程图</div>' + - '<div>3. 点击【分配规则】按钮,设置每个用户任务的审批人</div>' + - '<div>4. 点击【发布流程】按钮,完成流程的最终发布</div>' + + '<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' + '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', '重要提示', { @@ -220,6 +228,7 @@ const resetForm = () => { formType: 10, name: '', category: undefined, + icon: '', description: '', formId: '', formCustomCreatePath: '', diff --git a/src/views/bpm/model/ModelImportForm.vue b/src/views/bpm/model/ModelImportForm.vue index 74f10ffd..9a91e1d5 100644 --- a/src/views/bpm/model/ModelImportForm.vue +++ b/src/views/bpm/model/ModelImportForm.vue @@ -109,6 +109,7 @@ const submitFormSuccess = async (response: any) => { } // 提示成功 message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】') + dialogVisible.value = false // 发送操作成功的事件 emit('success') } diff --git a/src/views/bpm/model/editor/index.vue b/src/views/bpm/model/editor/index.vue index f5c0ec6e..29bca71c 100644 --- a/src/views/bpm/model/editor/index.vue +++ b/src/views/bpm/model/editor/index.vue @@ -89,11 +89,21 @@ onMounted(async () => { } // 查询模型 const data = await ModelApi.getModel(modelId) - xmlString.value = data.bpmnXml + if (!data.bpmnXml) { + // 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的 + data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?> +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef"> + <process id="${data.key}" name="${data.name}" isExecutable="true" /> + <bpmndi:BPMNDiagram id="BPMNDiagram"> + <bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" /> + </bpmndi:BPMNDiagram> +</definitions>` + } model.value = { ...data, bpmnXml: undefined // 清空 bpmnXml 属性 } + xmlString.value = data.bpmnXml }) </script> <style lang="scss"> diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue index c7318891..e4ba6d4c 100644 --- a/src/views/bpm/model/index.vue +++ b/src/views/bpm/model/index.vue @@ -1,5 +1,11 @@ <template> - <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + <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> <!-- 搜索工作栏 --> @@ -36,10 +42,10 @@ class="!w-240px" > <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)" - :key="dict.value" - :label="dict.label" - :value="dict.value" + v-for="category in categoryList" + :key="category.code" + :label="category.name" + :value="category.code" /> </el-select> </el-form-item> @@ -72,11 +78,12 @@ </el-button> </template> </el-table-column> - <el-table-column label="流程分类" align="center" prop="category" width="100"> + <el-table-column label="流程图标" align="center" prop="icon" width="100"> <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" /> + <el-image :src="scope.row.icon" class="w-32px h-32px" /> </template> </el-table-column> + <el-table-column label="流程分类" align="center" prop="categoryName" width="100" /> <el-table-column label="表单信息" align="center" prop="formType" width="200"> <template #default="scope"> <el-button @@ -164,10 +171,10 @@ <el-button link type="primary" - @click="handleAssignRule(scope.row)" - v-hasPermi="['bpm:task-assign-rule:query']" + @click="handleSimpleDesign(scope.row.id)" + v-hasPermi="['bpm:model:update']" > - 分配规则 + 仿钉钉设计流程 </el-button> <el-button link @@ -229,7 +236,6 @@ </template> <script lang="ts" setup> -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter, formatDate } from '@/utils/formatTime' import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package' import * as ModelApi from '@/api/bpm/model' @@ -237,6 +243,7 @@ import * as FormApi from '@/api/bpm/form' import ModelForm from './ModelForm.vue' import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue' import { setConfAndFields2 } from '@/utils/formCreate' +import { CategoryApi } from '@/api/bpm/category' defineOptions({ name: 'BpmModel' }) @@ -255,6 +262,7 @@ const queryParams = reactive({ category: undefined }) const queryFormRef = ref() // 搜索的表单 +const categoryList = ref([]) // 流程分类列表 /** 查询列表 */ const getList = async () => { @@ -334,6 +342,15 @@ const handleDesign = (row) => { }) } +const handleSimpleDesign = (row) => { + push({ + name: 'SimpleWorkflowDesignEditor', + query: { + modelId: row.id + } + }) +} + /** 发布流程 */ const handleDeploy = async (row) => { try { @@ -347,16 +364,6 @@ const handleDeploy = async (row) => { } catch {} } -/** 点击任务分配按钮 */ -const handleAssignRule = (row) => { - push({ - name: 'BpmTaskAssignRuleList', - query: { - modelId: row.id - } - }) -} - /** 跳转到指定流程定义列表 */ const handleDefinitionList = (row) => { push({ @@ -400,7 +407,9 @@ const handleBpmnDetail = async (row) => { } /** 初始化 **/ -onMounted(() => { - getList() +onMounted(async () => { + await getList() + // 查询流程分类列表 + categoryList.value = await CategoryApi.getCategorySimpleList() }) </script> diff --git a/src/views/bpm/oa/leave/create.vue b/src/views/bpm/oa/leave/create.vue index a22392f9..28a15af7 100644 --- a/src/views/bpm/oa/leave/create.vue +++ b/src/views/bpm/oa/leave/create.vue @@ -37,6 +37,36 @@ <el-form-item label="原因" prop="reason"> <el-input v-model="formData.reason" placeholder="请输请假原因" type="textarea" /> </el-form-item> + <el-col v-if="startUserSelectTasks.length > 0"> + <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> <el-form-item> <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> </el-form-item> @@ -46,10 +76,15 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import * as LeaveApi from '@/api/bpm/leave' import { useTagsViewStore } from '@/store/modules/tagsView' +import * as DefinitionApi from '@/api/bpm/definition' +import * as UserApi from '@/api/system/user' defineOptions({ name: 'BpmOALeaveCreate' }) const message = useMessage() // 消息弹窗 +const { delView } = useTagsViewStore() // 视图操作 +const { push, currentRoute } = useRouter() // 路由 + const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formData = ref({ type: undefined, @@ -64,18 +99,34 @@ const formRules = reactive({ endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }] }) const formRef = ref() // 表单 Ref -const { delView } = useTagsViewStore() // 视图操作 -const { push, currentRoute } = useRouter() // 路由 + +// 指定审批人 +const processDefineKey = 'oa_leave' // 流程定义 Key +const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表 +const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据 +const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref +const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules +const userList = ref<any[]>([]) // 用户列表 + /** 提交表单 */ const submitForm = async () => { // 校验表单 if (!formRef) return const valid = await formRef.value.validate() if (!valid) return + // 校验指定审批人 + if (startUserSelectTasks.value?.length > 0) { + await startUserSelectAssigneesFormRef.value.validate() + } + // 提交请求 formLoading.value = true try { - const data = formData.value as unknown as LeaveApi.LeaveVO + const data = { ...formData.value } as unknown as LeaveApi.LeaveVO + // 设置指定审批人 + if (startUserSelectTasks.value?.length > 0) { + data.startUserSelectAssignees = startUserSelectAssignees.value + } await LeaveApi.createLeave(data) message.success('发起成功') // 关闭当前 Tab @@ -85,4 +136,29 @@ const submitForm = async () => { formLoading.value = false } } + +/** 初始化 */ +onMounted(async () => { + const processDefinitionDetail = await DefinitionApi.getProcessDefinition( + undefined, + processDefineKey + ) + if (!processDefinitionDetail) { + message.error('OA 请假的流程模型未配置,请检查!') + return + } + startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks + // 设置指定审批人 + if (startUserSelectTasks.value?.length > 0) { + // 设置校验规则 + for (const userTask of startUserSelectTasks.value) { + startUserSelectAssignees.value[userTask.id] = [] + startUserSelectAssigneesFormRules.value[userTask.id] = [ + { required: true, message: '请选择审批人', trigger: 'blur' } + ] + } + // 加载用户列表 + userList.value = await UserApi.getSimpleUserList() + } +}) </script> diff --git a/src/views/bpm/oa/leave/index.vue b/src/views/bpm/oa/leave/index.vue index f6dac5bc..bd41104a 100644 --- a/src/views/bpm/oa/leave/index.vue +++ b/src/views/bpm/oa/leave/index.vue @@ -1,5 +1,5 @@ <template> - <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + <doc-alert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" /> <ContentWrap> <!-- 搜索工作栏 --> @@ -36,10 +36,15 @@ value-format="YYYY-MM-DD HH:mm:ss" /> </el-form-item> - <el-form-item label="结果" prop="result"> - <el-select v-model="queryParams.result" class="!w-240px" clearable placeholder="请选择结果"> + <el-form-item label="审批结果" prop="result"> + <el-select + v-model="queryParams.result" + class="!w-240px" + clearable + placeholder="请选择审批结果" + > <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)" + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" :key="dict.value" :label="dict.label" :value="dict.value" @@ -78,7 +83,7 @@ <el-table-column align="center" label="申请编号" prop="id" /> <el-table-column align="center" label="状态" prop="result"> <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" /> + <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.result" /> </template> </el-table-column> <el-table-column @@ -166,7 +171,7 @@ const queryParams = reactive({ pageNo: 1, pageSize: 10, type: undefined, - result: undefined, + status: undefined, reason: undefined, createTime: [] }) @@ -221,7 +226,7 @@ const cancelLeave = async (row) => { inputErrorMessage: '取消原因不能为空' }) // 发起取消 - await ProcessInstanceApi.cancelProcessInstance(row.id, value) + await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value) message.success('取消成功') // 刷新列表 await getList() diff --git a/src/views/bpm/processExpression/ProcessExpressionForm.vue b/src/views/bpm/processExpression/ProcessExpressionForm.vue new file mode 100644 index 00000000..acf0667c --- /dev/null +++ b/src/views/bpm/processExpression/ProcessExpressionForm.vue @@ -0,0 +1,114 @@ +<template> + <Dialog :title="dialogTitle" v-model="dialogVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="100px" + v-loading="formLoading" + > + <el-form-item label="名字" prop="name"> + <el-input v-model="formData.name" placeholder="请输入名字" /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="表达式" prop="expression"> + <el-input type="textarea" v-model="formData.expression" placeholder="请输入表达式" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' +import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression' +import { CommonStatusEnum } from '@/utils/constants' + +/** BPM 流程 表单 */ +defineOptions({ name: 'ProcessExpressionForm' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + name: undefined, + status: undefined, + expression: undefined +}) +const formRules = reactive({ + name: [{ required: true, message: '名字不能为空', trigger: 'blur' }], + status: [{ required: true, message: '状态不能为空', trigger: 'blur' }], + expression: [{ required: true, message: '表达式不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + dialogVisible.value = true + dialogTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await ProcessExpressionApi.getProcessExpression(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + await formRef.value.validate() + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as ProcessExpressionVO + if (formType.value === 'create') { + await ProcessExpressionApi.createProcessExpression(data) + message.success(t('common.createSuccess')) + } else { + await ProcessExpressionApi.updateProcessExpression(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + name: undefined, + status: CommonStatusEnum.ENABLE, + expression: undefined + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/bpm/processExpression/index.vue b/src/views/bpm/processExpression/index.vue new file mode 100644 index 00000000..ec2de5ad --- /dev/null +++ b/src/views/bpm/processExpression/index.vue @@ -0,0 +1,182 @@ +<template> + <doc-alert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" /> + + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <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="status"> + <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + /> + </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:process-expression:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="编号" align="center" prop="id" /> + <el-table-column label="名字" align="center" prop="name" /> + <el-table-column label="状态" align="center" prop="status"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column label="表达式" align="center" prop="expression" /> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="180px" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['bpm:process-expression:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['bpm:process-expression:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <ProcessExpressionForm ref="formRef" @success="getList" /> +</template> + +<script setup lang="ts"> +import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression' +import ProcessExpressionForm from './ProcessExpressionForm.vue' + +/** BPM 流程表达式列表 */ +defineOptions({ name: 'BpmProcessExpression' }) + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const list = ref<ProcessExpressionVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: undefined, + status: undefined, + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ProcessExpressionApi.getProcessExpressionPage(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 formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await ProcessExpressionApi.deleteProcessExpression(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/bpm/processInstance/create/index.vue b/src/views/bpm/processInstance/create/index.vue index a10e0208..cc588881 100644 --- a/src/views/bpm/processInstance/create/index.vue +++ b/src/views/bpm/processInstance/create/index.vue @@ -1,35 +1,47 @@ <template> + <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" /> + <!-- 第一步,通过流程定义的列表,选择对应的流程 --> - <ContentWrap v-if="!selectProcessInstance"> - <el-table v-loading="loading" :data="list"> - <el-table-column label="流程名称" align="center" prop="name" /> - <el-table-column label="流程分类" align="center" prop="category"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" /> - </template> - </el-table-column> - <el-table-column label="流程版本" align="center" prop="version"> - <template #default="scope"> - <el-tag>v{{ scope.row.version }}</el-tag> - </template> - </el-table-column> - <el-table-column label="流程描述" align="center" prop="description" /> - <el-table-column label="操作" align="center"> - <template #default="scope"> - <el-button link type="primary" @click="handleSelect(scope.row)"> - <Icon icon="ep:plus" /> 选择 - </el-button> - </template> - </el-table-column> - </el-table> + <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">申请信息【{{ selectProcessInstance.name }}】</span> - <el-button style="float: right" type="primary" @click="selectProcessInstance = undefined"> + <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> @@ -37,9 +49,43 @@ <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> <!-- 流程图预览 --> @@ -47,59 +93,127 @@ </ContentWrap> </template> <script lang="ts" setup> -import { DICT_TYPE } from '@/utils/dict' import * as DefinitionApi from '@/api/bpm/definition' import * as ProcessInstanceApi from '@/api/bpm/processInstance' import { 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 router = useRouter() // 路由 +const route = useRoute() // 路由 +const { push, currentRoute } = useRouter() // 路由 const message = useMessage() // 消息 +const { delView } = useTagsViewStore() // 视图操作 -// ========== 列表相关 ========== -const loading = ref(true) // 列表的加载中 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ - suspensionState: 1 -}) +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 { - list.value = await DefinitionApi.getProcessDefinitionList(queryParams) + // 流程分类 + 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 bpmnXML = ref(null) // BPMN 数据 const fApi = ref<ApiAttrs>() const detailForm = ref({ - // 流程表单详情 rule: [], - option: {} -}) -const selectProcessInstance = ref() // 选择的流程实例 + 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) => { +const handleSelect = async (row, formVariables) => { // 设置选择的流程 - selectProcessInstance.value = row + selectProcessDefinition.value = row + + // 重置指定审批人 + startUserSelectTasks.value = [] + startUserSelectAssignees.value = {} + startUserSelectAssigneesFormRules.value = {} // 情况一:流程表单 if (row.formType == 10) { // 设置表单 - setConfAndFields2(detailForm, row.formConf, row.formFields) + setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) // 加载流程图 - bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id) + 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 router.push({ + await push({ path: row.formCustomCreatePath }) // 这里暂时无需加载流程图,因为跳出到另外个 Tab; @@ -108,19 +222,29 @@ const handleSelect = async (row) => { /** 提交按钮 */ const submitForm = async (formData) => { - if (!fApi.value || !selectProcessInstance.value) { + 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: selectProcessInstance.value.id, - variables: formData + processDefinitionId: selectProcessDefinition.value.id, + variables: formData, + startUserSelectAssignees: startUserSelectAssignees.value }) // 提示 message.success('发起流程成功') - router.go(-1) + // 跳转回去 + delView(unref(currentRoute)) + await push({ + name: 'BpmProcessInstanceMy' + }) } finally { fApi.value.btn.loading(false) } diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue index 0a2057dd..8912593a 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue @@ -33,21 +33,18 @@ const bpmnControlForm = ref({ prefix: 'flowable' }) const activityList = ref([]) // 任务列表 -// const bpmnXML = computed(() => { // TODO 芋艿:不晓得为啊哈不能这么搞 -// if (!props.processInstance || !props.processInstance.processDefinition) { -// return -// } -// return DefinitionApi.getProcessDefinitionBpmnXML(props.processInstance.processDefinition.id) -// }) -/** 初始化 */ -onMounted(async () => { - if (props.id) { - activityList.value = await ActivityApi.getActivityList({ - processInstanceId: props.id - }) +/** 只有 loading 完成时,才去加载流程列表 */ +watch( + () => props.loading, + async (value) => { + if (value && props.id) { + activityList.value = await ActivityApi.getActivityList({ + processInstanceId: props.id + }) + } } -}) +) </script> <style> .box-card { diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue deleted file mode 100644 index 363874cf..00000000 --- a/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue +++ /dev/null @@ -1,96 +0,0 @@ -<template> - <el-drawer v-model="drawerVisible" title="子任务" size="70%"> - <!-- 当前任务 --> - <template #header> - <h4>【{{ baseTask.name }} 】审批人:{{ baseTask.assigneeUser?.nickname }}</h4> - <el-button - style="margin-left: 5px" - v-if="isSubSignButtonVisible(baseTask)" - type="danger" - plain - @click="handleSubSign(baseTask)" - > - <Icon icon="ep:remove" /> 减签 - </el-button> - </template> - <!-- 子任务列表 --> - <el-table :data="baseTask.children" style="width: 100%" row-key="id" border> - <el-table-column prop="assigneeUser.nickname" label="审批人" /> - <el-table-column prop="assigneeUser.deptName" label="所在部门" /> - <el-table-column label="审批状态" prop="result"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" /> - </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"> - <template #default="scope"> - <el-button - v-if="isSubSignButtonVisible(scope.row)" - type="danger" - plain - @click="handleSubSign(scope.row)" - > - <Icon icon="ep:remove" /> 减签 - </el-button> - </template> - </el-table-column> - </el-table> - <!-- 减签 --> - <TaskSubSignDialogForm ref="taskSubSignDialogForm" /> - </el-drawer> -</template> -<script lang="ts" setup> -import { isEmpty } from '@/utils/is' -import { DICT_TYPE } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' -import TaskSubSignDialogForm from './TaskSubSignDialogForm.vue' - -defineOptions({ name: 'ProcessInstanceChildrenTaskList' }) - -const message = useMessage() // 消息弹窗 -const drawerVisible = ref(false) // 抽屉的是否展示 - -const baseTask = ref<object>({}) -/** 打开弹窗 */ -const open = async (task: any) => { - if (isEmpty(task.children)) { - message.warning('该任务没有子任务') - return - } - baseTask.value = task - // 展开抽屉 - drawerVisible.value = true -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 发起减签 */ -const taskSubSignDialogForm = ref() -const handleSubSign = (item) => { - taskSubSignDialogForm.value.open(item.id) - // TODO @海洋:减签后,需要刷新下界面哈 -} - -/** 是否显示减签按钮 */ -const isSubSignButtonVisible = (task: any) => { - if (task && task.children && !isEmpty(task.children)) { - // 有子任务,且子任务有任意一个是 待处理 和 待前置任务完成 则显示减签按钮 - const subTask = task.children.find((item) => item.result === 1 || item.result === 9) - return !isEmpty(subTask) - } - return false -} -</script> diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue index 97287e99..f82e8003 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue @@ -3,25 +3,44 @@ <template #header> <span class="el-icon-picture-outline">审批记录</span> </template> - <el-col :offset="4" :span="16"> + <el-col :offset="3" :span="17"> <div class="block"> <el-timeline> <el-timeline-item - v-for="(item, index) in tasks" - :key="index" - :icon="getTimelineItemIcon(item)" - :type="getTimelineItemType(item)" + v-if="processInstance.endTime" + :type="getProcessInstanceTimelineItemType(processInstance)" > <p style="font-weight: 700"> - 任务:{{ item.name }} - <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="item.result" /> + 结束流程:在 {{ formatDate(processInstance?.endTime) }} 结束 + <dict-tag + :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" + :value="processInstance.status" + /> + </p> + </el-timeline-item> + <el-timeline-item + v-for="(item, index) in tasks" + :key="index" + :type="getTaskTimelineItemType(item)" + > + <p style="font-weight: 700"> + 审批任务:{{ item.name }} + <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" /> <el-button - style="margin-left: 5px" + class="ml-10px" v-if="!isEmpty(item.children)" @click="openChildrenTask(item)" + size="small" > - <Icon icon="ep:memo" /> - 子任务 + <Icon icon="ep:memo" /> 子任务 + </el-button> + <el-button + class="ml-10px" + size="small" + v-if="item.formId > 0" + @click="handleFormDetail(item)" + > + <Icon icon="ep:document" /> 查看表单 </el-button> </p> <el-card :body-style="{ padding: '10px' }"> @@ -45,84 +64,112 @@ <label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c"> {{ formatPast2(item?.durationInMillis) }} </label> - <p v-if="item.reason"> - <el-tag :type="getTimelineItemType(item)">{{ item.reason }}</el-tag> - </p> + <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> - <!-- 子任务 --> - <ProcessInstanceChildrenTaskList ref="processInstanceChildrenTaskList" /> </el-card> + + <!-- 弹窗:子任务 --> + <TaskSignList ref="taskSignListRef" @success="refresh" /> + <!-- 弹窗:表单 --> + <Dialog title="表单详情" v-model="taskFormVisible" width="600"> + <form-create + ref="fApi" + v-model="taskForm.value" + :option="taskForm.option" + :rule="taskForm.rule" + /> + </Dialog> </template> <script lang="ts" setup> import { formatDate, formatPast2 } from '@/utils/formatTime' import { propTypes } from '@/utils/propTypes' import { DICT_TYPE } from '@/utils/dict' import { isEmpty } from '@/utils/is' -import ProcessInstanceChildrenTaskList from './ProcessInstanceChildrenTaskList.vue' +import TaskSignList from './dialog/TaskSignList.vue' +import type { ApiAttrs } from '@form-create/element-ui/types/config' +import { setConfAndFields2 } from '@/utils/formCreate' defineOptions({ name: 'BpmProcessInstanceTaskList' }) defineProps({ loading: propTypes.bool, // 是否加载中 + processInstance: propTypes.object, // 流程实例 tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组 }) -/** 获得任务对应的 icon */ -const getTimelineItemIcon = (item) => { - if (item.result === 1) { - return 'el-icon-time' +/** 获得流程实例对应的颜色 */ +const getProcessInstanceTimelineItemType = (item: any) => { + if (item.status === 2) { + return 'success' } - if (item.result === 2) { - return 'el-icon-check' + if (item.status === 3) { + return 'danger' } - if (item.result === 3) { - return 'el-icon-close' - } - if (item.result === 4) { - return 'el-icon-remove-outline' - } - if (item.result === 5) { - return 'el-icon-back' + if (item.status === 4) { + return 'warning' } return '' } /** 获得任务对应的颜色 */ -const getTimelineItemType = (item) => { - if (item.result === 1) { +const getTaskTimelineItemType = (item: any) => { + if ([0, 1, 6, 7].includes(item.status)) { return 'primary' } - if (item.result === 2) { + if (item.status === 2) { return 'success' } - if (item.result === 3) { + if (item.status === 3) { return 'danger' } - if (item.result === 4) { + if (item.status === 4) { return 'info' } - if (item.result === 5) { - return 'warning' - } - if (item.result === 6) { - return 'default' - } - if (item.result === 7 || item.result === 8) { + if (item.status === 5) { return 'warning' } return '' } -/** - * 子任务 - */ -const processInstanceChildrenTaskList = ref() +/** 子任务 */ +const taskSignListRef = ref() +const openChildrenTask = (item: any) => { + taskSignListRef.value.open(item) +} -const openChildrenTask = (item) => { - processInstanceChildrenTaskList.value.open(item) +/** 查看表单 */ +const fApi = ref<ApiAttrs>() // form-create 的 API 操作类 +const taskForm = ref({ + rule: [], + option: {}, + value: {} +}) // 流程任务的表单详情 +const taskFormVisible = ref(false) +const handleFormDetail = async (row) => { + // 设置表单 + setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables) + // 弹窗打开 + taskFormVisible.value = true + // 隐藏提交、重置按钮,设置禁用只读 + await nextTick() + fApi.value.fapi.btn.show(false) + fApi.value?.fapi?.resetBtn.show(false) + fApi.value?.fapi?.disabled(true) +} + +/** 刷新数据 */ +const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调 +const refresh = () => { + emit('refresh') } </script> diff --git a/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue b/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue deleted file mode 100644 index bdfecadb..00000000 --- a/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue +++ /dev/null @@ -1,242 +0,0 @@ -<!-- TODO @kyle:需要在讨论下;可能直接选人更合适 --> -<template> - <Dialog v-model="dialogVisible" title="修改任务规则" width="600"> - <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px"> - <el-form-item label="任务名称" prop="taskName"> - <el-input v-model="formData.taskName" disabled placeholder="请输入任务名称" /> - </el-form-item> - <el-form-item label="任务标识" prop="taskKey"> - <el-input v-model="formData.taskKey" disabled placeholder="请输入任务标识" /> - </el-form-item> - <el-form-item label="流程名称" prop="processInstanceName"> - <el-input v-model="formData.processInstanceName" disabled placeholder="请输入流程名称" /> - </el-form-item> - <el-form-item label="流程标识" prop="processInstanceKey"> - <el-input v-model="formData.processInstanceKey" disabled placeholder="请输入流程标识" /> - </el-form-item> - <el-form-item label="规则类型" prop="type"> - <el-select v-model="formData.type" clearable style="width: 100%"> - <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds"> - <el-select v-model="formData.roleIds" clearable multiple style="width: 100%"> - <el-option - v-for="item in roleOptions" - :key="item.id" - :label="item.name" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item - v-if="formData.type === 20 || formData.type === 21" - label="指定部门" - prop="deptIds" - span="24" - > - <el-tree-select - ref="treeRef" - v-model="formData.deptIds" - :data="deptTreeOptions" - :props="defaultProps" - empty-text="加载中,请稍后" - multiple - node-key="id" - show-checkbox - /> - </el-form-item> - <el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24"> - <el-select v-model="formData.postIds" clearable multiple style="width: 100%"> - <el-option - v-for="item in postOptions" - :key="parseInt(item.id)" - :label="item.name" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item - v-if="formData.type === 30 || formData.type === 31 || formData.type === 32" - label="指定用户" - prop="userIds" - span="24" - > - <el-select v-model="formData.userIds" clearable multiple style="width: 100%"> - <el-option - v-for="item in userOptions" - :key="parseInt(item.id)" - :label="item.nickname" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds"> - <el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%"> - <el-option - v-for="item in userGroupOptions" - :key="parseInt(item.id)" - :label="item.name" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts"> - <el-select v-model="formData.scripts" clearable multiple style="width: 100%"> - <el-option - v-for="dict in taskAssignScriptDictDatas" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="抄送原因" prop="reason"> - <el-input v-model="formData.reason" placeholder="请输入抄送原因" type="textarea" /> - </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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { defaultProps, handleTree } from '@/utils/tree' -import * as ProcessInstanceApi from '@/api/bpm/processInstance' -import * as RoleApi from '@/api/system/role' -import * as DeptApi from '@/api/system/dept' -import * as PostApi from '@/api/system/post' -import * as UserApi from '@/api/system/user' -import * as UserGroupApi from '@/api/bpm/userGroup' - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formData = ref({ - type: Number(undefined), - taskName: '', - taskKey: '', - processInstanceName: '', - processInstanceKey: '', - startUserId: '', - options: [], - roleIds: [], - deptIds: [], - postIds: [], - userIds: [], - userGroupIds: [], - scripts: [], - reason: '' -}) -const formRules = reactive({ - type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }], - roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }], - deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }], - postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }], - userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }], - userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }], - scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }], - reason: [{ required: true, message: '抄送原因不能为空', trigger: 'change' }] -}) -const formRef = ref() // 表单 Ref -const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 -const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 -const deptTreeOptions = ref() // 部门树 -const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表 -const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 -const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 -const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT) - -/** 打开弹窗 */ -const open = async (row) => { - // 1. 先重置表单 - resetForm() - // 2. 再设置表单 - if (row != null) { - formData.value.type = undefined as unknown as number - formData.value.taskName = row.name - formData.value.taskKey = row.id - formData.value.processInstanceName = row.processInstance.name - formData.value.processInstanceKey = row.processInstance.id - formData.value.startUserId = row.processInstance.startUserId - } - // 打开弹窗 - dialogVisible.value = true - - // 获得角色列表 - roleOptions.value = await RoleApi.getSimpleRoleList() - // 获得部门列表 - deptOptions.value = await DeptApi.getSimpleDeptList() - deptTreeOptions.value = handleTree(deptOptions.value, 'id') - // 获得岗位列表 - postOptions.value = await PostApi.getSimplePostList() - // 获得用户列表 - userOptions.value = await UserApi.getSimpleUserList() - // 获得用户组列表 - userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList() -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - - // 构建表单 - const form = { - ...formData.value - } - // 将 roleIds 等选项赋值到 options 中 - if (form.type === 10) { - form.options = form.roleIds - } else if (form.type === 20 || form.type === 21) { - form.options = form.deptIds - } else if (form.type === 22) { - form.options = form.postIds - } else if (form.type === 30 || form.type === 31 || form.type === 32) { - form.options = form.userIds - } else if (form.type === 40) { - form.options = form.userGroupIds - } else if (form.type === 50) { - form.options = form.scripts - } - form.roleIds = undefined - form.deptIds = undefined - form.postIds = undefined - form.userIds = undefined - form.userGroupIds = undefined - form.scripts = undefined - - // 提交请求 - formLoading.value = true - try { - const data = form as unknown as ProcessInstanceApi.ProcessInstanceCCVO - await ProcessInstanceApi.createProcessInstanceCC(data) - console.log(data) - message.success(t('common.createSuccess')) - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/TaskDelegateForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue similarity index 92% rename from src/views/bpm/processInstance/detail/TaskDelegateForm.vue rename to src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue index dc757a0c..178b1b97 100644 --- a/src/views/bpm/processInstance/detail/TaskDelegateForm.vue +++ b/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue @@ -37,10 +37,12 @@ const dialogVisible = ref(false) // 弹窗的是否展示 const formLoading = ref(false) // 表单的加载中 const formData = ref({ id: '', - delegateUserId: undefined + delegateUserId: undefined, + reason: '' }) const formRules = ref({ - delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }] + delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }], + reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref @@ -79,7 +81,8 @@ const submitForm = async () => { const resetForm = () => { formData.value = { id: '', - delegateUserId: undefined + delegateUserId: undefined, + reason: '' } formRef.value?.resetFields() } diff --git a/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue similarity index 78% rename from src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue rename to src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue index f93bf2c5..a1391697 100644 --- a/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue +++ b/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue @@ -1,5 +1,5 @@ <template> - <Dialog v-model="dialogVisible" title="回退" width="500"> + <Dialog v-model="dialogVisible" title="回退任务" width="500"> <el-form ref="formRef" v-loading="formLoading" @@ -7,13 +7,13 @@ :rules="formRules" label-width="110px" > - <el-form-item label="退回节点" prop="targetDefinitionKey"> - <el-select v-model="formData.targetDefinitionKey" clearable style="width: 100%"> + <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.definitionKey" + :key="item.taskDefinitionKey" :label="item.name" - :value="item.definitionKey" + :value="item.taskDefinitionKey" /> </el-select> </el-form-item> @@ -35,19 +35,19 @@ const dialogVisible = ref(false) // 弹窗的是否展示 const formLoading = ref(false) // 表单的加载中 const formData = ref({ id: '', - targetDefinitionKey: undefined, + targetTaskDefinitionKey: undefined, reason: '' }) const formRules = ref({ - targetDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }], + targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }], reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref -const returnList = ref([]) +const returnList = ref([] as any) /** 打开弹窗 */ const open = async (id: string) => { - returnList.value = await TaskApi.getReturnList({ taskId: id }) + returnList.value = await TaskApi.getTaskListByReturn(id) if (returnList.value.length === 0) { message.warning('当前没有可回退的节点') return false @@ -82,7 +82,7 @@ const submitForm = async () => { const resetForm = () => { formData.value = { id: '', - targetDefinitionKey: undefined, + targetTaskDefinitionKey: undefined, reason: '' } formRef.value?.resetFields() diff --git a/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue similarity index 85% rename from src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue rename to src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue index 40cd200e..9e4998c1 100644 --- a/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue +++ b/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue @@ -7,8 +7,8 @@ :rules="formRules" label-width="110px" > - <el-form-item label="加签处理人" prop="userIdList"> - <el-select v-model="formData.userIdList" multiple clearable style="width: 100%"> + <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" @@ -36,18 +36,19 @@ import * as TaskApi from '@/api/bpm/task' import * as UserApi from '@/api/system/user' -const message = useMessage() // 消息弹窗 -defineOptions({ name: 'BpmTaskUpdateAssigneeForm' }) +defineOptions({ name: 'TaskSignCreateForm' }) +const message = useMessage() // 消息弹窗 const dialogVisible = ref(false) // 弹窗的是否展示 const formLoading = ref(false) // 表单的加载中 const formData = ref({ id: '', - userIdList: [], - type: '' + userIds: [], + type: '', + reason: '' }) const formRules = ref({ - userIdList: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }], + userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }], reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }] }) @@ -75,7 +76,7 @@ const submitForm = async (type: string) => { formLoading.value = true formData.value.type = type try { - await TaskApi.taskAddSign(formData.value) + await TaskApi.signCreateTask(formData.value) message.success('加签成功') dialogVisible.value = false // 发送操作成功的事件 @@ -89,8 +90,9 @@ const submitForm = async (type: string) => { const resetForm = () => { formData.value = { id: '', - userIdList: [], - type: '' + userIds: [], + type: '', + reason: '' } formRef.value?.resetFields() } diff --git a/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue similarity index 80% rename from src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue rename to src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue index 61f7d68c..19bb2dce 100644 --- a/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue +++ b/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue @@ -9,8 +9,10 @@ > <el-form-item label="减签任务" prop="id"> <el-radio-group v-model="formData.id"> - <el-radio-button v-for="item in subTaskList" :key="item.id" :label="item.id"> - {{ item.name }}({{ item.assigneeUser.deptName }}{{ item.assigneeUser.nickname }}--审批) + <el-radio-button v-for="item in childrenTaskList" :key="item.id" :label="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> @@ -24,10 +26,12 @@ </template> </Dialog> </template> -<script lang="ts" name="TaskRollbackDialogForm" setup> +<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) // 表单的加载中 @@ -41,11 +45,11 @@ const formRules = ref({ }) const formRef = ref() // 表单 Ref -const subTaskList = ref([]) +const childrenTaskList = ref([]) /** 打开弹窗 */ const open = async (id: string) => { - subTaskList.value = await TaskApi.getChildrenTaskList(id) - if (isEmpty(subTaskList.value)) { + childrenTaskList.value = await TaskApi.getChildrenTaskList(id) + if (isEmpty(childrenTaskList.value)) { message.warning('当前没有可减签的任务') return false } @@ -64,7 +68,7 @@ const submitForm = async () => { // 提交请求 formLoading.value = true try { - await TaskApi.taskSubSign(formData.value) + await TaskApi.signDeleteTask(formData.value) message.success('减签成功') dialogVisible.value = false // 发送操作成功的事件 diff --git a/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue new file mode 100644 index 00000000..648e86b5 --- /dev/null +++ b/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue @@ -0,0 +1,106 @@ +<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> diff --git a/src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue similarity index 79% rename from src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue rename to src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue index 6adf1de8..c1012ac9 100644 --- a/src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue +++ b/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue @@ -1,5 +1,5 @@ <template> - <Dialog v-model="dialogVisible" title="转派审批人" width="500"> + <Dialog v-model="dialogVisible" title="转派任务" width="500"> <el-form ref="formRef" v-loading="formLoading" @@ -17,6 +17,9 @@ /> </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> @@ -28,16 +31,18 @@ import * as TaskApi from '@/api/bpm/task' import * as UserApi from '@/api/system/user' -defineOptions({ name: 'BpmTaskUpdateAssigneeForm' }) +defineOptions({ name: 'TaskTransferForm' }) const dialogVisible = ref(false) // 弹窗的是否展示 const formLoading = ref(false) // 表单的加载中 const formData = ref({ id: '', - assigneeUserId: undefined + assigneeUserId: undefined, + reason: '' }) const formRules = ref({ - assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }] + assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }], + reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref @@ -63,7 +68,7 @@ const submitForm = async () => { // 提交请求 formLoading.value = true try { - await TaskApi.updateTaskAssignee(formData.value) + await TaskApi.transferTask(formData.value) dialogVisible.value = false // 发送操作成功的事件 emit('success') @@ -76,7 +81,8 @@ const submitForm = async () => { const resetForm = () => { formData.value = { id: '', - assigneeUserId: undefined + assigneeUserId: undefined, + reason: '' } formRef.value?.resetFields() } diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue index 074d1329..ef260dee 100644 --- a/src/views/bpm/processInstance/detail/index.vue +++ b/src/views/bpm/processInstance/detail/index.vue @@ -21,9 +21,22 @@ {{ 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> + {{ processInstance?.startUser.nickname }} + <el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag> </el-form-item> + <el-card class="mb-15px !-mt-10px" v-if="runningTasks[index].formId > 0"> + <template #header> + <span class="el-icon-picture-outline"> + 填写表单【{{ runningTasks[index]?.formName }}】 + </span> + </template> + <form-create + v-model:api="approveFormFApis[index]" + v-model="approveForms[index].value" + :option="approveForms[index].option" + :rule="approveForms[index].rule" + /> + </el-card> <el-form-item label="审批建议" prop="reason"> <el-input v-model="auditForms[index].reason" @@ -31,6 +44,16 @@ type="textarea" /> </el-form-item> + <el-form-item label="抄送人" prop="copyUserIds"> + <el-select v-model="auditForms[index].copyUserIds" 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> <div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px"> <el-button type="success" @click="handleAudit(item, true)"> @@ -82,25 +105,30 @@ </el-card> <!-- 审批记录 --> - <ProcessInstanceTaskList :loading="tasksLoad" :tasks="tasks" /> + <ProcessInstanceTaskList + :loading="tasksLoad" + :process-instance="processInstance" + :tasks="tasks" + @refresh="getTaskList" + /> <!-- 高亮流程图 --> <ProcessInstanceBpmnViewer :id="`${id}`" - :bpmn-xml="bpmnXML" + :bpmn-xml="bpmnXml" :loading="processInstanceLoading" :process-instance="processInstance" :tasks="tasks" /> <!-- 弹窗:转派审批人 --> - <TaskUpdateAssigneeForm ref="taskUpdateAssigneeFormRef" @success="getDetail" /> - <!-- 弹窗,回退节点 --> - <TaskReturnDialog ref="taskReturnDialogRef" @success="getDetail" /> - <!-- 委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中--> + <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" /> + <!-- 弹窗:回退节点 --> + <TaskReturnForm ref="taskReturnFormRef" @success="getDetail" /> + <!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中--> <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" /> - <!-- 加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> - <TaskAddSignDialogForm ref="taskAddSignDialogForm" @success="getDetail" /> + <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> + <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" /> </ContentWrap> </template> <script lang="ts" setup> @@ -110,14 +138,15 @@ 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 TaskUpdateAssigneeForm from './TaskUpdateAssigneeForm.vue' import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue' import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue' -import TaskReturnDialog from './TaskReturnDialogForm.vue' -import TaskDelegateForm from './TaskDelegateForm.vue' -import TaskAddSignDialogForm from './TaskAddSignDialogForm.vue' +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 { registerComponent } from '@/utils/routerHelper' import { isEmpty } from '@/utils/is' +import * as UserApi from '@/api/system/user' defineOptions({ name: 'BpmProcessInstanceDetail' }) @@ -126,10 +155,10 @@ const message = useMessage() // 消息弹窗 const { proxy } = getCurrentInstance() as any const userId = useUserStore().getUser.id // 当前登录的编号 -const id = query.id as unknown as number // 流程实例的编号 +const id = query.id as unknown as string // 流程实例的编号 const processInstanceLoading = ref(false) // 流程实例的加载中 const processInstance = ref<any>({}) // 流程实例 -const bpmnXML = ref('') // BPMN XML +const bpmnXml = ref('') // BPMN XML const tasksLoad = ref(true) // 任务的加载中 const tasks = ref<any[]>([]) // 任务列表 // ========== 审批信息 ========== @@ -138,14 +167,30 @@ const auditForms = ref<any[]>([]) // 审批任务的表单 const auditRule = reactive({ reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }] }) +const approveForms = ref<any[]>([]) // 审批通过时,额外的补充信息 +const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi + // ========== 申请信息 ========== const fApi = ref<ApiAttrs>() // const detailForm = ref({ - // 流程表单详情 rule: [], option: {}, 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) => { @@ -161,9 +206,16 @@ const handleAudit = async (task, pass) => { // 2.1 提交审批 const data = { id: task.id, - reason: auditForms.value[index].reason + 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 + } await TaskApi.approveTask(data) message.success('审批通过成功') } else { @@ -175,28 +227,27 @@ const handleAudit = async (task, pass) => { } /** 转派审批人 */ -const taskUpdateAssigneeFormRef = ref() +const taskTransferFormRef = ref() const openTaskUpdateAssigneeForm = (id: string) => { - taskUpdateAssigneeFormRef.value.open(id) + taskTransferFormRef.value.open(id) } -const taskDelegateForm = ref() /** 处理审批退回的操作 */ +const taskDelegateForm = ref() const handleDelegate = async (task) => { taskDelegateForm.value.open(task.id) } -//回退弹框组件 -const taskReturnDialogRef = ref() /** 处理审批退回的操作 */ -const handleBack = async (task) => { - taskReturnDialogRef.value.open(task.id) +const taskReturnFormRef = ref() +const handleBack = async (task: any) => { + taskReturnFormRef.value.open(task.id) } -const taskAddSignDialogForm = ref() /** 处理审批加签的操作 */ -const handleSign = async (task) => { - taskAddSignDialogForm.value.open(task.id) +const taskSignCreateFormRef = ref() +const handleSign = async (task: any) => { + taskSignCreateFormRef.value.open(task.id) } /** 获得详情 */ @@ -239,7 +290,9 @@ const getProcessInstance = async () => { } // 加载流程图 - bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id as number) + bpmnXml.value = ( + await DefinitionApi.getProcessDefinition(processDefinition.id as number) + )?.bpmnXml } finally { processInstanceLoading.value = false } @@ -247,6 +300,10 @@ const getProcessInstance = async () => { /** 加载任务列表 */ const getTaskList = async () => { + runningTasks.value = [] + auditForms.value = [] + approveForms.value = [] + approveFormFApis.value = [] try { // 获得未取消的任务 tasksLoad.value = true @@ -254,7 +311,7 @@ const getTaskList = async () => { tasks.value = [] // 1.1 移除已取消的审批 data.forEach((task) => { - if (task.result !== 4) { + if (task.status !== 4) { tasks.value.push(task) } }) @@ -274,8 +331,6 @@ const getTaskList = async () => { }) // 获得需要自己审批的任务 - runningTasks.value = [] - auditForms.value = [] loadRunningTask(tasks.value) } finally { tasksLoad.value = false @@ -291,7 +346,7 @@ const loadRunningTask = (tasks) => { loadRunningTask(task.children) } // 2.1 只有待处理才需要 - if (task.result !== 1 && task.result !== 6) { + if (task.status !== 1 && task.status !== 6) { return } // 2.2 自己不是处理人 @@ -301,13 +356,26 @@ const loadRunningTask = (tasks) => { // 2.3 添加到处理任务 runningTasks.value.push({ ...task }) auditForms.value.push({ - reason: '' + reason: '', + copyUserIds: [] }) + + // 2.4 处理 approve 表单 + if (task.formId && task.formConf) { + const approveForm = {} + setConfAndFields2(approveForm, task.formConf, task.formFields, task.formVariable) + approveForms.value.push(approveForm) + } else { + approveForms.value.push({}) // 占位,避免为空 + } }) } /** 初始化 */ -onMounted(() => { +const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 +onMounted(async () => { getDetail() + // 获得用户列表 + userOptions.value = await UserApi.getSimpleUserList() }) </script> diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue index 8b9f8a1a..7ca07f90 100644 --- a/src/views/bpm/processInstance/index.vue +++ b/src/views/bpm/processInstance/index.vue @@ -1,5 +1,5 @@ <template> - <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" /> <ContentWrap> <!-- 搜索工作栏 --> @@ -36,15 +36,20 @@ class="!w-240px" > <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)" - :key="dict.value" - :label="dict.label" - :value="dict.value" + v-for="category in categoryList" + :key="category.code" + :label="category.name" + :value="category.code" /> </el-select> </el-form-item> - <el-form-item label="状态" prop="status"> - <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px"> + <el-form-item label="流程状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择流程状态" + clearable + class="!w-240px" + > <el-option v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" :key="dict.value" @@ -53,17 +58,7 @@ /> </el-select> </el-form-item> - <el-form-item label="结果" prop="result"> - <el-select v-model="queryParams.result" placeholder="请选择结果" clearable class="!w-240px"> - <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="提交时间" prop="createTime"> + <el-form-item label="发起时间" prop="createTime"> <el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" @@ -81,7 +76,7 @@ type="primary" plain v-hasPermi="['bpm:process-instance:query']" - @click="handleCreate" + @click="handleCreate()" > <Icon icon="ep:plus" class="mr-5px" /> 发起流程 </el-button> @@ -92,34 +87,23 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="流程编号" align="center" prop="id" width="300px" /> - <el-table-column label="流程名称" align="center" prop="name" /> - <el-table-column label="流程分类" align="center" prop="category"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" /> - </template> - </el-table-column> - <el-table-column label="当前审批任务" align="center" prop="tasks"> - <template #default="scope"> - <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link> - <span>{{ task.name }}</span> - </el-button> - </template> - </el-table-column> - <el-table-column label="状态" prop="status"> + <el-table-column label="流程名称" align="center" prop="name" min-width="200px" fixed="left" /> + <el-table-column + label="流程分类" + align="center" + prop="categoryName" + min-width="100" + fixed="left" + /> + <el-table-column label="流程状态" prop="status" width="120"> <template #default="scope"> <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" /> </template> </el-table-column> - <el-table-column label="结果" prop="result"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" /> - </template> - </el-table-column> <el-table-column - label="提交时间" + label="发起时间" align="center" - prop="createTime" + prop="startTime" width="180" :formatter="dateFormatter" /> @@ -130,7 +114,20 @@ width="180" :formatter="dateFormatter" /> - <el-table-column label="操作" align="center"> + <el-table-column align="center" label="耗时" prop="durationInMillis" width="160"> + <template #default="scope"> + {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }} + </template> + </el-table-column> + <el-table-column label="当前审批任务" align="center" prop="tasks" min-width="120px"> + <template #default="scope"> + <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link> + <span>{{ task.name }}</span> + </el-button> + </template> + </el-table-column> + <el-table-column label="流程编号" align="center" prop="id" min-width="320px" /> + <el-table-column label="操作" align="center" fixed="right" width="180"> <template #default="scope"> <el-button link @@ -143,12 +140,15 @@ <el-button link type="primary" - v-if="scope.row.result === 1" + v-if="scope.row.status === 1" v-hasPermi="['bpm:process-instance:query']" @click="handleCancel(scope.row)" > 取消 </el-button> + <el-button link type="primary" v-else @click="handleCreate(scope.row.id)"> + 重新发起 + </el-button> </template> </el-table-column> </el-table> @@ -163,11 +163,12 @@ </template> <script lang="ts" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' import { ElMessageBox } from 'element-plus' import * as ProcessInstanceApi from '@/api/bpm/processInstance' +import { CategoryApi } from '@/api/bpm/category' -defineOptions({ name: 'BpmProcessInstance' }) +defineOptions({ name: 'BpmProcessInstanceMy' }) const router = useRouter() // 路由 const message = useMessage() // 消息弹窗 @@ -183,16 +184,16 @@ const queryParams = reactive({ processDefinitionId: undefined, category: undefined, status: undefined, - result: undefined, createTime: [] }) const queryFormRef = ref() // 搜索的表单 +const categoryList = ref([]) // 流程分类列表 /** 查询列表 */ const getList = async () => { loading.value = true try { - const data = await ProcessInstanceApi.getMyProcessInstancePage(queryParams) + const data = await ProcessInstanceApi.getProcessInstanceMyPage(queryParams) list.value = data.list total.value = data.total } finally { @@ -213,9 +214,10 @@ const resetQuery = () => { } /** 发起流程操作 **/ -const handleCreate = () => { +const handleCreate = (id) => { router.push({ - name: 'BpmProcessInstanceCreate' + name: 'BpmProcessInstanceCreate', + query: { processInstanceId: id } }) } @@ -239,14 +241,20 @@ const handleCancel = async (row) => { inputErrorMessage: '取消原因不能为空' }) // 发起取消 - await ProcessInstanceApi.cancelProcessInstance(row.id, value) + await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value) message.success('取消成功') // 刷新列表 await getList() } -/** 初始化 **/ -onMounted(() => { +/** 激活时 **/ +onActivated(() => { getList() }) + +/** 初始化 **/ +onMounted(async () => { + await getList() + categoryList.value = await CategoryApi.getCategorySimpleList() +}) </script> diff --git a/src/views/bpm/processInstance/manager/index.vue b/src/views/bpm/processInstance/manager/index.vue new file mode 100644 index 00000000..ab8da9c9 --- /dev/null +++ b/src/views/bpm/processInstance/manager/index.vue @@ -0,0 +1,255 @@ +<template> + <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="发起人" prop="startUserId"> + <el-select v-model="queryParams.startUserId" placeholder="请选择发起人" class="!w-240px"> + <el-option + v-for="user in userList" + :key="user.id" + :label="user.nickname" + :value="user.id" + /> + </el-select> + </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="processDefinitionId"> + <el-input + v-model="queryParams.processDefinitionId" + 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 label="流程状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择流程状态" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="发起时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="daterange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-220px" + /> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="流程名称" align="center" prop="name" min-width="200px" fixed="left" /> + <el-table-column + label="流程分类" + align="center" + prop="categoryName" + min-width="100" + fixed="left" + /> + <el-table-column label="流程发起人" align="center" prop="startUser.nickname" width="120" /> + <el-table-column label="发起部门" align="center" prop="startUser.deptName" width="120" /> + <el-table-column label="流程状态" prop="status" width="120"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="发起时间" + align="center" + prop="startTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column + label="结束时间" + align="center" + prop="endTime" + width="180" + :formatter="dateFormatter" + /> + <el-table-column align="center" label="耗时" prop="durationInMillis" width="169"> + <template #default="scope"> + {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }} + </template> + </el-table-column> + <el-table-column label="当前审批任务" align="center" prop="tasks" min-width="120px"> + <template #default="scope"> + <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link> + <span>{{ task.name }}</span> + </el-button> + </template> + </el-table-column> + <el-table-column label="流程编号" align="center" prop="id" min-width="320px" /> + <el-table-column label="操作" align="center" fixed="right" width="180"> + <template #default="scope"> + <el-button + link + type="primary" + v-hasPermi="['bpm:process-instance:cancel']" + @click="handleDetail(scope.row)" + > + 详情 + </el-button> + <el-button + link + type="primary" + v-if="scope.row.status === 1" + v-hasPermi="['bpm:process-instance:query']" + @click="handleCancel(scope.row)" + > + 取消 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> +</template> +<script lang="ts" setup> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' +import { ElMessageBox } from 'element-plus' +import * as ProcessInstanceApi from '@/api/bpm/processInstance' +import { CategoryApi } from '@/api/bpm/category' +import * as UserApi from '@/api/system/user' +import { cancelProcessInstanceByAdmin } from '@/api/bpm/processInstance' + +// 它和【我的流程】的差异是,该菜单可以看全部的流程实例 +defineOptions({ name: 'BpmProcessInstanceManager' }) + +const router = useRouter() // 路由 +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + startUserId: undefined, + name: '', + processDefinitionId: undefined, + category: undefined, + status: undefined, + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 +const categoryList = ref([]) // 流程分类列表 +const userList = ref<any[]>([]) // 用户列表 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ProcessInstanceApi.getProcessInstanceManagerPage(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 handleDetail = (row) => { + router.push({ + name: 'BpmProcessInstanceDetail', + query: { + id: row.id + } + }) +} + +/** 取消按钮操作 */ +const handleCancel = async (row) => { + // 二次确认 + const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格 + inputErrorMessage: '取消原因不能为空' + }) + // 发起取消 + await ProcessInstanceApi.cancelProcessInstanceByAdmin(row.id, value) + message.success('取消成功') + // 刷新列表 + await getList() +} + +/** 激活时 **/ +onActivated(() => { + getList() +}) + +/** 初始化 **/ +onMounted(async () => { + await getList() + categoryList.value = await CategoryApi.getCategorySimpleList() + userList.value = await UserApi.getSimpleUserList() +}) +</script> diff --git a/src/views/bpm/processListener/ProcessListenerForm.vue b/src/views/bpm/processListener/ProcessListenerForm.vue new file mode 100644 index 00000000..8d4e9796 --- /dev/null +++ b/src/views/bpm/processListener/ProcessListenerForm.vue @@ -0,0 +1,162 @@ +<template> + <Dialog :title="dialogTitle" v-model="dialogVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="110px" + v-loading="formLoading" + > + <el-form-item label="名字" prop="name"> + <el-input v-model="formData.name" placeholder="请输入名字" /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-radio-group v-model="formData.status"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="类型" prop="type"> + <el-select + v-model="formData.type" + placeholder="请选择类型" + @change="formData.event = undefined" + > + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="事件" prop="event"> + <el-select v-model="formData.event" placeholder="请选择事件"> + <el-option + v-for="event in formData.type == 'execution' + ? ['start', 'end'] + : ['create', 'assignment', 'complete', 'delete', 'update', 'timeout']" + :label="event" + :value="event" + :key="event" + /> + </el-select> + </el-form-item> + <el-form-item label="值类型" prop="valueType"> + <el-select v-model="formData.valueType" placeholder="请选择值类型"> + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="类路径" prop="value" v-if="formData.type == 'class'"> + <el-input v-model="formData.value" placeholder="请输入类路径" /> + </el-form-item> + <el-form-item label="表达式" prop="value" v-else> + <el-input v-model="formData.value" placeholder="请输入表达式" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { getIntDictOptions, getStrDictOptions, DICT_TYPE } from '@/utils/dict' +import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener' +import { CommonStatusEnum } from '@/utils/constants' + +/** BPM 流程 表单 */ +defineOptions({ name: 'ProcessListenerForm' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + name: undefined, + type: undefined, + status: undefined, + event: undefined, + valueType: undefined, + value: undefined +}) +const formRules = reactive({ + name: [{ required: true, message: '名字不能为空', trigger: 'blur' }], + type: [{ required: true, message: '类型不能为空', trigger: 'change' }], + status: [{ required: true, message: '状态不能为空', trigger: 'blur' }], + event: [{ required: true, message: '监听事件不能为空', trigger: 'blur' }], + valueType: [{ required: true, message: '值类型不能为空', trigger: 'change' }], + value: [{ required: true, message: '值不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + dialogVisible.value = true + dialogTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await ProcessListenerApi.getProcessListener(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + await formRef.value.validate() + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as ProcessListenerVO + if (formType.value === 'create') { + await ProcessListenerApi.createProcessListener(data) + message.success(t('common.createSuccess')) + } else { + await ProcessListenerApi.updateProcessListener(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + name: undefined, + type: undefined, + status: CommonStatusEnum.ENABLE, + event: undefined, + valueType: undefined, + value: undefined + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/bpm/processListener/index.vue b/src/views/bpm/processListener/index.vue new file mode 100644 index 00000000..8b5c36e7 --- /dev/null +++ b/src/views/bpm/processListener/index.vue @@ -0,0 +1,185 @@ +<template> + <doc-alert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" /> + + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="85px" + > + <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="type"> + <el-select v-model="queryParams.type" placeholder="请选择类型" clearable class="!w-240px"> + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </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:process-listener:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="编号" align="center" prop="id" /> + <el-table-column label="名字" align="center" prop="name" /> + <el-table-column label="类型" align="center" prop="type"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" /> + </template> + </el-table-column> + <el-table-column label="状态" align="center" prop="status"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column label="事件" align="center" prop="event" /> + <el-table-column label="值类型" align="center" prop="valueType"> + <template #default="scope"> + <dict-tag + :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE" + :value="scope.row.valueType" + /> + </template> + </el-table-column> + <el-table-column label="值" align="center" prop="value" /> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="180px" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['bpm:process-listener:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['bpm:process-listener:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <ProcessListenerForm ref="formRef" @success="getList" /> +</template> + +<script setup lang="ts"> +import { getStrDictOptions, DICT_TYPE } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener' +import ProcessListenerForm from './ProcessListenerForm.vue' + +/** BPM 流程 列表 */ +defineOptions({ name: 'BpmProcessListener' }) + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const list = ref<ProcessListenerVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: undefined, + type: undefined, + event: undefined +}) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ProcessListenerApi.getProcessListenerPage(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 formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await ProcessListenerApi.deleteProcessListener(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/bpm/simpleWorkflow/index.vue b/src/views/bpm/simpleWorkflow/index.vue new file mode 100644 index 00000000..144615e0 --- /dev/null +++ b/src/views/bpm/simpleWorkflow/index.vue @@ -0,0 +1,28 @@ +<template> + <div> + <section class="dingflow-design"> + <div class="box-scale"> + <nodeWrap v-model:nodeConfig="nodeConfig" /> + <div class="end-node"> + <div class="end-node-circle"></div> + <div class="end-node-text">流程结束</div> + </div> + </div> + </section> + </div> +</template> +<script lang="ts" setup> +import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue' +defineOptions({ name: 'SimpleWorkflowDesignEditor' }) +let nodeConfig = ref({ + nodeName: '发起人', + type: 0, + id: 'root', + formPerms: {}, + nodeUserList: [], + childNode: {} +}) +</script> +<style> +@import url('@/components/SimpleProcessDesigner/theme/workflow.css'); +</style> \ No newline at end of file diff --git a/src/views/bpm/task/cc/index.vue b/src/views/bpm/task/copy/index.vue similarity index 74% rename from src/views/bpm/task/cc/index.vue rename to src/views/bpm/task/copy/index.vue index 50ddf889..adc1fe32 100644 --- a/src/views/bpm/task/cc/index.vue +++ b/src/views/bpm/task/copy/index.vue @@ -1,5 +1,10 @@ <!-- 工作流 - 抄送我的流程 --> <template> + <doc-alert + title="审批转办、委派、抄送" + url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" + /> + <ContentWrap> <!-- 搜索工作栏 --> <el-form ref="queryFormRef" :inline="true" class="-mb-15px" label-width="68px"> @@ -11,14 +16,6 @@ placeholder="请输入流程名称" /> </el-form-item> - <el-form-item label="所属流程" prop="processDefinitionId"> - <el-input - v-model="queryParams.processInstanceId" - placeholder="请输入流程定义的编号" - clearable - class="!w-240px" - /> - </el-form-item> <el-form-item label="抄送时间" prop="createTime"> <el-date-picker v-model="queryParams.createTime" @@ -46,12 +43,17 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column align="center" label="所属流程" prop="processInstanceId" width="300px" /> - <el-table-column align="center" label="流程名称" prop="processInstanceName" /> - <el-table-column align="center" label="任务名称" prop="taskName" /> - <el-table-column align="center" label="流程发起人" prop="startUserNickname" /> - <el-table-column align="center" label="抄送发起人" prop="creatorNickname" /> - <el-table-column align="center" label="抄送原因" prop="reason" /> + <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 + :formatter="dateFormatter" + align="center" + label="流程发起时间" + prop="processInstanceStartTime" + width="180" + /> + <el-table-column align="center" label="抄送任务" prop="taskName" min-width="180" /> + <el-table-column align="center" label="抄送人" prop="creatorName" min-width="100" /> <el-table-column align="center" label="抄送时间" @@ -59,9 +61,9 @@ width="180" :formatter="dateFormatter" /> - <el-table-column align="center" label="操作"> + <el-table-column align="center" label="操作" fixed="right" width="80"> <template #default="scope"> - <el-button link type="primary" @click="handleAudit(scope.row)">跳转待办</el-button> + <el-button link type="primary" @click="handleAudit(scope.row)">详情</el-button> </template> </el-table-column> </el-table> @@ -78,14 +80,14 @@ import { dateFormatter } from '@/utils/formatTime' import * as ProcessInstanceApi from '@/api/bpm/processInstance' -defineOptions({ name: 'BpmCCProcessInstance' }) +defineOptions({ name: 'BpmProcessInstanceCopy' }) const { push } = useRouter() // 路由 const loading = ref(false) // 列表的加载中 const total = ref(0) // 列表的总页数 const list = ref([]) // 列表的数据 -const queryParams = ref({ +const queryParams = reactive({ pageNo: 1, pageSize: 10, processInstanceId: '', @@ -98,7 +100,7 @@ const queryFormRef = ref() // 搜索的表单 const getList = async () => { loading.value = true try { - const data = await ProcessInstanceApi.getProcessInstanceCCPage(queryParams) + const data = await ProcessInstanceApi.getProcessInstanceCopyPage(queryParams) list.value = data.list total.value = data.total } finally { @@ -118,7 +120,7 @@ const handleAudit = (row: any) => { /** 搜索按钮操作 */ const handleQuery = () => { - queryParams.value.pageNo = 1 + queryParams.pageNo = 1 getList() } diff --git a/src/views/bpm/task/done/TaskDetail.vue b/src/views/bpm/task/done/TaskDetail.vue deleted file mode 100644 index 5bc06f19..00000000 --- a/src/views/bpm/task/done/TaskDetail.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情"> - <el-descriptions :column="1" border> - <el-descriptions-item label="任务编号" min-width="120"> - {{ detailData.id }} - </el-descriptions-item> - <el-descriptions-item label="任务名称"> - {{ detailData.name }} - </el-descriptions-item> - <el-descriptions-item label="所属流程"> - {{ detailData.processInstance.name }} - </el-descriptions-item> - <el-descriptions-item label="流程发起人"> - {{ detailData.processInstance.startUserNickname }} - </el-descriptions-item> - <el-descriptions-item label="状态"> - <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="detailData.result" /> - </el-descriptions-item> - <el-descriptions-item label="原因"> - {{ detailData.reason }} - </el-descriptions-item> - <el-descriptions-item label="创建时间"> - {{ formatDate(detailData.createTime) }} - </el-descriptions-item> - </el-descriptions> - </Dialog> -</template> -<script lang="ts" setup> -import { DICT_TYPE } from '@/utils/dict' -import { formatDate } from '@/utils/formatTime' -import * as TaskApi from '@/api/bpm/task' - -defineOptions({ name: 'BpmTaskDetail' }) - -const dialogVisible = ref(false) // 弹窗的是否展示 -const detailLoading = ref(false) // 表单的加载中 -const detailData = ref() // 详情数据 - -/** 打开弹窗 */ -const open = async (data: TaskApi.TaskVO) => { - dialogVisible.value = true - // 设置数据 - detailLoading.value = true - try { - detailData.value = data - } finally { - detailLoading.value = false - } -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 -</script> diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue index ee1e1d14..a5137199 100644 --- a/src/views/bpm/task/done/index.vue +++ b/src/views/bpm/task/done/index.vue @@ -1,5 +1,11 @@ <template> - <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + <doc-alert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" /> + <doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" /> + <doc-alert + title="审批转办、委派、抄送" + url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" + /> + <doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" /> <ContentWrap> <!-- 搜索工作栏 --> @@ -46,27 +52,51 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column align="center" label="任务编号" prop="id" width="300px" /> - <el-table-column align="center" label="任务名称" prop="name" /> - <el-table-column align="center" label="所属流程" prop="processInstance.name" /> - <el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" /> - <el-table-column align="center" label="状态" prop="result"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" /> - </template> - </el-table-column> - <el-table-column align="center" label="原因" prop="reason" /> + <el-table-column align="center" label="流程" prop="processInstance.name" width="180" /> + <el-table-column + align="center" + label="发起人" + prop="processInstance.startUser.nickname" + width="100" + /> <el-table-column :formatter="dateFormatter" align="center" - label="创建时间" + label="发起时间" prop="createTime" width="180" /> - <el-table-column align="center" label="操作"> + <el-table-column align="center" label="当前任务" prop="name" width="180" /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="任务开始时间" + prop="createTime" + width="180" + /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="任务结束时间" + prop="endTime" + width="180" + /> + <el-table-column align="center" label="审批状态" prop="status" width="120"> <template #default="scope"> - <el-button link type="primary" @click="openDetail(scope.row)">详情</el-button> - <el-button link type="primary" @click="handleAudit(scope.row)">流程</el-button> + <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="180" /> + <el-table-column align="center" label="耗时" prop="durationInMillis" width="160"> + <template #default="scope"> + {{ formatPast2(scope.row.durationInMillis) }} + </template> + </el-table-column> + <el-table-column align="center" label="流程编号" prop="id" :show-overflow-tooltip="true" /> + <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" /> + <el-table-column align="center" label="操作" fixed="right" width="80"> + <template #default="scope"> + <el-button link type="primary" @click="handleAudit(scope.row)">历史</el-button> </template> </el-table-column> </el-table> @@ -78,15 +108,11 @@ @pagination="getList" /> </ContentWrap> - - <!-- 表单弹窗:详情 --> - <TaskDetail ref="detailRef" @success="getList" /> </template> <script lang="ts" setup> import { DICT_TYPE } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' import * as TaskApi from '@/api/bpm/task' -import TaskDetail from './TaskDetail.vue' defineOptions({ name: 'BpmTodoTask' }) @@ -107,7 +133,7 @@ const queryFormRef = ref() // 搜索的表单 const getList = async () => { loading.value = true try { - const data = await TaskApi.getDoneTaskPage(queryParams) + const data = await TaskApi.getTaskDonePage(queryParams) list.value = data.list total.value = data.total } finally { @@ -127,14 +153,8 @@ const resetQuery = () => { handleQuery() } -/** 详情操作 */ -const detailRef = ref() -const openDetail = (row: TaskApi.TaskVO) => { - detailRef.value.open(row) -} - /** 处理审批按钮 */ -const handleAudit = (row) => { +const handleAudit = (row: any) => { push({ name: 'BpmProcessInstanceDetail', query: { diff --git a/src/views/bpm/task/manager/index.vue b/src/views/bpm/task/manager/index.vue new file mode 100644 index 00000000..688e5150 --- /dev/null +++ b/src/views/bpm/task/manager/index.vue @@ -0,0 +1,166 @@ +<template> + <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + ref="queryFormRef" + :inline="true" + :model="queryParams" + class="-mb-15px" + label-width="68px" + > + <el-form-item label="任务名称" prop="name"> + <el-input + v-model="queryParams.name" + class="!w-240px" + clearable + placeholder="请输入任务名称" + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + end-placeholder="结束日期" + start-placeholder="开始日期" + type="daterange" + value-format="YYYY-MM-DD HH:mm:ss" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"> + <Icon class="mr-5px" icon="ep:search" /> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon class="mr-5px" icon="ep:refresh" /> + 重置 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column align="center" label="流程" prop="processInstance.name" width="180" /> + <el-table-column + align="center" + label="发起人" + prop="processInstance.startUser.nickname" + width="100" + /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="发起时间" + prop="createTime" + width="180" + /> + <el-table-column align="center" label="当前任务" prop="name" width="180" /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="任务开始时间" + prop="createTime" + width="180" + /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="任务结束时间" + prop="endTime" + width="180" + /> + <el-table-column align="center" label="审批人" prop="assigneeUser.nickname" width="100" /> + <el-table-column align="center" 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 align="center" label="审批建议" prop="reason" min-width="180" /> + <el-table-column align="center" label="耗时" prop="durationInMillis" width="160"> + <template #default="scope"> + {{ formatPast2(scope.row.durationInMillis) }} + </template> + </el-table-column> + <el-table-column align="center" label="流程编号" prop="id" :show-overflow-tooltip="true" /> + <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" /> + <el-table-column align="center" label="操作" fixed="right" width="80"> + <template #default="scope"> + <el-button link type="primary" @click="handleAudit(scope.row)">历史</el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" + @pagination="getList" + /> + </ContentWrap> +</template> +<script lang="ts" setup> +import { DICT_TYPE } from '@/utils/dict' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' +import * as TaskApi from '@/api/bpm/task' + +// 它和【待办任务】【已办任务】的差异是,该菜单可以看全部的流程任务 +defineOptions({ name: 'BpmManagerTask' }) + +const { push } = useRouter() // 路由 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: '', + createTime: [] +}) +const queryFormRef = ref() // 搜索的表单 + +/** 查询任务列表 */ +const getList = async () => { + loading.value = true + try { + const data = await TaskApi.getTaskManagerPage(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 handleAudit = (row: any) => { + push({ + name: 'BpmProcessInstanceDetail', + query: { + id: row.processInstance.id + } + }) +} + +/** 初始化 **/ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/bpm/task/todo/index.vue b/src/views/bpm/task/todo/index.vue index 29ba73dd..670fc683 100644 --- a/src/views/bpm/task/todo/index.vue +++ b/src/views/bpm/task/todo/index.vue @@ -1,5 +1,11 @@ <template> - <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> + <doc-alert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" /> + <doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" /> + <doc-alert + title="审批转办、委派、抄送" + url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" + /> + <doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" /> <ContentWrap> <!-- 搜索工作栏 --> @@ -46,27 +52,33 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column align="center" label="任务编号" prop="id" width="300px" /> - <el-table-column align="center" label="任务名称" prop="name" /> - <el-table-column align="center" label="所属流程" prop="processInstance.name" /> - <el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" /> + <el-table-column align="center" label="流程" prop="processInstance.name" width="180" /> + <el-table-column + align="center" + label="发起人" + prop="processInstance.startUser.nickname" + width="100" + /> <el-table-column :formatter="dateFormatter" align="center" - label="创建时间" + label="发起时间" prop="createTime" width="180" /> - <el-table-column label="任务状态" prop="suspensionState"> + <el-table-column align="center" label="当前任务" prop="name" width="180" /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="任务时间" + prop="createTime" + width="180" + /> + <el-table-column align="center" label="流程编号" prop="id" :show-overflow-tooltip="true" /> + <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" /> + <el-table-column align="center" label="操作" fixed="right" width="80"> <template #default="scope"> - <el-tag v-if="scope.row.suspensionState === 1" type="success">激活</el-tag> - <el-tag v-if="scope.row.suspensionState === 2" type="warning">挂起</el-tag> - </template> - </el-table-column> - <el-table-column align="center" label="操作"> - <template #default="scope"> - <el-button link type="primary" @click="handleAudit(scope.row)">审批进度</el-button> - <el-button link type="primary" @click="handleCC(scope.row)">抄送</el-button> + <el-button link type="primary" @click="handleAudit(scope.row)">办理</el-button> </template> </el-table-column> </el-table> @@ -77,16 +89,14 @@ :total="total" @pagination="getList" /> - <TaskCCDialogForm ref="taskCCDialogForm" /> </ContentWrap> </template> <script lang="ts" setup> import { dateFormatter } from '@/utils/formatTime' import * as TaskApi from '@/api/bpm/task' -import TaskCCDialogForm from '../../processInstance/detail/TaskCCDialogForm.vue' -defineOptions({ name: 'BpmDoneTask' }) +defineOptions({ name: 'BpmTodoTask' }) const { push } = useRouter() // 路由 @@ -105,7 +115,7 @@ const queryFormRef = ref() // 搜索的表单 const getList = async () => { loading.value = true try { - const data = await TaskApi.getTodoTaskPage(queryParams) + const data = await TaskApi.getTaskTodoPage(queryParams) list.value = data.list total.value = data.total } finally { @@ -126,7 +136,7 @@ const resetQuery = () => { } /** 处理审批按钮 */ -const handleAudit = (row) => { +const handleAudit = (row: any) => { push({ name: 'BpmProcessInstanceDetail', query: { @@ -135,12 +145,6 @@ const handleAudit = (row) => { }) } -const taskCCDialogForm = ref() -/** 处理抄送按钮 */ -const handleCC = (row) => { - taskCCDialogForm.value.open(row) -} - /** 初始化 **/ onMounted(() => { getList() diff --git a/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue b/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue deleted file mode 100644 index 9b215e0f..00000000 --- a/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue +++ /dev/null @@ -1,250 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="修改任务规则" width="600"> - <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px"> - <el-form-item label="任务名称" prop="taskDefinitionName"> - <el-input v-model="formData.taskDefinitionName" disabled placeholder="请输入流标标识" /> - </el-form-item> - <el-form-item label="任务标识" prop="taskDefinitionKey"> - <el-input v-model="formData.taskDefinitionKey" disabled placeholder="请输入任务标识" /> - </el-form-item> - <el-form-item label="规则类型" prop="type"> - <el-select v-model="formData.type" clearable style="width: 100%"> - <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds"> - <el-select v-model="formData.roleIds" clearable multiple style="width: 100%"> - <el-option - v-for="item in roleOptions" - :key="item.id" - :label="item.name" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item - v-if="formData.type === 20 || formData.type === 21" - label="指定部门" - prop="deptIds" - span="24" - > - <el-tree-select - ref="treeRef" - v-model="formData.deptIds" - :data="deptTreeOptions" - :props="defaultProps" - empty-text="加载中,请稍后" - multiple - node-key="id" - show-checkbox - /> - </el-form-item> - <el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24"> - <el-select v-model="formData.postIds" clearable multiple style="width: 100%"> - <el-option - v-for="item in postOptions" - :key="parseInt(item.id)" - :label="item.name" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item - v-if="formData.type === 30 || formData.type === 31 || formData.type === 32" - label="指定用户" - prop="userIds" - span="24" - > - <el-select v-model="formData.userIds" clearable multiple style="width: 100%"> - <el-option - v-for="item in userOptions" - :key="parseInt(item.id)" - :label="item.nickname" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds"> - <el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%"> - <el-option - v-for="item in userGroupOptions" - :key="parseInt(item.id)" - :label="item.name" - :value="parseInt(item.id)" - /> - </el-select> - </el-form-item> - <el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts"> - <el-select v-model="formData.scripts" clearable multiple style="width: 100%"> - <el-option - v-for="dict in taskAssignScriptDictDatas" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { defaultProps, handleTree } from '@/utils/tree' -import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule' -import * as RoleApi from '@/api/system/role' -import * as DeptApi from '@/api/system/dept' -import * as PostApi from '@/api/system/post' -import * as UserApi from '@/api/system/user' -import * as UserGroupApi from '@/api/bpm/userGroup' - -defineOptions({ name: 'BpmTaskAssignRuleForm' }) - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formData = ref({ - type: Number(undefined), - modelId: '', - options: [], - roleIds: [], - deptIds: [], - postIds: [], - userIds: [], - userGroupIds: [], - scripts: [] -}) -const formRules = reactive({ - type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }], - roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }], - deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }], - postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }], - userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }], - userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }], - scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }] -}) -const formRef = ref() // 表单 Ref -const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 -const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 -const deptTreeOptions = ref() // 部门树 -const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表 -const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 -const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 -const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT) - -/** 打开弹窗 */ -const open = async (modelId: string, row: TaskAssignRuleApi.TaskAssignVO) => { - // 1. 先重置表单 - resetForm() - // 2. 再设置表单 - formData.value = { - ...row, - modelId: modelId, - options: [], - roleIds: [], - deptIds: [], - postIds: [], - userIds: [], - userGroupIds: [], - scripts: [] - } - // 将 options 赋值到对应的 roleIds 等选项 - if (row.type === 10) { - formData.value.roleIds.push(...row.options) - } else if (row.type === 20 || row.type === 21) { - formData.value.deptIds.push(...row.options) - } else if (row.type === 22) { - formData.value.postIds.push(...row.options) - } else if (row.type === 30 || row.type === 31 || row.type === 32) { - formData.value.userIds.push(...row.options) - } else if (row.type === 40) { - formData.value.userGroupIds.push(...row.options) - } else if (row.type === 50) { - formData.value.scripts.push(...row.options) - } - // 打开弹窗 - dialogVisible.value = true - - // 获得角色列表 - roleOptions.value = await RoleApi.getSimpleRoleList() - // 获得部门列表 - deptOptions.value = await DeptApi.getSimpleDeptList() - deptTreeOptions.value = handleTree(deptOptions.value, 'id') - // 获得岗位列表 - postOptions.value = await PostApi.getSimplePostList() - // 获得用户列表 - userOptions.value = await UserApi.getSimpleUserList() - // 获得用户组列表 - userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList() -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - - // 构建表单 - const form = { - ...formData.value, - taskDefinitionName: undefined - } - // 将 roleIds 等选项赋值到 options 中 - if (form.type === 10) { - form.options = form.roleIds - } else if (form.type === 20 || form.type === 21) { - form.options = form.deptIds - } else if (form.type === 22) { - form.options = form.postIds - } else if (form.type === 30 || form.type === 31 || form.type === 32) { - form.options = form.userIds - } else if (form.type === 40) { - form.options = form.userGroupIds - } else if (form.type === 50) { - form.options = form.scripts - } - form.roleIds = undefined - form.deptIds = undefined - form.postIds = undefined - form.userIds = undefined - form.userGroupIds = undefined - form.scripts = undefined - - // 提交请求 - formLoading.value = true - try { - const data = form as unknown as TaskAssignRuleApi.TaskAssignVO - if (!data.id) { - await TaskAssignRuleApi.createTaskAssignRule(data) - message.success(t('common.createSuccess')) - } else { - await TaskAssignRuleApi.updateTaskAssignRule(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/taskAssignRule/index.vue b/src/views/bpm/taskAssignRule/index.vue deleted file mode 100644 index 0fe9bde6..00000000 --- a/src/views/bpm/taskAssignRule/index.vue +++ /dev/null @@ -1,136 +0,0 @@ -<template> - <ContentWrap> - <el-table v-loading="loading" :data="list"> - <el-table-column label="任务名" align="center" prop="taskDefinitionName" /> - <el-table-column label="任务标识" align="center" prop="taskDefinitionKey" /> - <el-table-column label="规则类型" align="center" prop="type"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE" :value="scope.row.type" /> - </template> - </el-table-column> - <el-table-column label="规则范围" align="center" prop="options"> - <template #default="scope"> - <el-tag class="mr-5px" :key="option" v-for="option in scope.row.options"> - {{ getAssignRuleOptionName(scope.row.type, option) }} - </el-tag> - </template> - </el-table-column> - <el-table-column v-if="queryParams.modelId" label="操作" align="center"> - <template #default="scope"> - <el-button - link - type="primary" - @click="openForm(scope.row)" - v-hasPermi="['bpm:task-assign-rule:update']" - > - 修改 - </el-button> - </template> - </el-table-column> - </el-table> - </ContentWrap> - <!-- 添加/修改弹窗 --> - <TaskAssignRuleForm ref="formRef" @success="getList" /> -</template> -<script lang="ts" setup> -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule' -import * as RoleApi from '@/api/system/role' -import * as DeptApi from '@/api/system/dept' -import * as PostApi from '@/api/system/post' -import * as UserApi from '@/api/system/user' -import * as UserGroupApi from '@/api/bpm/userGroup' -import TaskAssignRuleForm from './TaskAssignRuleForm.vue' - -defineOptions({ name: 'BpmTaskAssignRule' }) - -const { query } = useRoute() // 查询参数 - -const loading = ref(true) // 列表的加载中 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ - modelId: query.modelId, // 流程模型的编号。如果 modelId 非空,则用于流程模型的查看与配置 - processDefinitionId: query.processDefinitionId // 流程定义的编号。如果 processDefinitionId 非空,则用于流程定义的查看,不支持配置 -}) -const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 -const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 -const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表 -const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 -const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 -const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT) - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - list.value = await TaskAssignRuleApi.getTaskAssignRuleList(queryParams) - } finally { - loading.value = false - } -} - -/** 翻译规则范围 */ -// TODO 芋艿:各种 ts 报错 -const getAssignRuleOptionName = (type, option) => { - if (type === 10) { - for (const roleOption of roleOptions.value) { - if (roleOption.id === option) { - return roleOption.name - } - } - } else if (type === 20 || type === 21) { - for (const deptOption of deptOptions.value) { - if (deptOption.id === option) { - return deptOption.name - } - } - } else if (type === 22) { - for (const postOption of postOptions.value) { - if (postOption.id === option) { - return postOption.name - } - } - } else if (type === 30 || type === 31 || type === 32) { - for (const userOption of userOptions.value) { - if (userOption.id === option) { - return userOption.nickname - } - } - } else if (type === 40) { - for (const userGroupOption of userGroupOptions.value) { - if (userGroupOption.id === option) { - return userGroupOption.name - } - } - } else if (type === 50) { - option = option + '' // 转换成 string - for (const dictData of taskAssignScriptDictDatas) { - if (dictData.value === option) { - return dictData.label - } - } - } - return '未知(' + option + ')' -} - -/** 添加/修改操作 */ -const formRef = ref() -const openForm = (row: TaskAssignRuleApi.TaskAssignVO) => { - formRef.value.open(queryParams.modelId, row) -} - -/** 初始化 */ -onMounted(async () => { - await getList() - // 获得角色列表 - roleOptions.value = await RoleApi.getSimpleRoleList() - // 获得部门列表 - deptOptions.value = await DeptApi.getSimpleDeptList() - // 获得岗位列表 - postOptions.value = await PostApi.getSimplePostList() - // 获得用户列表 - userOptions.value = await UserApi.getSimpleUserList() - // 获得用户组列表 - userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList() -}) -</script> diff --git a/src/views/system/role/RoleForm.vue b/src/views/system/role/RoleForm.vue index 01f29b8b..161b7571 100644 --- a/src/views/system/role/RoleForm.vue +++ b/src/views/system/role/RoleForm.vue @@ -59,11 +59,11 @@ const formData = ref({ remark: '' }) const formRules = reactive({ - name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }], - code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }], - sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }], - status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }], - remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }] + name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }], + code: [{ required: true, message: '角色标识不能为空', trigger: 'change' }], + sort: [{ required: true, message: '显示顺序不能为空', trigger: 'change' }], + status: [{ required: true, message: '状态不能为空', trigger: 'change' }], + remark: [{ required: false, message: '备注不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref diff --git a/types/global.d.ts b/types/global.d.ts index 5e292687..e91e1fe4 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -14,6 +14,9 @@ declare global { type LocaleType = 'zh-CN' | 'en' + declare type TimeoutHandle = ReturnType<typeof setTimeout> + declare type IntervalHandle = ReturnType<typeof setInterval> + type AxiosHeaders = | 'application/json' | 'application/x-www-form-urlencoded' diff --git a/vite.config.ts b/vite.config.ts index fe2d7131..8cba9150 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -25,10 +25,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => { root: root, // 服务端渲染 server: { - // 是否开启 https - https: false, - // 端口号 - port: env.VITE_PORT, + port: env.VITE_PORT, // 端口号 host: "0.0.0.0", open: env.VITE_OPEN === 'true', // 本地跨域代理. 目前注释的原因:暂时没有用途,server 端已经支持跨域