From 1220f5ca98b10b735a47c37a81fbfc554b01e2fe Mon Sep 17 00:00:00 2001 From: liriming <1343021927@qq.com> Date: 星期一, 20 一月 2025 14:41:35 +0800 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- src/views/infra/apiErrorLog/index.vue | 14 src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue | 5 src/views/bpm/model/index_old.vue | 404 + src/views/system/tenant/index.vue | 2 .env.test | 7 src/components/DiyEditor/components/mobile/MenuSwiper/property.vue | 14 src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue | 102 src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue | 2 src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue | 148 src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss | 755 ++ src/components/FormCreate/src/config/useDictSelectRule.ts | 2 src/layout/components/useRenderLayout.tsx | 38 src/router/modules/remaining.ts | 44 src/views/model/mpk/project/index.vue | 14 src/views/bpm/definition/index.vue | 22 src/components/DiyEditor/components/mobile/Carousel/config.ts | 4 src/components/bpmnProcessDesigner/package/penal/task/data.ts | 36 src/components/DiyEditor/components/mobile/SearchBar/property.vue | 8 src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue | 163 src/components/SimpleProcessDesignerV2/src/index.ts | 5 src/views/system/app/AppForm.vue | 34 .env.dev | 12 src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue | 244 src/components/DiyEditor/components/ComponentContainer.vue | 1 src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts | 42 src/layout/components/Message/src/Message.vue | 8 src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue | 168 src/views/system/role/index.vue | 10 src/components/UserSelectForm/index.vue | 171 src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue | 138 src/views/bpm/form/index.vue | 4 src/components/DiyEditor/components/mobile/PromotionPoint/property.vue | 154 src/layout/components/Logo/src/Logo.vue | 28 src/views/data/ind/item/AtomIndDefineForm.vue | 44 src/components/DiyEditor/components/mobile/Popover/property.vue | 4 src/components/SimpleProcessDesignerV2/src/NodeHandler.vue | 231 src/api/model/mpk/project.ts | 7 vite.config.ts | 28 src/assets/svgs/bpm/copy.svg | 1 src/assets/imgs/logo.png | 0 src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue | 292 src/views/bpm/simple/SimpleModelDesign.vue | 155 src/components/DictTag/src/DictTag.vue | 90 src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue | 623 + src/components/DiyEditor/components/mobile/NavigationBar/property.vue | 12 src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue | 5 src/views/bpm/model/form/index.vue | 439 + src/views/model/mpk/file/index.vue | 18 src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue | 6 src/views/data/channel/http/api/tag/index.vue | 7 src/views/system/menu/MenuForm.vue | 6 src/assets/svgs/bpm/cancel.svg | 1 src/views/model/pre/item/index.vue | 62 src/assets/svgs/bpm/parallel.svg | 1 src/api/data/da/point/index.ts | 9 src/components/DiyEditor/components/mobile/ProductCard/property.vue | 10 src/store/modules/user.ts | 14 src/main.ts | 17 src/components/DiyEditor/components/mobile/PromotionCombination/property.vue | 86 src/views/model/pre/type/index.vue | 1 src/components/DiyEditor/components/mobile/MenuGrid/property.vue | 4 src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue | 989 ++ src/views/model/sche/model/index.vue | 8 src/views/bpm/processListener/ProcessListenerForm.vue | 2 src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue | 252 src/views/model/sche/model/ScheduleModelForm.vue | 307 src/api/bpm/task/index.ts | 55 src/components/FormCreate/src/components/useApiSelect.tsx | 5 src/components/ShortcutDateRangePicker/index.vue | 6 src/components/UploadFile/src/useUpload.ts | 33 src/views/data/ind/item/CalIndDefineForm.vue | 35 src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue | 87 src/components/Icon/src/IconSelect.vue | 18 src/views/data/point/index.vue | 39 src/assets/svgs/bpm/running.svg | 1 src/views/bpm/simpleWorkflow/index.vue | 13 src/views/model/mpk/file/MpkRun.vue | 70 src/views/model/mpk/project/ProjectPackage.vue | 5 src/views/bpm/category/CategoryForm.vue | 7 src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue | 97 src/components/DiyEditor/components/mobile/ProductList/index.vue | 3 src/views/model/mpk/pack/index.vue | 1 index.html | 4 src/components/DiyEditor/components/mobile/NoticeBar/config.ts | 2 src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue | 280 src/views/bpm/processInstance/manager/index.vue | 6 src/api/login/index.ts | 8 src/utils/permission.ts | 6 src/views/system/appmenu/index.vue | 6 src/directives/index.ts | 11 src/utils/constants.ts | 20 src/api/model/pre/item/index.ts | 2 src/components/SimpleProcessDesignerV2/theme/iconfont.ttf | 0 src/components/DiyEditor/components/mobile/TitleBar/property.vue | 10 src/layout/components/UserInfo/src/UserInfo.vue | 6 src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts | 2 src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue | 4 tsconfig.json | 10 src/assets/svgs/bpm/simple-process-bg.svg | 1 src/utils/dict.ts | 4 stylelint.config.js | 10 web-types.json | 19 src/views/bpm/form/editor/index.vue | 63 src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue | 48 src/components/UploadFile/src/UploadFile.vue | 24 src/config/axios/service.ts | 54 src/components/bpmnProcessDesigner/package/theme/process-designer.scss | 18 src/views/data/plan/category/CategoryForm.vue | 6 src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js | 12 src/components/DiyEditor/components/mobile/UserCard/index.vue | 2 src/views/report/drag/index.vue | 13 src/components/DiyEditor/components/ComponentContainerProperty.vue | 4 src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue | 142 src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue | 154 src/views/model/pre/item/MmPredictItemChart.vue | 55 src/views/bpm/processInstance/detail/index.vue | 597 src/views/bpm/model/form/FormDesign.vue | 137 src/views/model/mpk/menu/index.vue | 1 .env.prod | 20 src/views/bpm/model/form/ProcessDesign.vue | 235 src/components/ContentWrap/src/ContentWrap.vue | 2 src/views/bpm/model/form/BasicInfo.vue | 301 src/api/bpm/simple/index.ts | 15 src/components/DiyEditor/components/mobile/PromotionCombination/index.vue | 244 src/components/Echart/src/Echart.vue | 8 src/views/bpm/task/todo/index.vue | 97 src/views/infra/dataSourceConfig/index.vue | 8 src/views/system/appmenu/AppMenuForm.vue | 6 src/views/bpm/task/copy/index.vue | 27 src/views/system/app/index.vue | 4 src/views/model/sche/scheme/record/index.vue | 153 src/api/system/tenantPackage/index.ts | 3 src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue | 125 src/views/bpm/processInstance/create/index.vue | 423 package.json | 37 src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js | 6 src/api/model/mpk/mpk.ts | 4 src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts | 13 src/api/bpm/category/index.ts | 10 src/views/bpm/processInstance/create/index_old.vue | 266 src/directives/permission/hasPermi.ts | 20 src/views/system/tenantPackage/TenantPackageForm.vue | 24 src/views/model/sche/scheme/index.vue | 81 src/components/DiyEditor/components/mobile/Carousel/property.vue | 12 src/utils/routerHelper.ts | 8 .env.local | 6 src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue | 429 + src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js | 2 src/views/system/operatelog/index.vue | 4 src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue | 235 src/store/modules/permission.ts | 12 src/components/DiyEditor/components/mobile/ProductList/property.vue | 6 src/plugins/formCreate/index.ts | 75 src/views/system/tenantPackage/index.vue | 235 src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue | 189 src/api/model/sche/record/index.ts | 15 src/components/bpmnProcessDesigner/package/theme/element-variables.scss | 2 src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue | 6 src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue | 233 src/components/SimpleProcessDesignerV2/src/consts.ts | 606 + src/views/bpm/model/CategoryDraggableModel.vue | 511 + src/views/bpm/processInstance/index.vue | 137 src/components/DiyEditor/components/mobile/PromotionPoint/config.ts | 96 src/components/bpmnProcessDesigner/package/theme/index.scss | 119 src/views/model/sche/scheme/ScheduleSchemeForm.vue | 265 src/components/DiyEditor/components/mobile/PromotionPoint/index.vue | 202 src/utils/tree.ts | 3 uno.config.ts | 2 src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json | 227 src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue | 913 ++ src/assets/svgs/bpm/add-user.svg | 1 src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue | 4 src/assets/svgs/bpm/finish.svg | 1 src/api/model/sche/model/index.ts | 48 src/views/system/area/index.vue | 22 src/components/SimpleProcessDesignerV2/src/node.ts | 510 + src/components/AppLinkInput/data.ts | 8 src/views/model/mpk/icon/index.vue | 1 src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue | 39 types/env.d.ts | 2 src/views/bpm/model/index.vue | 515 src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue | 229 src/store/modules/bpm/simpleWorkflow.ts | 55 src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue | 194 src/views/infra/apiAccessLog/index.vue | 14 src/components/DiyEditor/components/mobile/PromotionCombination/config.ts | 40 src/views/system/menu/index.vue | 201 src/views/report/jmreport/index.vue | 4 src/utils/formCreate.ts | 1 src/styles/index.scss | 7 src/components/Editor/src/Editor.vue | 5 src/assets/svgs/bpm/condition.svg | 1 src/components/Crontab/src/Crontab.vue | 66 src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue | 959 - src/layout/components/Setting/src/Setting.vue | 5 src/views/system/loginlog/index.vue | 14 src/store/modules/tagsView.ts | 38 src/views/report/goview/index.vue | 6 src/views/data/ind/item/DerIndDefineForm.vue | 60 src/views/Home/Index.vue | 113 src/views/model/mpk/project/ProjectForm.vue | 2 src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue | 33 src/views/bpm/model/editor/index.vue | 262 src/views/model/pre/dm/index.vue | 1 src/components/SimpleProcessDesignerV2/src/utils.ts | 41 src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue | 298 src/views/model/mpk/file/MpkForm.vue | 5 src/styles/var.css | 8 src/views/bpm/task/manager/index.vue | 1 src/components/DiyEditor/components/mobile/CouponCard/property.vue | 6 src/assets/svgs/bpm/delay.svg | 1 src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue | 86 src/views/bpm/task/done/index.vue | 116 src/store/modules/app.ts | 10 src/api/model/sche/scheme/index.ts | 14 src/api/bpm/processInstance/index.ts | 46 src/assets/svgs/bpm/starter.svg | 1 src/components/RouterSearch/index.vue | 8 src/components/DiyEditor/components/mobile/TabBar/property.vue | 9 src/components/Draggable/index.vue | 4 src/router/index.ts | 2 src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue | 184 src/components/SimpleProcessDesignerV2/theme/iconfont.woff | 0 src/layout/components/TagsView/src/TagsView.vue | 146 src/components/DiyEditor/components/mobile/ProductCard/index.vue | 7 src/hooks/web/useCache.ts | 16 src/views/data/ind/category/CategoryForm.vue | 6 src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue | 98 src/hooks/web/useMessage.ts | 24 src/components/UploadFile/src/UploadImgs.vue | 23 src/components/IFrame/src/IFrame.vue | 33 src/styles/global.module.scss | 2 src/views/infra/config/index.vue | 14 src/layout/components/UserInfo/src/components/LockPage.vue | 2 src/layout/components/Menu/src/Menu.vue | 15 src/assets/svgs/bpm/approve.svg | 1 src/layout/components/TabMenu/src/TabMenu.vue | 6 src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue | 374 + src/views/data/point/DaPointForm.vue | 68 src/views/bpm/group/UserGroupForm.vue | 2 src/views/bpm/model/ModelForm.vue | 381 src/assets/svgs/bpm/auditor.svg | 1 src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue | 306 src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue | 91 types/router.d.ts | 3 src/layout/components/AppView.vue | 23 src/api/bpm/model/index.ts | 20 src/components/bpmnProcessDesigner/package/utils.ts | 2 src/layout/components/Breadcrumb/src/Breadcrumb.vue | 6 src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue | 67 src/layout/components/Footer/src/Footer.vue | 8 src/layout/components/UserInfo/src/components/LockDialog.vue | 2 src/components/DiyEditor/components/mobile/Divider/property.vue | 6 src/views/bpm/processExpression/ProcessExpressionForm.vue | 2 src/views/infra/file/index.vue | 10 src/components/SimpleProcessDesignerV2/theme/iconfont.woff2 | 0 src/views/micro/index.vue | 26 src/assets/svgs/bpm/reject.svg | 1 src/views/model/pre/item/MmPredictItemForm.vue | 171 /dev/null | 89 src/views/data/ind/data/DataSetForm.vue | 3 src/components/DiyEditor/components/mobile/TabBar/config.ts | 16 src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue | 174 src/directives/permission/hasRole.ts | 5 src/components/FormCreate/src/utils/index.ts | 43 src/views/data/point/DaPointChart.vue | 1 266 files changed, 18,517 insertions(+), 3,510 deletions(-) diff --git a/.env.dev b/.env.dev index 82ec13b..89a15bd 100644 --- a/.env.dev +++ b/.env.dev @@ -1,15 +1,13 @@ # 开发环境:本地只启动前端项目,依赖开发环境(后端、APP) -NODE_ENV=production +NODE_ENV=development VITE_DEV=true # 请求路径 -VITE_BASE_URL='http://localhost:48080' +VITE_BASE_URL='http://localhost' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server -# 上传路径 -VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' # 接口地址 VITE_API_URL=/admin-api @@ -24,13 +22,13 @@ VITE_SOURCEMAP=true # 打包路径 -VITE_BASE_PATH=/plat +VITE_BASE_PATH=/plat/ # 输出路径 VITE_OUT_DIR=dist # 公共静态文件路径 -VITE_STATIC_DIR=/ +VITE_STATIC_DIR=/plat/ # 商城H5会员端域名iai VITE_MALL_H5_DOMAIN='http://' @@ -39,4 +37,4 @@ VITE_APP_CAPTCHA_ENABLE=false # MDK模型上传路径 -MDK_UPLOAD_URL='http://localhost:48080/admin-api/model//pre/item/upload-model' +MDK_UPLOAD_URL='http://localhost/admin-api/model/pre/item/upload-model' diff --git a/.env.local b/.env.local index b28e8e2..9a61d8d 100644 --- a/.env.local +++ b/.env.local @@ -8,8 +8,6 @@ # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务 VITE_UPLOAD_TYPE=server -# 上传路径 -VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' # 接口地址 VITE_API_URL=/admin-api @@ -24,10 +22,10 @@ VITE_SOURCEMAP=false # 打包路径 -VITE_BASE_PATH=/plat +VITE_BASE_PATH=/plat/ # 公共静态文件路径 -VITE_STATIC_DIR=/ +VITE_STATIC_DIR=/plat/ # 商城H5会员端域名 VITE_MALL_H5_DOMAIN='http://localhost:3000' diff --git a/.env.prod b/.env.prod index 50729b1..3dfd239 100644 --- a/.env.prod +++ b/.env.prod @@ -1,15 +1,13 @@ -# 生产环境:只在打包时使用 +# 测试环境:只在打包时使用 NODE_ENV=production VITE_DEV=false # 请求路径 -VITE_BASE_URL='http://localhost:48080' +VITE_BASE_URL='http://10.88.4.131' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server -# 上传路径 -VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' # 接口地址 VITE_API_URL=/admin-api @@ -24,10 +22,16 @@ VITE_SOURCEMAP=false # 打包路径 -VITE_BASE_PATH=/ +VITE_BASE_PATH=/plat + +# 数据采集服务所在服务器,映射截图图片用 +VITE_VIDEO_CAMERA_DOMAIN='10.88.4.131' # 输出路径 -VITE_OUT_DIR=dist-prod +VITE_OUT_DIR=dist -# 商城H5会员端域名 -VITE_MALL_H5_DOMAIN='http://' +# 公共静态文件路径 +VITE_STATIC_DIR=/plat/ + +# 验证码的开关 +VITE_APP_CAPTCHA_ENABLE=false diff --git a/.env.test b/.env.test index 65af8e8..6478933 100644 --- a/.env.test +++ b/.env.test @@ -4,12 +4,10 @@ VITE_DEV=false # 请求路径 -VITE_BASE_URL='http://172.16.8.100:48080' +VITE_BASE_URL='http://172.16.8.100' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=server -# 上传路径 -VITE_UPLOAD_URL='http://172.16.8.100:48080/admin-api/infra/file/upload' # 接口地址 VITE_API_URL=/admin-api @@ -34,9 +32,6 @@ # 公共静态文件路径 VITE_STATIC_DIR=/plat/ - -# 商城H5会员端域名 -VITE_MALL_H5_DOMAIN='http://' # 验证码的开关 VITE_APP_CAPTCHA_ENABLE=false diff --git a/index.html b/index.html index 0d9deba..c3d9097 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="UTF-8" /> - <link rel="icon" href="/favicon.ico" /> + <link rel="icon" href="/src/assets/imgs/logo.png" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta @@ -136,7 +136,7 @@ <div class="app-loading"> <div class="app-loading-wrap"> <div class="app-loading-title"> - <img src="/logo.gif" class="app-loading-logo" alt="Logo" /> + <img src="/src/assets/imgs/logo.png" class="app-loading-logo" alt="Logo" /> <div class="app-loading-title">%VITE_APP_TITLE%</div> </div> <div class="app-loading-item"> diff --git a/package.json b/package.json index 435c717..f904721 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,16 @@ "description": "基于vue3、vite4、element-plus、typesScript", "author": "iailab", "private": false, - "main": "dist/iailab-plat-ui.min.js", "scripts": { "i": "pnpm install", "dev": "vite --mode env.local", "dev-server": "vite --mode dev", "ts:check": "vue-tsc --noEmit", - "build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build", - "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev", - "build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test", - "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage", - "build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod", + "build:local": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build", + "build:dev": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode dev", + "build:test": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode test", + "build:stage": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode stage", + "build:prod": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode prod", "serve:dev": "vite preview --mode dev", "serve:prod": "vite preview --mode prod", "preview": "pnpm build:local && vite preview", @@ -27,8 +26,8 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.1.0", - "@form-create/designer": "^3.1.3", - "@form-create/element-ui": "^3.1.24", + "@form-create/designer": "^3.2.6", + "@form-create/element-ui": "^3.2.11", "@iconify/iconify": "^3.1.1", "@microsoft/fetch-event-source": "^2.0.1", "@videojs-player/vue": "^1.0.0", @@ -39,7 +38,7 @@ "animate.css": "^4.1.1", "axios": "^1.6.8", "benz-amr-recorder": "^1.1.5", - "bpmn-js-token-simulation": "^0.10.0", + "bpmn-js-token-simulation": "^0.36.0", "camunda-bpmn-moddle": "^7.0.1", "cropperjs": "^1.6.1", "crypto-js": "^4.2.0", @@ -48,7 +47,7 @@ "driver.js": "^1.3.1", "echarts": "^5.5.0", "echarts-wordcloud": "^2.1.0", - "element-plus": "2.7.0", + "element-plus": "2.9.1", "fast-xml-parser": "^4.3.2", "highlight.js": "^11.9.0", "jsencrypt": "^3.3.2", @@ -65,18 +64,19 @@ "pinia-plugin-persistedstate": "^3.2.1", "qrcode": "^1.5.3", "qs": "^6.12.0", + "sortablejs": "^1.15.3", "steady-xml": "^0.1.0", "url": "^0.11.3", "video.js": "^7.21.5", - "vue": "3.4.21", + "vue": "3.5.12", "vue-dompurify-html": "^4.1.4", "vue-i18n": "9.10.2", - "vue-router": "^4.3.0", + "vue-router": "4.4.5", "vue-types": "^5.1.1", "vuedraggable": "^4.1.0", "web-storage-cache": "^1.1.1", - "wujie-vue3": "^1.0.22", - "xml-js": "^1.6.11" + "xml-js": "^1.6.11", + "wujie-vue3": "^1.0.22" }, "devDependencies": { "@commitlint/cli": "^19.0.1", @@ -97,8 +97,8 @@ "@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", + "bpmn-js": "^17.9.2", + "bpmn-js-properties-panel": "5.23.0", "consola": "^3.2.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -121,7 +121,7 @@ "stylelint-order": "^6.0.4", "terser": "^5.28.1", "typescript": "5.3.3", - "unocss": "^0.58.9", + "unocss": "^0.58.5", "unplugin-auto-import": "^0.16.7", "unplugin-element-plus": "^0.8.0", "unplugin-vue-components": "^0.25.2", @@ -132,7 +132,7 @@ "vite-plugin-progress": "^0.0.7", "vite-plugin-purge-icons": "^0.10.0", "vite-plugin-svg-icons": "^2.0.1", - "vite-plugin-top-level-await": "^1.3.1", + "vite-plugin-top-level-await": "^1.4.4", "vue-eslint-parser": "^9.3.2", "vue-tsc": "^1.8.27" }, @@ -145,6 +145,7 @@ "url": "https://xxxx" }, "homepage": "https://xxxx", + "web-types": "./web-types.json", "engines": { "node": ">= 16.0.0", "pnpm": ">=8.6.0" diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 5a7de08..0000000 --- a/public/favicon.ico +++ /dev/null Binary files differ diff --git a/public/logo.gif b/public/logo.gif deleted file mode 100644 index fdbd32c..0000000 --- a/public/logo.gif +++ /dev/null Binary files differ diff --git a/src/api/bpm/category/index.ts b/src/api/bpm/category/index.ts index d1e109c..1854f31 100644 --- a/src/api/bpm/category/index.ts +++ b/src/api/bpm/category/index.ts @@ -36,6 +36,16 @@ return await request.put({ url: `/bpm/category/update`, data }) }, + // 批量修改流程分类的排序 + updateCategorySortBatch: async (ids: number[]) => { + return await request.put({ + url: `/bpm/category/update-sort-batch`, + params: { + ids: ids.join(',') + } + }) + }, + // 删除流程分类 deleteCategory: async (id: number) => { return await request.delete({ url: `/bpm/category/delete?id=` + id }) diff --git a/src/api/bpm/model/index.ts b/src/api/bpm/model/index.ts index 2b484a6..0c499db 100644 --- a/src/api/bpm/model/index.ts +++ b/src/api/bpm/model/index.ts @@ -26,11 +26,11 @@ bpmnXml: string } -export const getModelPage = async (params) => { - return await request.get({ url: '/bpm/model/page', params }) +export const getModelList = async (name: string | undefined) => { + return await request.get({ url: '/bpm/model/list', params: { name } }) } -export const getModel = async (id: number) => { +export const getModel = async (id: string) => { return await request.get({ url: '/bpm/model/get?id=' + id }) } @@ -38,6 +38,20 @@ return await request.put({ url: '/bpm/model/update', data: data }) } +// 批量修改流程分类的排序 +export const updateModelSortBatch = async (ids: number[]) => { + return await request.put({ + url: `/bpm/model/update-sort-batch`, + params: { + ids: ids.join(',') + } + }) +} + +export const updateModelBpmn = async (data: ModelVO) => { + return await request.put({ url: '/bpm/model/update-bpmn', data: data }) +} + // 任务状态修改 export const updateModelState = async (id: number, state: number) => { const data = { diff --git a/src/api/bpm/processInstance/index.ts b/src/api/bpm/processInstance/index.ts index 9122b2b..f97270f 100644 --- a/src/api/bpm/processInstance/index.ts +++ b/src/api/bpm/processInstance/index.ts @@ -1,6 +1,6 @@ import request from '@/config/axios' import { ProcessDefinitionVO } from '@/api/bpm/model' - +import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts' export type Task = { id: string name: string @@ -20,6 +20,35 @@ createTime: string endTime: string processDefinition?: ProcessDefinitionVO +} + +// 用户信息 +export type User = { + id: number + nickname: string + avatar: string +} + +// 审批任务信息 +export type ApprovalTaskInfo = { + id: number + ownerUser: User + assigneeUser: User + status: number + reason: string +} + +// 审批节点信息 +export type ApprovalNodeInfo = { + id: number + name: string + nodeType: NodeType + candidateStrategy?: CandidateStrategy + status: number + startTime?: Date + endTime?: Date + candidateUsers?: User[] + tasks: ApprovalTaskInfo[] } export const getProcessInstanceMyPage = async (params: any) => { @@ -57,3 +86,18 @@ export const getProcessInstanceCopyPage = async (params: any) => { return await request.get({ url: '/bpm/process-instance/copy/page', params }) } + +// 获取审批详情 +export const getApprovalDetail = async (params: any) => { + return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params }) +} + +// 获取表单字段权限 +export const getFormFieldsPermission = async (params: any) => { + return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params }) +} + +// 获取流程实例的 BPMN 模型视图 +export const getProcessInstanceBpmnModelView = async (id: string) => { + return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id }) +} diff --git a/src/api/bpm/simple/index.ts b/src/api/bpm/simple/index.ts new file mode 100644 index 0000000..6e1e995 --- /dev/null +++ b/src/api/bpm/simple/index.ts @@ -0,0 +1,15 @@ +import request from '@/config/axios' + + +export const updateBpmSimpleModel = async (data) => { + return await request.post({ + url: '/bpm/model/simple/update', + data: data + }) +} + +export const getBpmSimpleModel = async (id) => { + return await request.get({ + url: '/bpm/model/simple/get?id=' + id + }) +} diff --git a/src/api/bpm/task/index.ts b/src/api/bpm/task/index.ts index f3cda9f..d4c1038 100644 --- a/src/api/bpm/task/index.ts +++ b/src/api/bpm/task/index.ts @@ -1,7 +1,44 @@ import request from '@/config/axios' -export type TaskVO = { - id: number +/** + * 任务状态枚举 + */ +export enum TaskStatusEnum { + /** + * 未开始 + */ + NOT_START = -1, + + /** + * 待审批 + */ + WAIT = 0, + /** + * 审批中 + */ + RUNNING = 1, + /** + * 审批通过 + */ + APPROVE = 2, + + /** + * 审批不通过 + */ + REJECT = 3, + + /** + * 已取消 + */ + CANCEL = 4, + /** + * 已退回 + */ + RETURN = 5, + /** + * 审批通过中 + */ + APPROVING = 7 } export const getTaskTodoPage = async (params: any) => { @@ -30,12 +67,12 @@ }) } -// 获取所有可回退的节点 +// 获取所有可退回的节点 export const getTaskListByReturn = async (id: string) => { return await request.get({ url: '/bpm/task/list-by-return', params: { id } }) } -// 回退 +// 退回 export const returnTask = async (data: any) => { return await request.put({ url: '/bpm/task/return', data }) } @@ -60,6 +97,16 @@ return await request.delete({ url: '/bpm/task/delete-sign', data }) } +// 抄送 +export const copyTask = async (data: any) => { + return await request.put({ url: '/bpm/task/copy', data }) +} + +// 获取我的待办任务 +export const myTodoTask = async (processInstanceId: string) => { + return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId }) +} + // 获取减签任务列表 export const getChildrenTaskList = async (id: string) => { return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id }) diff --git a/src/api/data/da/point/index.ts b/src/api/data/da/point/index.ts index 366f331..b64a001 100644 --- a/src/api/data/da/point/index.ts +++ b/src/api/data/da/point/index.ts @@ -20,7 +20,9 @@ export interface DaPointPageReqVO extends PageParam { pointNo?: string, - pointName?: string + pointName?: string, + tagNo?: string, + collectQuality?: string, } @@ -34,6 +36,11 @@ return request.get({ url: '/data/da/point/list', params }) } +// 查询DaPoint simpleList +export const getPointSimpleList = (params: DaPointPageReqVO) => { + return request.get({ url: '/data/da/point/simple-list', params }) +} + // 查询DaPoint详情 export const getDaPoint = (id: number) => { return request.get({ url: `/data/da/point/info/${id}`}) diff --git a/src/api/login/index.ts b/src/api/login/index.ts index e6d9f52..4b82a93 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -42,10 +42,10 @@ return request.get({ url: '/system/auth/get-permission-info' }) } -// 获取用应用户权限信息 -export const getUserAppInfo = (id: number) => { - return request.get({ url: '/system/auth/get-app-permission-info?id=' + id }) -} +// // 获取用应用户权限信息 +// export const getUserAppInfo = (id: number) => { +// return request.get({ url: '/system/auth/get-app-permission-info?id=' + id }) +// } //获取登录验证码 export const sendSmsCode = (data: SmsCodeVO) => { diff --git a/src/api/model/mpk/mpk.ts b/src/api/model/mpk/mpk.ts index 653cf8b..dc2782f 100644 --- a/src/api/model/mpk/mpk.ts +++ b/src/api/model/mpk/mpk.ts @@ -42,8 +42,8 @@ return request.post({ url: '/model/mpk/api/test', data: params }) } -export const list = () => { - return request.get({ url: '/model/mpk/file/list'}) +export const list = (params) => { + return request.get({ url: '/model/mpk/file/list', params}) } export const publish = (params) => { diff --git a/src/api/model/mpk/project.ts b/src/api/model/mpk/project.ts index 514b05b..33f1cf6 100644 --- a/src/api/model/mpk/project.ts +++ b/src/api/model/mpk/project.ts @@ -1,6 +1,6 @@ import request from '@/config/axios' -export const getPage = async (params: PageParam) => { +export const getPage = async (params) => { return await request.get({ url: '/model/mpk/project/page', params }) } @@ -21,13 +21,14 @@ } export const packageProject = (params) => { - return request.download({ url: '/model/mpk/file/packageModel', params }) + // 超时时间两分钟 + return request.download({ url: '/model/mpk/file/packageModel', params, timeout: 2 * 60 * 1000 }) } export const list = () => { return request.get({ url: '/model/mpk/project/list'}) } -export const getProjectModel = async (params: PageParam) => { +export const getProjectModel = async (params) => { return await request.get({ url: '/model/mpk/project/getProjectModel', params }) } diff --git a/src/api/model/pre/item/index.ts b/src/api/model/pre/item/index.ts index a4e2065..73a7683 100644 --- a/src/api/model/pre/item/index.ts +++ b/src/api/model/pre/item/index.ts @@ -69,6 +69,8 @@ export interface MmPredictItemPageReqVO extends PageParam { itemno?: string, itemname?: string, + itemtypeid?: string, + modulename?: string, } // 查询MmPredictItem列表 diff --git a/src/api/model/sche/model/index.ts b/src/api/model/sche/model/index.ts index 8863d31..0ac2b05 100644 --- a/src/api/model/sche/model/index.ts +++ b/src/api/model/sche/model/index.ts @@ -20,6 +20,7 @@ status: number, paramList: null, settingList: null + modelOut:null } export interface ModelParamVO { @@ -70,7 +71,7 @@ } // 查询模型参数列表 -export const getModelParamList = async () => { +export const getModelParamList = async (id) => { const dataPointList = ref([] as DataPointApi.DaPointVO) dataPointList.value = await DataPointApi.getPointList({}) @@ -80,7 +81,8 @@ pointList.push( { id: item.id, - name: item.pointName + name: item.pointName, + itemNo : item.pointNo } ) }) @@ -88,16 +90,25 @@ const predictItemList = ref([] as PredictItemApi.MmPredictItemVO) predictItemList.value = await PredictItemApi.getMmPredictItemList({ - status: CommonEnabled.ENABLE, - itemtypename: 'NormalItem' + status: CommonEnabled.ENABLE }) - const itemList = [] - if (predictItemList.value) { - predictItemList.value.forEach(item => { - itemList.push( + const normalItemList = [] + const predictNormalItemList = predictItemList.value.filter(e => e.itemtypename === 'NormalItem' && e.outPuts && e.outPuts.length > 0); + if (predictNormalItemList && predictNormalItemList.length > 0) { + // 过滤掉本身 + predictNormalItemList.filter(e => e.id !== id).forEach(item => { + normalItemList.push( { - id: item.id, - name: item.itemname + value: item.id, + label: item.itemname, + predictlength: item.predictlength, + moduleid: item.moduleid, + children: item.outPuts?.map(e => { + return { + value: e.id, + label: e.resultName + } + }) } ) }) @@ -118,9 +129,24 @@ }) } + const predictMergeItemList = predictItemList.value.filter(e => e.itemtypename === 'MergeItem' && e.outPuts && e.outPuts.length > 0); + const mergeItemList = [] + if (predictMergeItemList && predictMergeItemList.length > 0) { + // 过滤掉本身 + predictMergeItemList.filter(e => e.id !== id).forEach(item => { + mergeItemList.push( + { + id: item.outPuts[0].id, + name: item.itemname + } + ) + }) + } + return { 'DATAPOINT':pointList, - 'PREDICTITEM': itemList, + 'NormalItem': normalItemList, + 'MergeItem': mergeItemList, 'PLAN': planList, } } diff --git a/src/api/model/sche/record/index.ts b/src/api/model/sche/record/index.ts new file mode 100644 index 0000000..f712041 --- /dev/null +++ b/src/api/model/sche/record/index.ts @@ -0,0 +1,15 @@ +import request from '@/config/axios' + +export interface StScheduleRecordPageReqVO extends PageParam { + schemeId?: string +} + +// 查询ScheduleRecord列表 +export const getScheduleRecordPage = (params: StScheduleRecordPageReqVO) => { + return request.get({ url: '/model/sche/record/page', params }) +} + +// 查询ScheduleRecord详情 +export const getScheduleRecord = (id: string) => { + return request.get({ url: '/model/sche/record/get?id=' + id}) +} diff --git a/src/api/model/sche/scheme/index.ts b/src/api/model/sche/scheme/index.ts index 3d3971f..0376394 100644 --- a/src/api/model/sche/scheme/index.ts +++ b/src/api/model/sche/scheme/index.ts @@ -13,6 +13,7 @@ scheduleTime: string remark: string status: number + mpkprojectid: string } export interface ScheduleSchemePageReqVO extends PageParam { @@ -44,3 +45,16 @@ export const deleteScheduleScheme = (id: number) => { return request.delete({ url: '/model/sche/scheme/delete?id=' + id }) } + +// 启用 +export const enable = (ids) => { + const data = ids + return request.put({ url: '/model/sche/scheme/enable', data }) +} + +// 禁用 +export const disable = (ids) => { + const data = ids + return request.put({ url: '/model/sche/scheme/disable', data }) +} + diff --git a/src/api/system/tenantPackage/index.ts b/src/api/system/tenantPackage/index.ts index e01375a..fdf9a5b 100644 --- a/src/api/system/tenantPackage/index.ts +++ b/src/api/system/tenantPackage/index.ts @@ -4,6 +4,9 @@ id: number name: string status: number + icon: string + labels: string + description: string remark: string creator: string updater: string diff --git a/src/assets/imgs/logo.png b/src/assets/imgs/logo.png index 4017d80..f55aafb 100644 --- a/src/assets/imgs/logo.png +++ b/src/assets/imgs/logo.png Binary files differ diff --git a/src/assets/svgs/bpm/add-user.svg b/src/assets/svgs/bpm/add-user.svg new file mode 100644 index 0000000..bc7bdbf --- /dev/null +++ b/src/assets/svgs/bpm/add-user.svg @@ -0,0 +1 @@ +<svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/approve.svg b/src/assets/svgs/bpm/approve.svg new file mode 100644 index 0000000..06aa09d --- /dev/null +++ b/src/assets/svgs/bpm/approve.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724316565416" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1339" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#13C463" p-id="1340"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#13C463" p-id="1341"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#13C463" p-id="1342"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#13C463" p-id="1343"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#13C463" p-id="1344"></path><path d="M356.770909 569.250909l22.341818 38.749091-15.476363 8.727273L349.090909 592.64l-153.483636 88.785455 14.778182 25.483636-15.476364 8.96-23.272727-39.912727L256 627.2c-6.283636-4.887273-11.636364-8.843636-16.174545-11.636364L256 602.647273c3.956364 3.141818 9.774545 8.261818 17.338182 15.127272z m-17.338182 199.447273l-49.221818 28.392727 7.563636 13.149091-15.476363 8.96-62.138182-107.52 64.814545-37.469091-12.8-22.574545 15.941819-9.192728 12.8 22.109091 65.396363-37.701818 61.672728 106.821818-15.476364 8.96-7.214546-12.450909-49.92 28.858182 26.065455 45.032727-16.058182 9.192728z m-46.545454-79.825455L244.363636 717.265455l14.778182 25.6 49.221818-28.509091zM267.636364 756.945455l14.778181 25.6 49.221819-28.509091-14.778182-25.483637z m106.938181-80.523637l-14.778181-25.483636-49.92 28.741818 14.778181 25.483636zM346.996364 744.727273l49.803636-28.741818-14.661818-25.483637-49.92 28.741818zM505.832727 609.978182c-4.654545 6.283636-10.123636 13.265455-16.523636 21.061818l35.84 62.021818a18.967273 18.967273 0 0 1-6.749091 29.672727l-19.316364 11.636364-12.450909-13.847273a170.123636 170.123636 0 0 0 17.803637-8.727272 8.494545 8.494545 0 0 0 2.909091-13.614546L477.090909 645.352727l-9.890909 10.472728-10.007273 10.24-12.683636-13.149091c9.309091-8.261818 17.221818-15.941818 23.272727-23.272728l-31.301818-54.341818-25.018182 14.545455-8.843636-15.36 25.018182-14.429091-23.272728-41.076364 15.476364-8.96 23.272727 41.076364L465.454545 538.763636l8.843637 15.36-22.109091 12.567273 28.509091 49.221818c5.469091-6.516364 10.938182-13.498182 16.407273-21.061818z m9.076364-45.730909L572.043636 663.272727a207.825455 207.825455 0 0 0 23.272728-27.461818l11.636363 13.149091a365.381818 365.381818 0 0 1-41.774545 45.498182l-12.567273-12.567273a11.636364 11.636364 0 0 0 1.745455-13.963636L453.818182 493.963636l15.709091-9.076363 36.887272 63.883636 31.301819-18.152727 8.96 15.592727z m129.745454 83.316363a20.596364 20.596364 0 0 1-31.418181-9.774545l-103.098182-178.618182 15.709091-9.192727 38.632727 67.025454a200.261818 200.261818 0 0 0 28.043636-41.076363l16.872728 7.68a303.243636 303.243636 0 0 1-35.723637 49.338182l53.410909 93.090909a9.192727 9.192727 0 0 0 13.963637 4.072727l10.821818-6.283636a14.312727 14.312727 0 0 0 8.029091-11.636364 103.447273 103.447273 0 0 0-15.243637-39.098182l17.338182-3.84c12.567273 25.134545 18.036364 41.658182 16.290909 49.803636A28.392727 28.392727 0 0 1 663.272727 636.741818zM860.276364 521.774545c-7.563636 4.421818-20.829091 11.636364-39.912728 22.574546a179.432727 179.432727 0 0 1-37.352727 16.174545 58.181818 58.181818 0 0 1-33.047273-1.978181 14.312727 14.312727 0 0 0-11.636363-0.581819c-5.352727 3.025455-8.261818 18.385455-8.727273 45.847273l-18.269091-3.956364c1.047273-25.483636 5.003636-42.821818 11.636364-52.014545l-38.865455-67.374545-31.534545 18.152727-8.378182-14.661818 46.545454-26.647273 47.825455 82.850909a55.505455 55.505455 0 0 1 8.494545 1.861818 59.694545 59.694545 0 0 0 25.367273 4.072727 101.701818 101.701818 0 0 0 33.512727-11.636363L849.454545 508.509091l31.418182-18.734546c11.636364-7.214545 19.898182-12.334545 24.087273-15.127272l5.469091 18.152727zM676.072727 413.207273L671.185455 430.545455a279.272727 279.272727 0 0 0-58.181819-13.265455l4.887273-16.64a307.781818 307.781818 0 0 1 58.181818 12.567273zM754.967273 372.363636a261.818182 261.818182 0 0 0 20.247272-38.516363l-98.443636 56.785454-7.796364-13.498182 119.97091-69.46909 6.632727 11.636363a281.134545 281.134545 0 0 1-25.949091 54.807273l5.236364 0.930909L818.734545 349.090909l57.25091 99.025455a18.385455 18.385455 0 0 1-8.843637 27.927272l-18.385454 10.589091-11.636364-11.636363 17.92-9.425455a7.796364 7.796364 0 0 0 3.607273-11.636364L849.454545 437.410909l-37.236363 21.527273 21.992727 38.050909-14.894545 8.610909-21.992728-38.167273L760.203636 488.727273l22.458182 38.749091-15.127273 8.727272L699.461818 418.909091l55.389091-32a306.269091 306.269091 0 0 0-39.330909-1.047273l4.305455-15.127273c13.265455-0.232727 24.901818 0.465455 35.141818 1.629091z m15.825454 49.454546l-11.636363-20.014546-37.003637 21.410909 11.636364 20.014546z m-29.44 34.909091l11.636364 19.549091 37.003636-21.410909-11.636363-19.549091z m81.454546-64.814546l-11.636364-19.898182-37.236364 21.527273 11.636364 19.898182z m-29.556364 34.909091l11.636364 19.432727 37.236363-21.527272-11.636363-19.432728zM1086.370909 391.214545l-19.898182 11.636364-10.589091 6.167273-10.938181 6.050909a186.181818 186.181818 0 0 1-38.749091 16.989091 60.16 60.16 0 0 1-33.978182-1.978182 14.312727 14.312727 0 0 0-11.636364 0c-5.585455 3.258182-8.610909 18.734545-8.96 46.545455l-18.036363-3.723637c0.814545-26.181818 4.770909-43.752727 11.636363-52.945454l-38.865454-67.141819-31.883637 18.385455-8.727272-15.010909 47.243636-27.345455 47.941818 83.2h4.189091a32.465455 32.465455 0 0 1 4.538182 1.163637 71.68 71.68 0 0 0 26.298182 3.490909 112.872727 112.872727 0 0 0 34.210909-13.265455c16.523636-9.192727 31.767273-17.803636 46.545454-25.949091l14.545455-8.727272 14.196363-8.727273c11.636364-6.865455 18.618182-11.636364 22.574546-14.196364l5.352727 18.385455zM896 286.021818l-4.770909 18.385455a296.378182 296.378182 0 0 0-58.181818-14.661818l4.770909-16.872728a311.156364 311.156364 0 0 1 58.181818 13.149091zM1031.098182 384l-12.334546-13.149091c11.636364-5.934545 21.76-11.636364 30.138182-15.941818a9.658182 9.658182 0 0 0 4.189091-14.661818l-54.341818-94.138182-83.781818 48.290909-9.076364-15.709091 83.781818-48.407273-20.712727-35.84 16.174545-9.425454 20.712728 36.072727 32.814545-18.967273 8.610909 15.243637-32.349091 18.850909 56.552728 97.978182a20.247273 20.247273 0 0 1-8.843637 31.185454z m-23.272727-59.345455L1000.727273 340.48a405.876364 405.876364 0 0 0-58.181818-25.6l7.796363-15.127273a393.890909 393.890909 0 0 1 57.716364 24.436364z" fill="#13C463" p-id="1345"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/auditor.svg b/src/assets/svgs/bpm/auditor.svg new file mode 100644 index 0000000..66d2c2c --- /dev/null +++ b/src/assets/svgs/bpm/auditor.svg @@ -0,0 +1 @@ +<svg t="1729561718271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8640" width="200" height="200"><path d="M908.5952 920.4224H164.7616a31.0784 31.0784 0 0 1-30.976-30.976c0-17.0496 13.9264-30.976 30.976-30.976h743.8336c17.0496 0 31.0272 13.9264 31.0272 30.976a31.0784 31.0784 0 0 1-31.0272 30.976z m0-123.9552H164.7616a31.0784 31.0784 0 0 1-30.976-30.976v-154.9824c0-51.1488 41.8304-92.9792 92.9792-92.9792h198.3488c-6.1952-37.1712-24.7808-72.8064-51.1488-103.8336a216.576 216.576 0 0 1-54.2208-144.128c0-58.88 23.2448-114.688 66.6112-156.4672C429.7728 71.168 485.5296 51.0976 545.9968 52.6848c111.5648 4.608 206.08 100.6592 207.616 212.2752 1.536 55.808-20.1216 110.0288-57.344 151.8592-26.3168 27.904-41.8304 61.952-48.0256 100.7104h198.3488c51.2 0 93.0304 41.8304 93.0304 92.9792v154.9824a31.0784 31.0784 0 0 1-31.0272 30.976z m-712.8064-61.952H877.568v-124.0064a31.0784 31.0784 0 0 0-31.0272-30.976h-232.448a31.0784 31.0784 0 0 1-30.976-31.0272c0-65.024 23.2448-127.0784 66.6624-173.568 27.8528-29.3888 41.8304-68.1472 41.8304-108.4416-1.536-80.5888-68.1984-148.7872-148.7872-151.8592a150.528 150.528 0 0 0-113.152 43.3664 153.6 153.6 0 0 0-48.0256 111.616c0 37.1712 13.9776 74.3424 38.7584 102.2464 44.9536 51.1488 69.7344 113.152 69.7344 176.64a31.0784 31.0784 0 0 1-30.976 31.0272h-232.448a31.0784 31.0784 0 0 0-30.976 30.976v123.9552z" fill="#fff" p-id="8641"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/cancel.svg b/src/assets/svgs/bpm/cancel.svg new file mode 100644 index 0000000..ab9b155 --- /dev/null +++ b/src/assets/svgs/bpm/cancel.svg @@ -0,0 +1 @@ +<svg t="1729178183592" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4332" width="200" height="200"><path d="M784.074702 99.196443l10.927871 18.473304-21.302843-2.56935-14.180213 16.066571-4.130475-21.042655-19.676671-8.521137 18.733492-10.440019 2.016452-21.335366 15.708814 14.603017 20.945085-4.683373-9.041512 19.449008zM1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512z" fill="#8a8a8a" p-id="4333"></path><path d="M1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512zM571.924408 100.009528l-17.400031-12.488994 20.52228-6.211974 6.504685-20.457234 12.261331 17.595172 21.432936-0.09757-12.944323 17.074798 6.732349 20.359663-20.262093-7.02506-17.269938 12.716659 0.422804-21.46546zM991.444053 784.43246l-21.172749 3.480006 10.114785-18.928632-9.822074-19.026203 21.107702 3.772717 15.090868-15.253486 2.927109 21.237796 19.156296 9.626933-19.318914 9.366746-3.219819 21.205273-14.863204-15.48115zM428.008258 156.795426l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.936097 19.936859-7.870669-5.88674 20.619851 13.692361 16.521899-21.432936 0.813086-11.513292 18.083024-7.382817-20.132zM854.260251 896.475655l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.96862 19.936859-7.903192-5.854217 20.619851 13.659838 16.554423-21.432936 0.780562-11.513292 18.115547-7.382817-20.164523zM562.460092 923.665237l10.895347 18.440782-21.302843-2.569351-14.180212 16.099095-4.130475-21.042655-19.676672-8.521137 18.733493-10.440019 2.016452-21.36789 15.708814 14.603018 20.945085-4.683373-9.008989 19.48153zM242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988z" fill="#8a8a8a" p-id="4334"></path><path d="M242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988zM700.814737 943.959854l-17.400032-12.521518 20.522281-6.211974 6.504685-20.42471 12.26133 17.595172 21.432937-0.130094-12.944323 17.107321 6.732349 20.359663-20.262093-7.025059-17.269938 12.684135 0.422804-21.432936zM303.541115 278.823313l-21.140226 3.480006 10.114785-18.928633-9.854597-19.058726 21.107702 3.772717 15.090868-15.220962 2.927109 21.237796 19.156296 9.626933-19.28639 9.366746-3.252342 21.172749-14.863205-15.448626z" fill="#8a8a8a" p-id="4335"></path><path d="M407.648595 90.642782a486.713038 486.713038 0 0 1 504.568397 11.578339l25.010513-14.407877A512.081309 512.081309 0 0 0 139.850723 547.401747l24.977989-14.407877a486.778085 486.778085 0 0 1 242.819883-442.351088zM893.28836 933.422265a486.810608 486.810608 0 0 1-504.568398-11.610863l-25.010513 14.407877a512.081309 512.081309 0 0 0 797.5394-459.621026l-24.97799 14.505447a486.843132 486.843132 0 0 1-242.982499 442.318565z" fill="#8a8a8a" p-id="4336"></path><path d="M814.061299 795.880705a326.665269 326.665269 0 0 1-258.170939 29.563792l-29.791456 17.172368a353.236906 353.236906 0 0 0 472.793013-272.448721l-29.693886 17.172367a326.762839 326.762839 0 0 1-155.136732 208.540194zM486.875655 228.119295a326.795363 326.795363 0 0 1 258.170939-29.563792l29.791456-17.172368a353.236906 353.236906 0 0 0-472.793013 272.448721l29.82398-17.172367a326.762839 326.762839 0 0 1 155.006638-208.540194zM1288.350389 374.73489a53.923837 53.923837 0 0 1-14.34283 12.001143L229.420232 988.712085A53.793743 53.793743 0 0 1 156.112434 968.937843l-148.924757-258.235985a53.76122 53.76122 0 0 1 19.741718-73.437891L1071.516722 35.352962A53.826266 53.826266 0 0 1 1144.82452 55.062157l148.827187 258.268508a53.793743 53.793743 0 0 1-5.398888 61.404225zM32.19819 665.754486a28.360426 28.360426 0 0 0-5.626553 10.73273 28.067715 28.067715 0 0 0 2.699444 21.432936L178.195839 956.188661a28.165285 28.165285 0 0 0 38.442687 10.342449l1044.587328-601.976052a28.132762 28.132762 0 0 0 10.440019-38.442687l-148.924758-258.268509a28.197808 28.197808 0 0 0-38.442687-10.342449L39.711101 659.444942a28.230332 28.230332 0 0 0-7.512911 6.309544z" fill="#8a8a8a" p-id="4337"></path><path d="M498.941845 597.390249l-138.322121 79.877529 38.637827 66.933207q8.000762 13.854979 21.595554 5.98431l114.254788-65.957504a21.172749 21.172749 0 0 0 9.952167-11.123011q2.634397-9.757027-16.91218-47.321582l18.440781-4.130474q20.489757 43.22363 18.148071 56.167953a36.166047 36.166047 0 0 1-16.261712 19.514054l-123.068636 71.031158q-25.17313 14.603017-40.394092-11.77348L317.103383 639.020232l16.066571-9.269176 18.570875 32.133143 122.027886-70.47826-33.596697-58.249452-150.160648 86.707448-9.041511-15.611243 166.454883-96.106718zM691.903319 563.663459c-3.935334 3.837764-9.757027 9.399269-17.497602 16.619469l23.319295 40.394093-15.611244 9.008988-21.237795-36.816516q-31.027346 27.709957-64.754137 54.314118l-12.814229-13.39965 9.171605-7.382818 9.236653-7.122629-79.714912-138.126982-17.627696 10.179832-8.781324-15.155915L601.683341 414.836271l6.960013 12.06619 86.34969-49.858408 8.488614 14.733111q28.197808 65.82741 30.506972 123.39387a274.660314 274.660314 0 0 0 69.339939 27.612387l-3.642623 18.440781a322.177037 322.177037 0 0 1-65.534699-26.40902 220.899095 220.899095 0 0 1-15.38358 72.819946l-18.14807-6.179451a215.272542 215.272542 0 0 0 15.448626-77.340702 312.940384 312.940384 0 0 1-89.374369-86.739971l-8.748801 5.138701-7.2202-12.488995-17.172368 9.919644 71.876767 124.499667q10.570113-10.017215 17.465079-16.61947z m-134.32174-56.948515l40.166428-23.189202-19.969382-34.702493-40.166429 23.189201z m28.067714 48.785135l40.166429-23.189201-19.514055-33.921931-40.166428 23.189201z m48.557472-8.813847l-40.166428 23.189201 21.888264 37.922312q13.334604-10.92787 35.775766-30.767159z m7.2202-117.832365A289.848753 289.848753 0 0 0 715.515325 503.365031a330.437986 330.437986 0 0 0-26.441544-101.92841zM812.760362 400.460918l-4.813467 17.95293a280.482007 280.482007 0 0 0-56.167953-12.781706l5.073654-17.530125a291.637542 291.637542 0 0 1 55.907766 12.358901z m24.360045 28.78323a925.063745 925.063745 0 0 1 10.017214 101.895887l-18.440781 2.016452a812.792886 812.792886 0 0 0-8.878895-101.375512z m-45.923075-86.25212l-4.813467 18.017977a290.922026 290.922026 0 0 0-58.542163-11.513292l5.073655-17.497602a308.972527 308.972527 0 0 1 58.281975 10.992917z m48.459902-17.562649l-9.334223 13.724885A298.792695 298.792695 0 0 0 783.814515 315.477211l9.757027-14.180212a437.635191 437.635191 0 0 1 46.085692 24.13238zM834.355916 269.944418l16.521899-9.529363 35.157821 60.916373 48.199714-27.840051L1003.282579 413.047483q12.716659 22.115928-8.228426 34.214642l-26.018739 15.058345-13.237034-13.009369 25.238177-13.952549c6.992536-4.065428 8.45609-9.561887 4.423186-16.554423l-12.716659-22.018358-80.527997 46.475973L919.762427 491.1037l-16.066572 9.269176-81.926505-141.899698 47.744387-27.579864z m107.750103 73.763125l-14.830682-25.660981-80.56052 46.508496 14.830681 25.726028z m-72.592282 60.330952l14.700587 25.433317 80.560521-46.508496-14.700587-25.433318z m45.532793-166.064603a222.720407 222.720407 0 0 1-2.406733 56.13543l-16.456853 0.878132a242.722312 242.722312 0 0 0 2.081499-55.647578z" fill="#8a8a8a" p-id="4338"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/condition.svg b/src/assets/svgs/bpm/condition.svg new file mode 100644 index 0000000..41ea85d --- /dev/null +++ b/src/assets/svgs/bpm/condition.svg @@ -0,0 +1 @@ +<svg t="1729585232424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1602" width="200" height="200"><path d="M925.5 898.9H804.9c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V572.2c0-19-15.4-34.4-34.5-34.4H529.2V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H443.1c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V537.8H219.1c-19 0-34.5 15.4-34.5 34.4V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H98.5c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4H133V555c0-38 30.9-68.8 68.9-68.8h275.7V297.1h-34.5c-19 0-34.5-15.4-34.5-34.4V159.5c0-19 15.4-34.4 34.5-34.4h120.6c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4h-34.5v189.2h292.9c38.1 0 68.9 30.8 68.9 68.8v172h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 18.8-15.4 34.2-34.5 34.2z m0 0" p-id="1603" fill="#fff"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/copy.svg b/src/assets/svgs/bpm/copy.svg new file mode 100644 index 0000000..8ff3bba --- /dev/null +++ b/src/assets/svgs/bpm/copy.svg @@ -0,0 +1 @@ +<svg t="1729649333541" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1644" width="200" height="200"><path d="M647.888 893.84L491.904 571.52l393.888-393.888-237.904 716.208zM872.32 123.232L459.872 535.68 134.96 380.88 872.32 123.232z m90.72-68.32a23.968 23.968 0 0 0-24.784-5.568L64.08 354.816a23.984 23.984 0 0 0-2.4 44.32l381.392 181.728 187.36 387.088a24.048 24.048 0 0 0 23.152 13.504 24.032 24.032 0 0 0 21.232-16.4L968.96 79.552c2.88-8.672 0.592-18.24-5.92-24.64z" fill="#fff" p-id="1645"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/delay.svg b/src/assets/svgs/bpm/delay.svg new file mode 100644 index 0000000..cbc31df --- /dev/null +++ b/src/assets/svgs/bpm/delay.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1735905505218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4277" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M561.778 454.929h198.117c0.549 0 0.994 0.444 0.994 1.001v97.553a0.998 0.998 0 0 1-0.994 1.001H463.224a1.005 1.005 0 0 1-1.002-1V207.04c0-0.552 0.444-1 1.002-1h97.552c0.553 0 1.002 0.455 1.002 1v247.89zM512 952.706c-247.424 0-448-200.576-448-448 0-247.423 200.576-448 448-448s448 200.577 448 448c0 247.424-200.576 448-448 448z m0-99.555c192.44 0 348.444-156.004 348.444-348.445 0-192.44-156.003-348.444-348.444-348.444-192.44 0-348.444 156.004-348.444 348.444 0 192.441 156.003 348.445 348.444 348.445z" fill="#3296FA" p-id="4278"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/finish.svg b/src/assets/svgs/bpm/finish.svg new file mode 100644 index 0000000..674c6df --- /dev/null +++ b/src/assets/svgs/bpm/finish.svg @@ -0,0 +1 @@ +<svg t="1730189225011" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2651" id="mx_n_1730189225011" width="200" height="200"><path d="M793.889347 200.380242c27.648573 20.615681 42.196018 32.710677 63.781037 56.119312 25.313864 27.453234 43.242957 48.52047 64.502857 86.507991 44.537416 79.580127 53.527718 136.949077 53.517684 212.063821 0 64.933675-15.452562 130.459388-40.138263 187.311893-22.076044 50.841799-61.545336 104.359483-101.886297 138.933914-45.506755 39.001681-81.214423 60.462941-137.605337 81.826531-55.699867 21.102023-114.070267 28.641326-181.379458 27.791064-68.274516-0.862973-129.364283-11.040029-180.533878-31.80489-46.159002-18.731189-98.338744-46.827973-141.596418-87.541551-43.946046-41.361142-70.369064-75.958317-93.88139-127.198155-26.157437-57.004361-40.094111-129.065922-39.680686-191.781288 0-36.980719 4.033895-70.902234 12.252873-105.241856 8.532726-35.651474 20.069131-69.572989 38.13135-102.35257 18.856956-34.221214 36.754607-62.067803 58.869452-88.973149 23.248751-28.285434 39.2104-46.417894 64.295476-63.475987 18.297696-12.442861 36.879036-9.295353 47.199252-2.306612 4.403836 2.982273 8.919391 6.577992 12.933218 12.933217 9.572307 15.156208-0.334486 29.769212-6.69038 38.465836-7.148625 9.781026-23.130343 26.023643-38.738775 43.218205-38.192895 42.075603-55.133918 65.965228-74.986303 106.965794-30.772668 63.552249-37.495827 115.718611-38.131349 166.573791-0.668971 53.517684 9.995096 99.647251 27.427813 140.483919 33.916163 80.572211 94.807915 144.44289 175.270414 178.615938 41.108271 17.845472 113.812713 37.319888 181.960793 38.13135 56.193568 0.668971 125.919751-11.321666 166.574459-28.096784 45.935566-18.954626 97.223569-56.862539 127.10383-94.324918 23.013273-28.852721 52.179742-70.910931 64.413884-105.694749 14.863868-42.260239 24.806784-87.661297 24.559934-132.458943 0-54.414105-11.53373-108.417461-36.918505-156.856317-20.16747-38.483228-46.480777-74.607665-84.66899-108.048189-13.377414-11.714352-23.822728-20.067124-38.808348-31.619586-10.191774-7.857065-36.059546-25.027545-28.923632-47.326356 4.970455-15.53217 18.303717-25.294464 31.887843-27.205046 19.456354-2.736092 28.565733 2.427027 43.705885 12.041479l6.179955 4.322891zM510.755379 531.65738c-8.696624-0.668971-10.034566-0.446204-20.738102-6.689711-11.031333-6.434832-17.839451-21.183637-16.514219-35.175166V92.220334c0-18.178619 0.386665-22.815926 8.988295-31.685813 5.351768-5.519011 10.963097-11.381873 26.08987-11.539751 16.055305-0.167243 21.407073 3.846584 27.929542 9.700081 9.70677 8.711341 10.703537 17.56049 10.377078 33.525483v397.5715c-0.509756 15.273947 0.326458 22.967114-11.380535 33.502739-3.884046 3.495374-8.027653 7.693167-20.96087 8.362138l-3.791059 0.000669z m4.453341 0.573308" p-id="2652" fill="#ffffff"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/parallel.svg b/src/assets/svgs/bpm/parallel.svg new file mode 100644 index 0000000..ba0ac67 --- /dev/null +++ b/src/assets/svgs/bpm/parallel.svg @@ -0,0 +1 @@ +<svg t="1729585239190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1755" width="200" height="200"><path d="M901.489435 536.822664v-0.931601l-1.001722-198.240726c-0.100172-19.162936-9.21584-37.474409-25.043042-50.246361-14.024104-11.349507-32.265456-17.60025-51.348255-17.610268l-618.062295-0.18031c-19.142902 0-37.424323 6.280795-51.478478 17.690405-15.827203 12.842072-24.902802 31.2437-24.892785 50.486775v196.798247A114.987635 114.987635 0 1 0 195.295664 536.922836V338.782282c1.15198-1.252152 4.808264-3.596181 10.768509-3.596181l276.725622 0.090155v199.753326a114.987635 114.987635 0 1 0 65.612772 1.412428V335.326342l275.693849 0.080138c6.01033 0 9.626546 2.344029 10.768508 3.596181l1.001722 195.70637a114.987635 114.987635 0 1 0 65.592737 2.113633zM214.979496 645.910158a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m354.689623 0a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m295.507904 56.437001a56.437001 56.437001 0 1 1 56.437001-56.437001 56.507122 56.507122 0 0 1-56.457035 56.437001z" p-id="1756" fill='#fff'></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/reject.svg b/src/assets/svgs/bpm/reject.svg new file mode 100644 index 0000000..21fd5f6 --- /dev/null +++ b/src/assets/svgs/bpm/reject.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724316570161" class="icon" viewBox="0 0 1185 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1505" xmlns:xlink="http://www.w3.org/1999/xlink" width="231.4453125" height="200"><path d="M414.276535 230.004913l-2.443086-31.647244 26.446614 17.351559 29.437984-11.852598-8.143622 30.31685 20.423559 24.221229-31.623055 1.475527-16.722646 26.801386-11.239811-29.760504-30.663559-7.522772zM581.664252 176.902047l13.884472-28.542992 14.206993 28.220473 31.42148 4.321763-22.350614 22.092599 5.684409 31.123149-28.180157-14.513385-27.897953 14.819779 5.28126-31.066709-22.76989-21.689448zM896.507969 672.735748l17.754708 26.398236-31.494047-2.064126-19.560819 24.705008-7.95011-30.502299-29.575055-11.02211 26.744945-16.771024 1.104629-31.526299 24.414741 20.197795 30.268472-8.619338zM777.030551 801.961323l2.112504 31.647244-26.446614-17.682142-29.413795 11.546205 8.466141-30.308787-20.092976-24.221229 31.606929-1.153008 17.045166-26.793323 10.86085 29.704063 30.647433 7.837229zM609.312252 853.451591l-14.198929 28.518803-14.110236-28.542992-31.405355-4.636221 22.673134-22.084535-5.36189-31.12315 27.833449 14.835906 28.188221-14.803654-5.28126 31.066709 22.76989 22.060346zM298.435528 354.828094l-17.448315-26.390173 31.485984 2.394709 19.875275-24.753386 7.611465 30.865134 29.583118 11.288189-27.011024 16.779087-1.419086 31.526299-24.084158-20.504189-30.518425 8.280693zM962.56 91.53915a43.636913 43.636913 0 0 1 59.375874 15.601889l138.627024 236.753638c12.175118 20.447748 5.12 47.208819-15.609953 59.375874L229.13411 938.185575a43.636913 43.636913 0 0 1-59.375874-15.60189L31.12315 685.773606a43.636913 43.636913 0 0 1 15.601889-59.319433z m25.672567 24.108346a13.594205 13.594205 0 0 0-10.441575 1.548095L61.625449 652.054173a13.586142 13.586142 0 0 0-4.853921 18.83515l138.643149 236.793953a13.586142 13.586142 0 0 0 18.843213 4.837795l915.818834-534.915024a13.957039 13.957039 0 0 0 5.160315-18.778708l-138.602834-236.78589a13.594205 13.594205 0 0 0-8.401638-6.393953z" fill="#F5222D" p-id="1506"></path><path d="M395.981606 172.338394c123.670173-72.349228 271.11811-69.462677 388.394331-5.12l29.623433-17.335433a414.574866 414.574866 0 0 0-112.107842-47.071748 429.991307 429.991307 0 0 0-162.009701-10.498016 412.792945 412.792945 0 0 0-158.80063 54.707401 417.856504 417.856504 0 0 0-125.363402 111.922394A426.282331 426.282331 0 0 0 185.206929 405.004094a417.348535 417.348535 0 0 0-13.529701 120.977134l29.623433-17.335433c1.386835-133.958551 70.688252-263.958173 194.672882-336.307401z m397.666772 679.484472c-123.670173 72.365354-271.110047 69.462677-388.394331 5.128063l-29.623433 17.335433a414.679685 414.679685 0 0 0 112.075591 47.087874 429.991307 429.991307 0 0 0 162.009701 10.498016 412.744567 412.744567 0 0 0 158.808692-54.707402 423.145827 423.145827 0 0 0 209.105638-378.976756l-29.623433 17.335434c-1.072378 133.974677-70.712441 263.95011-194.350362 336.307401h-0.008063z" fill="#F5222D" p-id="1507"></path><path d="M478.377323 313.110173a226.271748 226.271748 0 0 1 109.979212-31.219905l45.668788-26.761071c-58.634079-9.13537-118.316346 2.314079-170.612914 32.735748a258.693039 258.693039 0 0 0-111.91433 132.71685l45.67685-26.761071a230.359685 230.359685 0 0 1 81.097575-80.589606l0.104819-0.120945z m232.568945 397.674835a226.328189 226.328189 0 0 1-109.979213 31.227968l-45.668787 26.753008c58.634079 9.13537 118.316346-2.314079 170.612913-32.735748a258.709165 258.709165 0 0 0 111.914331-132.71685l-45.676851 26.761071a225.215496 225.215496 0 0 1-81.097574 80.597669l-0.104819 0.112882zM188.57726 706.938961l-10.062614-17.424126 109.938897-63.471874 9.578835 16.585574 17.093543-9.869102-18.770645-32.509984-63.689575 36.767244c-4.047622-3.918614-7.804976-7.337323-11.272063-10.24l-16.859717 13.747401c3.249386 2.144756 6.595528 4.458835 9.869103 7.038993l-62.173733 35.896441 19.254426 33.348535 17.093543-9.869102zM317.44 781.142677l-19.060913-33.017953 32.679307-18.867401 4.741039 8.216189 17.093543-9.869103-48.474708-83.959937-49.772851 28.736504-7.933984-13.747401-17.432189 10.062614 7.933984 13.747402-49.264882 28.446236 48.764977 84.459842 17.093543-9.869102-5.031307-8.708032 32.171339-18.585196 19.060913 33.017952 17.432189-10.062614z m-12.505701-97.126803l-32.679307 18.867402-8.321008-14.41663 32.679307-18.867402 8.321008 14.41663z m-50.111496 28.930016l-32.171338 18.577134-8.321008-14.41663 32.171338-18.577134 8.321008 14.41663z m16.932284 29.325102l-32.171339 18.577134-8.127496-14.077984 32.171339-18.577134 8.127496 14.077984z m50.111496-28.930016l-32.679307 18.867402-8.127496-14.077984 32.679307-18.867402 8.127496 14.077984z m95.828661 7.684032c11.062425-6.38589 13.368441-15.537386 6.692284-27.099717l-25.05978-43.411149c3.55578-4.289512 7.014803-8.740283 10.48189-13.199118l-9.482079-16.424315c-3.467087 4.458835-6.92611 8.917669-10.48189 13.199118l-17.803086-30.832882 14.755275-8.51452-9.780409-16.932283-14.747213 8.522582-16.738771-28.994519-17.093544 9.869102 16.738772 28.99452-16.924221 9.772346 9.772347 16.924221 16.932283-9.772347 20.891213 36.202835a299.927181 299.927181 0 0 1-16.690394 15.214866l13.868347 14.344063a572.617575 572.617575 0 0 0 12.497638-12.804031l19.157669 33.179212c2.322142 4.031496 1.475528 7.200252-2.20926 9.328882-3.85411 2.225386-8.167811 4.039559-12.578268 5.692472l13.55389 14.964914 14.247307-8.224252z m111.390236-65.205417c6.369764-3.676724 10.15937-8.329071 11.151118-13.586142 1.225575-5.619906-3.201008-18.706142-13.182992-39.089386l-18.827086 4.160504c7.627591 14.368252 11.368819 23.164976 11.570393 26.615937 0.112882 3.289701-0.959496 5.692472-3.467086 7.143811l-6.539087 3.77348c-3.354205 1.935118-6.095622 1.064315-8.224252-2.628535l-38.702362-67.027654c8.933795-10.07874 17.762772-21.874898 26.390173-35.573921l-18.383622-8.603213a168.443969 168.443969 0 0 1-17.972409 26.914268l-26.801386-46.426709-17.254803 9.965859 77.686929 134.571338c6.966425 12.070299 16.077606 15.077795 27.478677 8.498394l15.077795-8.708031z m-78.501291 45.547842c13.626457-12.779843 25.285543-25.100094 34.783748-37.291339l-12.473449-14.247307a157.808882 157.808882 0 0 1-14.706897 17.875654l-38.412095-66.535811 20.617071-11.900976-9.869102-17.093544-20.617071 11.900977-27.18841-47.087874-17.254803 9.965858 72.94589 126.363212c2.999433 5.192567 2.418898 9.99811-1.564221 14.311811l13.739339 13.739339z m201.663496-113.978457l-65.21348-112.946393c0.137071-7.901732-0.16126-15.771213-0.886929-23.624567l53.78822-31.050583-9.869102-17.093543-144.795213 83.597102 9.869102 17.093543 71.05915-41.024504c1.894803 37.331654-9.45789 76.517795-33.848441 117.856756l20.367118 8.570961c14.860094-26.898142 25.05978-53.344756 30.445859-79.243087l50.990362 88.313953 18.093354-10.449638z m28.728441-76.017889l5.716661-21.850709c-21.157291-7.224441-45.330142-12.707276-72.349228-16.54526l-5.603779 19.318929c29.163843 4.837795 53.385071 11.191433 72.244409 19.07704z m18.738394-105.33493l5.265134-19.13348c-12.739528-4.25726-27.414173-7.627591-43.612725-10.127118l-5.410268 18.101417c17.674079 2.74948 32.380976 6.555213 43.757859 11.159181z m88.934803 67.74526l-15.76315-27.317417 21.786205-12.578268 15.674457 27.148095 16.085669-9.288567-15.674457-27.148095 22.455433-12.965291 4.063748 7.038992c2.031874 3.523528 1.249764 6.426205-2.435023 8.554835l-11.852599 6.176252 12.175118 12.183181 12.570205-7.256693c11.393008-6.579402 13.997354-15.230992 7.998488-25.616126l-42.862866-74.244032-33.848441 19.544693-0.532157-0.145133a202.445606 202.445606 0 0 0 18.738393-38.750741L790.173228 306.87748l-92.676031 53.506016 8.321008 14.41663 31.679496-18.286866-3.85411 13.836094c8.401638-0.16126 16.125984 0.08063 23.261732 0.427339l-37.202646 21.479811 52.538457 90.998929 16.424315-9.482079z m-25.35811-117.856756c-6.724535-0.806299-14.126362-1.233638-21.947465-1.628724l33.517858-19.351181c-3.305827 7.047055-7.143811 13.948976-11.570393 20.979905z m47.571653 16.996788l-22.455433 12.965291-6.095622-10.56252 22.455433-12.965291 6.095622 10.56252z m-38.541102 22.253858l-21.786205 12.578268-6.095622-10.56252 21.786205-12.578268 6.095622 10.56252z m-24.253481 137.570772c-0.330583-19.915591 1.112693-30.582929 4.458835-32.518048 1.846425-1.064315 4.628157-0.886929 8.627402 0.604725 8.304882 2.797858 16.400126 3.265512 24.269606 1.402961 8.006551-2.386646 17.464441-6.506835 28.462362-12.626646 10.812472-6.031118 20.96378-11.66715 30.187843-16.988725l38.379842-22.157102-5.781165-19.673701c-4.329827 2.942992-10.675402 7.055118-19.028662 12.320252-8.708031 5.031307-16.996787 10.038425-25.374236 14.876221-13.07011 7.546961-24.398614 13.868346-34.211275 19.302803-10.07874 5.378016-18.230425 8.296819-24.543748 8.587087-5.28126 0.145134-11.070488-0.983685-17.440252-3.120378l-2.902678-0.774048-36.767244-63.681511-38.379842 22.157102 9.288567 16.085669 22.116787-12.771779 26.511118 45.91874c-4.571717 7.555024-7.014803 20.359055-7.651779 38.605606l19.778519 4.450772z m38.476599-112.938331l-21.786205 12.578268-6.095622-10.56252 21.786205-12.578268 6.095622 10.56252z m38.541102-22.253858l-22.455433 12.965291-6.095622-10.56252 22.455433-12.957228 6.095622 10.56252z m172.241638-43.798173c12.062236-6.966425 14.610142-16.488819 7.740472-28.381733l-39.863433-69.051464 23.302048-13.449071-9.869103-17.093543-23.302047 13.44907-14.513386-25.132346-17.424126 10.062614 14.513386 25.132347-62.681701 36.186708 9.869103 17.093544 62.6817-36.186709 37.34778 64.689386c2.515654 4.354016 1.523906 8.062992-2.838173 10.578645-6.692283 3.870236-14.190866 7.522772-21.955528 11.110804l13.529701 14.537574 23.463307-13.545826z m-130.942992-43.725607l5.386079-20.092976c-12.900787-4.168567-27.389984-7.200252-43.65304-9.433701l-5.321575 18.27074c17.682142 2.74948 32.219717 6.643906 43.596599 11.255937z m80.702488 27.148095l8.466142-17.851465c-10.756031-5.853732-24.825953-12.038047-41.846929-18.302992l-8.740284 16.22274c16.883906 6.789039 30.808693 13.497449 42.121071 19.931717z m-31.219905 99.577952c-0.354772-20.350992 1.064315-31.445669 4.418519-33.380787 2.007685-1.161071 5.128063-1.177197 9.119244 0.32252a42.951559 42.951559 0 0 0 24.938835 1.007874c8.175874-2.483402 18.141732-6.893858 29.639559-13.303937 11.320441-6.321386 21.810394-12.150929 31.365039-17.657953l35.525544-20.520315-5.966614-20.012346c-3.999244 2.74948-10.006173 6.668094-17.859528 11.651023-7.95011 4.805543-15.722835 9.522394-23.439118 13.973166a2406.72252 2406.72252 0 0 1-35.719055 20.181669c-10.586709 5.66022-19.165732 8.603213-25.712882 9.256315-5.28126 0.145134-11.401071-0.790173-17.940158-2.822047l-3.080063-0.685355-36.767244-63.681512-39.041008 22.544126 9.482079 16.424315 22.455433-12.965291 26.511118 45.91874c-4.57978 7.555024-7.256693 20.72189-7.700157 39.299024l19.770457 4.450771z" fill="#F5222D" p-id="1508"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/running.svg b/src/assets/svgs/bpm/running.svg new file mode 100644 index 0000000..5908c13 --- /dev/null +++ b/src/assets/svgs/bpm/running.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724304256588" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1272" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#2196F3" p-id="1273"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#2196F3" p-id="1274"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#2196F3" p-id="1275"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#2196F3" p-id="1276"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#2196F3" p-id="1277"></path><path d="M477.090909 500.945455l22.109091 38.283636-15.36 8.843636-13.963636-24.436363-151.272728 87.621818 14.545455 25.134545-15.243636 8.843637-23.272728-39.330909L377.949091 558.545455c-6.050909-4.887273-11.636364-8.843636-15.825455-11.636364l14.894546-12.450909c3.956364 3.141818 9.658182 8.145455 17.105454 14.894545zM459.869091 698.181818l-48.407273 28.043637 7.447273 12.334545-15.36 8.843636-61.207273-106.007272L406.225455 605.090909l-12.683637-21.876364 15.709091-9.076363 12.683636 21.876363L486.4 558.545455l60.509091 104.727272-15.36 8.843637-7.098182-12.218182-49.105454 28.392727L501.294545 733.090909l-15.70909 9.076364z m-45.381818-78.661818l-48.523637 27.461818 14.545455 25.134546 48.523636-28.043637zM388.538182 686.545455l14.545454 25.134545 48.523637-28.043636-14.545455-25.134546z m105.425454-79.476364L479.418182 581.818182 430.545455 609.861818l14.545454 25.134546z m-26.647272 67.490909l49.221818-28.392727-14.545455-25.134546-49.105454 28.392728zM624.058182 541.090909c-4.654545 6.167273-10.123636 13.149091-16.290909 20.829091l34.909091 61.207273a18.734545 18.734545 0 0 1-6.632728 29.207272l-18.734545 10.938182-11.636364-13.614545a174.545455 174.545455 0 0 0 17.454546-8.610909 8.378182 8.378182 0 0 0 2.327272-12.683637l-30.021818-52.363636-9.774545 10.24-9.890909 10.123636-12.450909-12.916363c9.076364-8.145455 16.872727-15.709091 23.272727-22.574546l-30.836364-53.527272-24.785454 14.196363-8.727273-15.010909L546.909091 492.218182l-23.272727-40.378182 15.36-8.843636 23.272727 40.378181 21.643636-12.450909 8.727273 15.127273-21.643636 12.450909L599.156364 546.909091c5.352727-6.4 10.821818-13.381818 16.290909-20.712727z m8.843636-45.032727L689.221818 593.454545a193.745455 193.745455 0 0 0 22.574546-27.112727l11.636363 13.032727a363.985455 363.985455 0 0 1-41.192727 44.8l-12.334545-12.450909a10.821818 10.821818 0 0 0 1.62909-13.730909l-98.90909-171.403636 15.476363-8.96 36.305455 62.952727 30.836363-17.803636 8.029091 15.476363z m128 81.454545a20.130909 20.130909 0 0 1-30.836363-9.541818L628.363636 392.378182l15.36-8.378182 38.050909 66.094545A206.08 206.08 0 0 0 709.818182 409.018182l16.64 7.563636a297.890909 297.890909 0 0 1-34.909091 48.64l52.712727 91.112727a8.843636 8.843636 0 0 0 13.614546 4.072728l10.821818-6.167273a14.429091 14.429091 0 0 0 7.912727-11.636364 102.981818 102.981818 0 0 0-15.010909-38.516363l17.105455-3.723637c12.334545 24.669091 17.687273 41.076364 16.058181 48.989091a28.16 28.16 0 0 1-15.127272 18.152728zM805.236364 288.116364l16.174545-9.309091 23.272727 39.330909 78.429091-45.265455 59.345455 102.749091-16.64 9.076364-7.912727-13.847273L896 407.272727l42.938182 74.472728-16.174546 9.30909-42.938181-74.472727-62.603637 36.072727 8.029091 13.73091-15.825454 9.192727L749.730909 372.363636l78.196364-45.265454z m2.676363 149.061818l62.603637-36.072727-33.745455-58.181819-62.487273 36.072728z m78.778182-45.381818l62.72-36.189091-33.745454-58.181818-62.72 36.072727z" fill="#2196F3" p-id="1278"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/simple-process-bg.svg b/src/assets/svgs/bpm/simple-process-bg.svg new file mode 100644 index 0000000..eb23ab5 --- /dev/null +++ b/src/assets/svgs/bpm/simple-process-bg.svg @@ -0,0 +1 @@ +<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FAFAFA" d="M0 0h22v22H0z"/><circle fill="#919BAE" cx="1" cy="1" r="1"/></g></svg> \ No newline at end of file diff --git a/src/assets/svgs/bpm/starter.svg b/src/assets/svgs/bpm/starter.svg new file mode 100644 index 0000000..c12c712 --- /dev/null +++ b/src/assets/svgs/bpm/starter.svg @@ -0,0 +1 @@ +<svg t="1729561814171" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1359" width="200" height="200"><path d="M674.496 603.456c120.256 0 218.176 90.752 221.44 203.84l0.064 5.888v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352a21.568 21.568 0 0 1-22.144-20.928v-125.888c0-67.712-56.512-123.264-128-125.76l-4.928-0.064H349.568c-71.488 0-130.176 53.504-132.864 121.152l-0.064 4.672v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352A21.568 21.568 0 0 1 128 939.072v-125.888c0-113.92 95.872-206.528 215.36-209.664l6.208-0.064h324.928zM497.216 128c122.368 0 221.568 93.888 221.568 209.728s-99.2 209.792-221.568 209.792c-122.304 0-221.44-93.952-221.44-209.728C275.712 221.952 374.848 128 497.152 128z m0 83.904c-73.408 0-132.864 56.32-132.864 125.888 0 69.504 59.52 125.824 132.864 125.824 73.408 0 132.928-56.32 132.928-125.824 0-69.504-59.52-125.888-132.928-125.888z" fill="#fff" p-id="1360"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/alipay_app.svg b/src/assets/svgs/pay/icon/alipay_app.svg deleted file mode 100644 index ebf1188..0000000 --- a/src/assets/svgs/pay/icon/alipay_app.svg +++ /dev/null @@ -1 +0,0 @@ -<svg t="1627279997305" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11904" width="40" height="40"><path d="M938.7008 669.525333L938.7008 249.412267c0-90.555733-73.5232-164.078933-164.1472-164.078933L249.378133 85.333333c-90.555733 0-164.078933 73.48906699-164.078933 164.078933l0 525.2096c0 90.555733 73.454933 164.078933 164.07893301 164.078933l525.20959999 0c80.725333 0 147.8656-58.368 161.553067-135.099733-43.52-18.8416-232.106667-100.283733-330.376533-147.182933-74.786133 90.589867-153.088 144.930133-271.121067 144.930133s-196.81279999-72.704-187.357867-161.655467c6.2464-58.402133 46.2848-153.9072 220.296533-137.5232 91.682133 8.6016 133.666133 25.736533 208.418133 50.414933 19.3536-35.4304 35.4304-74.513067 47.616-116.0192L292.0448 436.565333l0-32.8704 164.0448 0 0-58.9824L256 344.712533l1e-8-36.181333 200.12373299 0L456.123733 223.3344c0 0 1.809067-13.312 16.520533-13.31200001l82.056533 1e-8 0 98.474667 213.333333 0 0 36.181333-213.333333 1e-8 0 58.98239999 174.045867 0c-16.00853301 65.1264-40.277333 124.962133-70.690133 177.220267C708.608 599.176533 938.7008 669.525333 938.7008 669.525333L938.7008 669.525333 938.7008 669.525333 938.7008 669.525333zM321.57013299 744.994133c-124.7232 0-144.452267-78.7456-137.83039999-111.65013299 6.5536-32.733867 42.666667-75.502933 112.0256-75.50293301 79.6672 0 151.04 20.445867 236.714667 62.088533C472.302933 698.333867 398.370133 744.994133 321.57013299 744.994133L321.57013299 744.994133 321.57013299 744.994133zM321.57013299 744.994133" fill="#1296db" p-id="11905"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/alipay_bar.svg b/src/assets/svgs/pay/icon/alipay_bar.svg deleted file mode 100644 index eb1e1e8..0000000 --- a/src/assets/svgs/pay/icon/alipay_bar.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279586085" class="icon" viewBox="0 0 1036 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6737" xmlns:xlink="http://www.w3.org/1999/xlink" width="40.46875" height="40"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); } -</style></defs><path d="M27.587124 336.619083h69.148134a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916A13.978733 13.978733 0 0 0 96.735258 0.011183H27.587124a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m165.880969 0h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-27.584701a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m138.109886 322.629167h-110.525185a27.771084 27.771084 0 0 0-27.584701 28.14385v111.829867a27.771084 27.771084 0 0 0 27.584701 28.14385h110.525185a27.957467 27.957467 0 0 0 27.584701-28.14385v-111.829867a27.957467 27.957467 0 0 0-27.584701-28.14385z m484.596091-322.629167h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-27.5847a13.978733 13.978733 0 0 0-13.978734 13.978733v308.650434a13.978733 13.978733 0 0 0 13.978734 13.978733z m-469.871825 0H428.68358a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916A13.978733 13.978733 0 0 0 428.68358 0.011183h-83.126867a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m594.189361 0h69.148134a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-69.148135a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m-412.279444 126.181367H66.91396A67.470687 67.470687 0 0 0 0.002423 530.830286v425.139878a67.470687 67.470687 0 0 0 66.911537 68.029836h418.802853a67.470687 67.470687 0 0 0 66.911537-68.029836V487.775787a24.788954 24.788954 0 0 0-24.416188-24.975337z m-58.337914 433.899885a42.681733 42.681733 0 0 1-42.495349 43.054498H125.438257a42.681733 42.681733 0 0 1-42.495349-43.054498V590.100115a42.681733 42.681733 0 0 1 42.495349-43.054498h301.940642a42.681733 42.681733 0 0 1 42.495349 43.054498z m525.22761-433.899885a41.749817 41.749817 0 0 0-41.377051 42.122583v55.914934a41.377051 41.377051 0 1 0 82.940485 0v-55.914934a41.749817 41.749817 0 0 0-41.563434-42.122583z m0 223.659734a41.749817 41.749817 0 0 0-41.377051 42.122584V894.65012a45.477479 45.477479 0 0 1-45.291096 45.850246h-159.730327a43.240882 43.240882 0 0 0-43.613649 37.276622A41.9362 41.9362 0 0 0 745.534871 1024h233.538039a57.778765 57.778765 0 0 0 57.405999-58.337914V729.3283a41.749817 41.749817 0 0 0-41.377051-41.9362zM732.488053 322.64035V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-82.940485a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733h82.940485a13.978733 13.978733 0 0 0 13.79235-13.978733zM532.126208 0.011183c-11.36937 0-20.688525 6.337026-20.688526 13.978733v308.650434c0 7.828091 9.319156 13.978733 20.688526 13.978733s20.688525-6.337026 20.688525-13.978733V13.989916c0-7.641708-9.319156-13.978733-20.688525-13.978733z" p-id="6738" fill="#1977FD"></path><path d="M745.534871 462.80045a41.749817 41.749817 0 0 0-41.377051 42.122583v252.549117a41.377051 41.377051 0 1 0 82.940485 0V504.923033A41.749817 41.749817 0 0 0 745.534871 462.80045" p-id="6739" fill="#1977FD"></path></svg> diff --git a/src/assets/svgs/pay/icon/alipay_pc.svg b/src/assets/svgs/pay/icon/alipay_pc.svg deleted file mode 100644 index 2a75277..0000000 --- a/src/assets/svgs/pay/icon/alipay_pc.svg +++ /dev/null @@ -1 +0,0 @@ -<svg t="1627279878333" class="icon" viewBox="0 0 1285 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8535" width="40" height="40"><path d="M1141.76 855.04h-286.72c0 40.96 30.72 71.68 71.68 71.68h107.52c20.48 0 35.84 15.36 35.84 35.84s-15.36 35.84-35.84 35.84h-783.36c-20.48 0-35.84-15.36-35.84-35.84s15.36-35.84 35.84-35.84h107.52c40.96 0 71.68-30.72 71.68-71.68h-286.72c-76.8 0-143.36-61.44-143.36-143.36v-568.32c0-76.8 61.44-143.36 143.36-143.36h993.28c76.8 0 143.36 61.44 143.36 143.36v568.32c5.12 76.8-56.32 143.36-138.24 143.36z m71.68-711.68c0-40.96-30.72-71.68-71.68-71.68h-993.28c-40.96 0-71.68 30.72-71.68 71.68v568.32c0 40.96 30.72 71.68 71.68 71.68h993.28c40.96 0 71.68-30.72 71.68-71.68v-568.32z m-143.36 568.32h-855.04c-40.96 0-71.68-30.72-71.68-71.68v-424.96c0-40.96 30.72-71.68 71.68-71.68h855.04c40.96 0 71.68 30.72 71.68 71.68v424.96c0 40.96-30.72 71.68-71.68 71.68z" p-id="8536" fill="#1977FD"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/alipay_qr.svg b/src/assets/svgs/pay/icon/alipay_qr.svg deleted file mode 100644 index 4833750..0000000 --- a/src/assets/svgs/pay/icon/alipay_qr.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279238245" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4112" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); } -</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#1977FD" p-id="4113"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/alipay_wap.svg b/src/assets/svgs/pay/icon/alipay_wap.svg deleted file mode 100644 index 87075db..0000000 --- a/src/assets/svgs/pay/icon/alipay_wap.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1645964864184" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8460" xmlns:xlink="http://www.w3.org/1999/xlink" width="40" height="40"><defs><style type="text/css"></style></defs><path d="M768.3 0 255.7 0c-70.8 0-128.1 57.4-128.1 128.1l0 767.8c0 70.8 57.4 128.1 128.1 128.1L512 1024l256.3 0c70.8 0 128.1-57.4 128.1-128.1L896.4 128.1C896.4 57.3 839 0 768.3 0zM383.9 96.1c0-17.7 14.3-32 32-32l192.2 0c17.7 0 32 14.3 32 32l0 0c0 17.7-14.3 32-32 32L415.9 128.1C398.2 128.1 383.9 113.8 383.9 96.1L383.9 96.1zM512 959.9 512 959.9 512 959.9c-35.4 0-64.1-28.8-64.1-64.1 0-35.4 28.7-64.1 64.1-64.1l0 0 0 0c35.4 0 64.1 28.7 64.1 64.1C576.1 931.1 547.4 959.9 512 959.9zM832.3 755.6c0 6.7-5.4 12.2-12.2 12.2L203.9 767.8c-6.7 0-12.2-5.4-12.2-12.2L191.7 204.3c0-6.7 5.4-12.2 12.2-12.2l616.3 0c6.7 0 12.2 5.4 12.2 12.2L832.4 755.6z" p-id="8461" fill="#1977FD"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/mock.svg b/src/assets/svgs/pay/icon/mock.svg deleted file mode 100644 index 27b09ea..0000000 --- a/src/assets/svgs/pay/icon/mock.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1676209854312" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3033" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M173.077333 362.666667l91.114667-214.677334a65.6 65.6 0 0 1 86.016-34.773333c11.584 4.906667 24.96 10.282667 40.896 16.448 8.277333 3.2 16.789333 6.464 27.904 10.666667 28.202667 10.709333 39.296 14.933333 46.144 17.642666l51.477333-51.669333c28.181333-28.16 74.112-27.946667 102.570667 0.533333l195.925333 195.925334c16.426667 16.426667 23.445333 38.634667 21.056 59.904H896a42.666667 42.666667 0 0 1 42.666667 42.666666v490.666667a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V405.333333a42.666667 42.666667 0 0 1 42.666667-42.666666h45.077333z m48.96 0h39.104l169.194667-169.770667-27.328-10.389333c-11.2-4.245333-19.818667-7.530667-28.224-10.794667a1459.2 1459.2 0 0 1-42.197333-17.002667 20.522667 20.522667 0 0 0-26.901334 10.88L222.037333 362.666667z m108.842667 0h454.954667a23.509333 23.509333 0 0 0-5.290667-25.322667l-195.925333-195.925333a23.36 23.36 0 0 0-33.024-0.213334L330.88 362.666667zM128 405.333333v490.666667h768V405.333333H128z m597.333333 320a85.333333 85.333333 0 1 1 0-170.666666 85.333333 85.333333 0 0 1 0 170.666666z m0-42.666666a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z" fill="#4296d5" p-id="3034"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/wx_app.svg b/src/assets/svgs/pay/icon/wx_app.svg deleted file mode 100644 index ad40b2a..0000000 --- a/src/assets/svgs/pay/icon/wx_app.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279375144" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4399" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); } -</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#04C361" p-id="4400"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/wx_bar.svg b/src/assets/svgs/pay/icon/wx_bar.svg deleted file mode 100644 index 11292e6..0000000 --- a/src/assets/svgs/pay/icon/wx_bar.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279586085" class="icon" viewBox="0 0 1036 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6737" xmlns:xlink="http://www.w3.org/1999/xlink" width="40.46875" height="40"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }</style></defs><path d="M27.587124 336.619083h69.148134a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916A13.978733 13.978733 0 0 0 96.735258 0.011183H27.587124a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m165.880969 0h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-27.584701a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m138.109886 322.629167h-110.525185a27.771084 27.771084 0 0 0-27.584701 28.14385v111.829867a27.771084 27.771084 0 0 0 27.584701 28.14385h110.525185a27.957467 27.957467 0 0 0 27.584701-28.14385v-111.829867a27.957467 27.957467 0 0 0-27.584701-28.14385z m484.596091-322.629167h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-27.5847a13.978733 13.978733 0 0 0-13.978734 13.978733v308.650434a13.978733 13.978733 0 0 0 13.978734 13.978733z m-469.871825 0H428.68358a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916A13.978733 13.978733 0 0 0 428.68358 0.011183h-83.126867a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m594.189361 0h69.148134a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-69.148135a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m-412.279444 126.181367H66.91396A67.470687 67.470687 0 0 0 0.002423 530.830286v425.139878a67.470687 67.470687 0 0 0 66.911537 68.029836h418.802853a67.470687 67.470687 0 0 0 66.911537-68.029836V487.775787a24.788954 24.788954 0 0 0-24.416188-24.975337z m-58.337914 433.899885a42.681733 42.681733 0 0 1-42.495349 43.054498H125.438257a42.681733 42.681733 0 0 1-42.495349-43.054498V590.100115a42.681733 42.681733 0 0 1 42.495349-43.054498h301.940642a42.681733 42.681733 0 0 1 42.495349 43.054498z m525.22761-433.899885a41.749817 41.749817 0 0 0-41.377051 42.122583v55.914934a41.377051 41.377051 0 1 0 82.940485 0v-55.914934a41.749817 41.749817 0 0 0-41.563434-42.122583z m0 223.659734a41.749817 41.749817 0 0 0-41.377051 42.122584V894.65012a45.477479 45.477479 0 0 1-45.291096 45.850246h-159.730327a43.240882 43.240882 0 0 0-43.613649 37.276622A41.9362 41.9362 0 0 0 745.534871 1024h233.538039a57.778765 57.778765 0 0 0 57.405999-58.337914V729.3283a41.749817 41.749817 0 0 0-41.377051-41.9362zM732.488053 322.64035V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-82.940485a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733h82.940485a13.978733 13.978733 0 0 0 13.79235-13.978733zM532.126208 0.011183c-11.36937 0-20.688525 6.337026-20.688526 13.978733v308.650434c0 7.828091 9.319156 13.978733 20.688526 13.978733s20.688525-6.337026 20.688525-13.978733V13.989916c0-7.641708-9.319156-13.978733-20.688525-13.978733z" p-id="6738" fill="#04C361"/><path d="M745.534871 462.80045a41.749817 41.749817 0 0 0-41.377051 42.122583v252.549117a41.377051 41.377051 0 1 0 82.940485 0V504.923033A41.749817 41.749817 0 0 0 745.534871 462.80045" p-id="6739" fill="#04C361"/></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/wx_lite.svg b/src/assets/svgs/pay/icon/wx_lite.svg deleted file mode 100644 index 0c925cf..0000000 --- a/src/assets/svgs/pay/icon/wx_lite.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1676209433089" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2990" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M608.6 290.3c67.1 0 121.7 50.5 121.7 112.9 0 19.4-5.6 38.4-15.7 55.5-15.3 25-39.8 43.5-69.4 52.3-7.9 2.3-13.9 3.2-19.4 3.2-13 0-23.1-10.2-23.1-23.1 0-13 10.2-23.1 23.1-23.1 0.9 0 2.8 0 5.1-0.9 19.9-5.6 35.6-17.1 44.4-32.4 6-9.7 8.8-20.4 8.8-31.5 0-36.6-33.8-66.6-75-66.6-14.4 0-28.2 3.7-40.7 10.6-21.8 12.5-34.7 33.3-34.7 56v193.9c0 39.3-21.8 75.4-57.9 95.8-19.4 11.1-41.2 16.7-63.4 16.7-67.1 0-121.7-50.5-121.7-112.9 0-19.4 5.6-38.4 15.7-55.5 15.3-25 39.8-43.5 69.4-52.3 8.3-2.3 13.9-3.2 19.4-3.2 13 0 23.1 10.2 23.1 23.1 0 13-10.2 23.1-23.1 23.1-0.9 0-2.8 0-5.1 0.9-19.9 6-35.6 17.6-44.4 32.4-6 9.7-8.8 20.4-8.8 31.5 0 36.6 33.8 66.6 75.4 66.6 14.4 0 28.2-3.7 40.7-10.6 21.8-12.5 34.7-33.3 34.7-56V403.3c0-39.3 21.8-75.4 57.9-95.8 19-11.6 40.7-17.2 63-17.2zM510.8 929c231.1 0 418.4-187.3 418.4-418.4S741.9 92.1 510.8 92.1 92.4 279.5 92.4 510.6 279.7 929 510.8 929z m0 22C267.5 951 70.3 753.8 70.3 510.6S267.5 70.1 510.8 70.1s440.5 197.2 440.5 440.5S754.1 951 510.8 951z" p-id="2991" fill="#58bf6b"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/wx_native.svg b/src/assets/svgs/pay/icon/wx_native.svg deleted file mode 100644 index bf3ba2b..0000000 --- a/src/assets/svgs/pay/icon/wx_native.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279375144" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4399" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#04C361" p-id="4400"/></svg> \ No newline at end of file diff --git a/src/assets/svgs/pay/icon/wx_pub.svg b/src/assets/svgs/pay/icon/wx_pub.svg deleted file mode 100644 index 3a6d15b..0000000 --- a/src/assets/svgs/pay/icon/wx_pub.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279797174" class="icon" viewBox="0 0 1260 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7665" xmlns:xlink="http://www.w3.org/1999/xlink" width="49.21875" height="40"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); } -</style></defs><path d="M797.14798 481.753a269.194 269.194 0 0 0 102.892-211.929C900.03998 120.99 779.02998 0 630.15698 0 481.28298 0 360.27398 120.99 360.27398 269.824c0 85.878 40.33 162.462 102.912 211.929A450.974 450.974 0 0 0 309.84198 582.774c-85.543 85.524-132.608 199.208-132.608 320.236 0 25.01 0 51.712 0.197 76.367a44.898 44.898 0 0 0 44.82 44.623h816.01a44.8 44.8 0 0 0 44.82-44.623V903.01c0-121.009-47.066-234.732-132.609-320.236a451.072 451.072 0 0 0-153.344-101.021z" p-id="7666" fill="#04C361"></path><path d="M1186.18898 580.391A378.644 378.644 0 0 0 1061.81198 473.03a223.783 223.783 0 0 0 64.237-157.657c0-49.742-15.872-96.67-45.746-136.074A225.34 225.34 0 0 0 964.70998 99.9a37.297 37.297 0 0 0-46.14 25.718c-5.592 19.89 5.79 40.724 25.6 46.356 63.114 18.196 107.363 77.135 107.363 143.4a148.913 148.913 0 0 1-81.23 133.06 38.065 38.065 0 0 0-20.363 36.608c1.32 15.203 11.58 28.16 25.975 32.65 125.479 39.601 209.703 155.038 209.703 287.173v63.074c0 20.638 16.62 37.534 37.16 37.711h0.196a37.396 37.396 0 0 0 37.337-37.336V805.06c-0.197-81.644-25.777-159.35-74.142-224.69z m-901.77-62.503a36.982 36.982 0 0 0 25.955-32.65 37.455 37.455 0 0 0-20.362-36.628 148.913 148.913 0 0 1-81.231-133.06c0-66.245 44.071-125.184 107.382-143.4a37.612 37.612 0 0 0 25.58-46.356 37.376 37.376 0 0 0-46.139-25.718 225.32 225.32 0 0 0-115.593 79.4 223.252 223.252 0 0 0-45.746 136.074c0 60.258 23.533 116.381 64.237 157.676A380.475 380.475 0 0 0 74.14498 580.569 373.839 373.839 0 0 0 0.00198 805.258v63.232c0 20.657 16.798 37.356 37.356 37.356h0.197a37.317 37.317 0 0 0 37.14-37.73V805.06c0-132.332 84.401-247.769 209.723-287.173z" p-id="7667" fill="#04C361"></path></svg> \ No newline at end of file diff --git a/src/components/AppLinkInput/data.ts b/src/components/AppLinkInput/data.ts index 1916e08..c9e3678 100644 --- a/src/components/AppLinkInput/data.ts +++ b/src/components/AppLinkInput/data.ts @@ -5,6 +5,7 @@ // 链接列表 links: AppLink[] } + // APP 链接 export interface AppLink { // 链接名称 @@ -21,6 +22,8 @@ ACTIVITY_COMBINATION, // 秒杀活动 ACTIVITY_SECKILL, + // 积分商城活动 + ACTIVITY_POINT, // 文章详情 ARTICLE_DETAIL, // 优惠券详情 @@ -131,6 +134,11 @@ type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL }, { + name: '积分商城活动', + path: '/pages/activity/point/list', + type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT + }, + { name: '签到中心', path: '/pages/app/sign' }, diff --git a/src/components/ContentWrap/src/ContentWrap.vue b/src/components/ContentWrap/src/ContentWrap.vue index c75e4b7..e603596 100644 --- a/src/components/ContentWrap/src/ContentWrap.vue +++ b/src/components/ContentWrap/src/ContentWrap.vue @@ -11,7 +11,7 @@ defineProps({ title: propTypes.string.def(''), message: propTypes.string.def(''), - bodyStyle: propTypes.object.def({ padding: '20px' }) + bodyStyle: propTypes.object.def({ padding: '10px' }) }) </script> diff --git a/src/components/Crontab/src/Crontab.vue b/src/components/Crontab/src/Crontab.vue index e61fef8..0914bb7 100644 --- a/src/components/Crontab/src/Crontab.vue +++ b/src/components/Crontab/src/Crontab.vue @@ -548,10 +548,10 @@ <el-form> <el-form-item label="类型"> <el-radio-group v-model="cronValue.second.type"> - <el-radio-button label="0">任意值</el-radio-button> - <el-radio-button label="1">范围</el-radio-button> - <el-radio-button label="2">间隔</el-radio-button> - <el-radio-button label="3">指定</el-radio-button> + <el-radio-button value="0">任意值</el-radio-button> + <el-radio-button value="1">范围</el-radio-button> + <el-radio-button value="2">间隔</el-radio-button> + <el-radio-button value="3">指定</el-radio-button> </el-radio-group> </el-form-item> <el-form-item v-if="cronValue.second.type == '1'" label="范围"> @@ -607,10 +607,10 @@ <el-form> <el-form-item label="类型"> <el-radio-group v-model="cronValue.minute.type"> - <el-radio-button label="0">任意值</el-radio-button> - <el-radio-button label="1">范围</el-radio-button> - <el-radio-button label="2">间隔</el-radio-button> - <el-radio-button label="3">指定</el-radio-button> + <el-radio-button value="0">任意值</el-radio-button> + <el-radio-button value="1">范围</el-radio-button> + <el-radio-button value="2">间隔</el-radio-button> + <el-radio-button value="3">指定</el-radio-button> </el-radio-group> </el-form-item> <el-form-item v-if="cronValue.minute.type == '1'" label="范围"> @@ -666,10 +666,10 @@ <el-form> <el-form-item label="类型"> <el-radio-group v-model="cronValue.hour.type"> - <el-radio-button label="0">任意值</el-radio-button> - <el-radio-button label="1">范围</el-radio-button> - <el-radio-button label="2">间隔</el-radio-button> - <el-radio-button label="3">指定</el-radio-button> + <el-radio-button value="0">任意值</el-radio-button> + <el-radio-button value="1">范围</el-radio-button> + <el-radio-button value="2">间隔</el-radio-button> + <el-radio-button value="3">指定</el-radio-button> </el-radio-group> </el-form-item> <el-form-item v-if="cronValue.hour.type == '1'" label="范围"> @@ -725,12 +725,12 @@ <el-form> <el-form-item label="类型"> <el-radio-group v-model="cronValue.day.type"> - <el-radio-button label="0">任意值</el-radio-button> - <el-radio-button label="1">范围</el-radio-button> - <el-radio-button label="2">间隔</el-radio-button> - <el-radio-button label="3">指定</el-radio-button> - <el-radio-button label="4">本月最后一天</el-radio-button> - <el-radio-button label="5">不指定</el-radio-button> + <el-radio-button value="0">任意值</el-radio-button> + <el-radio-button value="1">范围</el-radio-button> + <el-radio-button value="2">间隔</el-radio-button> + <el-radio-button value="3">指定</el-radio-button> + <el-radio-button value="4">本月最后一天</el-radio-button> + <el-radio-button value="5">不指定</el-radio-button> </el-radio-group> </el-form-item> <el-form-item v-if="cronValue.day.type == '1'" label="范围"> @@ -786,10 +786,10 @@ <el-form> <el-form-item label="类型"> <el-radio-group v-model="cronValue.month.type"> - <el-radio-button label="0">任意值</el-radio-button> - <el-radio-button label="1">范围</el-radio-button> - <el-radio-button label="2">间隔</el-radio-button> - <el-radio-button label="3">指定</el-radio-button> + <el-radio-button value="0">任意值</el-radio-button> + <el-radio-button value="1">范围</el-radio-button> + <el-radio-button value="2">间隔</el-radio-button> + <el-radio-button value="3">指定</el-radio-button> </el-radio-group> </el-form-item> <el-form-item v-if="cronValue.month.type == '1'" label="范围"> @@ -846,12 +846,12 @@ <el-form> <el-form-item label="类型"> <el-radio-group v-model="cronValue.week.type"> - <el-radio-button label="0">任意值</el-radio-button> - <el-radio-button label="1">范围</el-radio-button> - <el-radio-button label="2">间隔</el-radio-button> - <el-radio-button label="3">指定</el-radio-button> - <el-radio-button label="4">本月最后一周</el-radio-button> - <el-radio-button label="5">不指定</el-radio-button> + <el-radio-button value="0">任意值</el-radio-button> + <el-radio-button value="1">范围</el-radio-button> + <el-radio-button value="2">间隔</el-radio-button> + <el-radio-button value="3">指定</el-radio-button> + <el-radio-button value="4">本月最后一周</el-radio-button> + <el-radio-button value="5">不指定</el-radio-button> </el-radio-group> </el-form-item> <el-form-item v-if="cronValue.week.type == '1'" label="范围"> @@ -925,11 +925,11 @@ <el-form> <el-form-item label="类型"> <el-radio-group v-model="cronValue.year.type"> - <el-radio-button label="-1">忽略</el-radio-button> - <el-radio-button label="0">任意值</el-radio-button> - <el-radio-button label="1">范围</el-radio-button> - <el-radio-button label="2">间隔</el-radio-button> - <el-radio-button label="3">指定</el-radio-button> + <el-radio-button value="-1">忽略</el-radio-button> + <el-radio-button value="0">任意值</el-radio-button> + <el-radio-button value="1">范围</el-radio-button> + <el-radio-button value="2">间隔</el-radio-button> + <el-radio-button value="3">指定</el-radio-button> </el-radio-group> </el-form-item> <el-form-item v-if="cronValue.year.type == '1'" label="范围"> diff --git a/src/components/DictTag/src/DictTag.vue b/src/components/DictTag/src/DictTag.vue index 1d075a1..6414eaa 100644 --- a/src/components/DictTag/src/DictTag.vue +++ b/src/components/DictTag/src/DictTag.vue @@ -1,8 +1,9 @@ <script lang="tsx"> -import { defineComponent, PropType, ref } from 'vue' +import { computed, defineComponent, PropType } from 'vue' import { isHexColor } from '@/utils/color' import { ElTag } from 'element-plus' import { DictDataType, getDictOptions } from '@/utils/dict' +import { isArray, isBoolean, isNumber, isString } from '@/utils/is' export default defineComponent({ name: 'DictTag', @@ -12,49 +13,78 @@ required: true }, value: { - type: [String, Number, Boolean] as PropType<string | number | boolean>, + type: [String, Number, Boolean, Array], required: true + }, + // 字符串分隔符 只有当 props.value 传入值为字符串时有效 + separator: { + type: String as PropType<string>, + default: ',' + }, + // 每个 tag 之间的间隔,默认为 5px,参考的 el-row 的 gutter + gutter: { + type: String as PropType<string>, + default: '5px' } }, setup(props) { - const dictData = ref<DictDataType>() - const getDictObj = (dictType: string, value: string) => { - const dictOptions = getDictOptions(dictType) - dictOptions.forEach((dict: DictDataType) => { - if (dict.value === value) { - if (dict.colorType + '' === 'default') { - dict.colorType = 'info' - } - dictData.value = dict - } - }) - } - const rederDictTag = () => { + const valueArr: any = computed(() => { + // 1. 是 Number 类型和 Boolean 类型的情况 + if (isNumber(props.value) || isBoolean(props.value)) { + return [String(props.value)] + } + // 2. 是字符串(进一步判断是否有包含分隔符号 -> props.sepSymbol ) + else if (isString(props.value)) { + return props.value.split(props.separator) + } + // 3. 数组 + else if (isArray(props.value)) { + return props.value.map(String) + } + return [] + }) + const renderDictTag = () => { if (!props.type) { return null } // 解决自定义字典标签值为零时标签不渲染的问题 - if (props.value === undefined || props.value === null) { + if (props.value === undefined || props.value === null || props.value === '') { return null } - getDictObj(props.type, props.value.toString()) - // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题 + const dictOptions = getDictOptions(props.type) + return ( - <ElTag - style={dictData.value?.cssClass ? 'color: #fff' : ''} - type={dictData.value?.colorType} - color={ - dictData.value?.cssClass && isHexColor(dictData.value?.cssClass) - ? dictData.value?.cssClass - : '' - } - disableTransitions={true} + <div + class="dict-tag" + style={{ + display: 'inline-flex', + gap: props.gutter, + justifyContent: 'center', + alignItems: 'center' + }} > - {dictData.value?.label} - </ElTag> + {dictOptions.map((dict: DictDataType) => { + if (valueArr.value.includes(dict.value)) { + if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') { + dict.colorType = '' + } + return ( + // 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题 + <ElTag + style={dict?.cssClass ? 'color: #fff' : ''} + type={dict?.colorType || null} + color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''} + disableTransitions={true} + > + {dict?.label} + </ElTag> + ) + } + })} + </div> ) } - return () => rederDictTag() + return () => renderDictTag() } }) </script> diff --git a/src/components/DiyEditor/components/ComponentContainer.vue b/src/components/DiyEditor/components/ComponentContainer.vue index 0137278..4856722 100644 --- a/src/components/DiyEditor/components/ComponentContainer.vue +++ b/src/components/DiyEditor/components/ComponentContainer.vue @@ -165,6 +165,7 @@ width: 80px; height: 25px; font-size: 12px; + color: #6a6a6a; line-height: 25px; text-align: center; background: #fff; diff --git a/src/components/DiyEditor/components/ComponentContainerProperty.vue b/src/components/DiyEditor/components/ComponentContainerProperty.vue index 9d0750d..25119a5 100644 --- a/src/components/DiyEditor/components/ComponentContainerProperty.vue +++ b/src/components/DiyEditor/components/ComponentContainerProperty.vue @@ -11,8 +11,8 @@ <el-form :model="formData" label-width="80px"> <el-form-item label="组件背景" prop="bgType"> <el-radio-group v-model="formData.bgType"> - <el-radio label="color">纯色</el-radio> - <el-radio label="img">图片</el-radio> + <el-radio value="color">纯色</el-radio> + <el-radio value="img">图片</el-radio> </el-radio-group> </el-form-item> <el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'"> diff --git a/src/components/DiyEditor/components/mobile/Carousel/config.ts b/src/components/DiyEditor/components/mobile/Carousel/config.ts index 9cee5c2..3e74a51 100644 --- a/src/components/DiyEditor/components/mobile/Carousel/config.ts +++ b/src/components/DiyEditor/components/mobile/Carousel/config.ts @@ -38,8 +38,8 @@ autoplay: false, interval: 3, items: [ - { type: 'img', imgUrl: 'https://xxxx/banner-01.jpg', videoUrl: '' }, - { type: 'img', imgUrl: 'https://xxxx/banner-02.jpg', videoUrl: '' } + { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' }, + { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' } ] as CarouselItemProperty[], style: { bgType: 'color', diff --git a/src/components/DiyEditor/components/mobile/Carousel/property.vue b/src/components/DiyEditor/components/mobile/Carousel/property.vue index c3a5154..e11b032 100644 --- a/src/components/DiyEditor/components/mobile/Carousel/property.vue +++ b/src/components/DiyEditor/components/mobile/Carousel/property.vue @@ -5,12 +5,12 @@ <el-form-item label="样式" prop="type"> <el-radio-group v-model="formData.type"> <el-tooltip class="item" content="默认" placement="bottom"> - <el-radio-button label="default"> + <el-radio-button value="default"> <Icon icon="system-uicons:carousel" /> </el-radio-button> </el-tooltip> <el-tooltip class="item" content="卡片" placement="bottom"> - <el-radio-button label="card"> + <el-radio-button value="card"> <Icon icon="ic:round-view-carousel" /> </el-radio-button> </el-tooltip> @@ -18,8 +18,8 @@ </el-form-item> <el-form-item label="指示器" prop="indicator"> <el-radio-group v-model="formData.indicator"> - <el-radio label="dot">小圆点</el-radio> - <el-radio label="number">数字</el-radio> + <el-radio value="dot">小圆点</el-radio> + <el-radio value="number">数字</el-radio> </el-radio-group> </el-form-item> <el-form-item label="是否轮播" prop="autoplay"> @@ -43,8 +43,8 @@ <template #default="{ element }"> <el-form-item label="类型" prop="type" class="m-b-8px!" label-width="40px"> <el-radio-group v-model="element.type"> - <el-radio label="img">图片</el-radio> - <el-radio label="video">视频</el-radio> + <el-radio value="img">图片</el-radio> + <el-radio value="video">视频</el-radio> </el-radio-group> </el-form-item> <el-form-item diff --git a/src/components/DiyEditor/components/mobile/CouponCard/property.vue b/src/components/DiyEditor/components/mobile/CouponCard/property.vue index 4f32c21..4f69000 100644 --- a/src/components/DiyEditor/components/mobile/CouponCard/property.vue +++ b/src/components/DiyEditor/components/mobile/CouponCard/property.vue @@ -26,17 +26,17 @@ <el-form-item label="列数" prop="type"> <el-radio-group v-model="formData.columns"> <el-tooltip class="item" content="一列" placement="bottom"> - <el-radio-button :label="1"> + <el-radio-button :value="1"> <Icon icon="fluent:text-column-one-24-filled" /> </el-radio-button> </el-tooltip> <el-tooltip class="item" content="二列" placement="bottom"> - <el-radio-button :label="2"> + <el-radio-button :value="2"> <Icon icon="fluent:text-column-two-24-filled" /> </el-radio-button> </el-tooltip> <el-tooltip class="item" content="三列" placement="bottom"> - <el-radio-button :label="3"> + <el-radio-button :value="3"> <Icon icon="fluent:text-column-three-24-filled" /> </el-radio-button> </el-tooltip> diff --git a/src/components/DiyEditor/components/mobile/Divider/property.vue b/src/components/DiyEditor/components/mobile/Divider/property.vue index 3d7be26..0c3cb0e 100644 --- a/src/components/DiyEditor/components/mobile/Divider/property.vue +++ b/src/components/DiyEditor/components/mobile/Divider/property.vue @@ -11,7 +11,7 @@ :key="index" :content="item.text" > - <el-radio-button :label="item.type"> + <el-radio-button :value="item.type"> <Icon :icon="item.icon" /> </el-radio-button> </el-tooltip> @@ -24,12 +24,12 @@ <el-form-item label="左右边距" prop="paddingType"> <el-radio-group v-model="formData!.paddingType"> <el-tooltip content="无边距" placement="top"> - <el-radio-button label="none"> + <el-radio-button value="none"> <Icon icon="tabler:box-padding" /> </el-radio-button> </el-tooltip> <el-tooltip content="左右留边" placement="top"> - <el-radio-button label="horizontal"> + <el-radio-button value="horizontal"> <Icon icon="vaadin:padding" /> </el-radio-button> </el-tooltip> diff --git a/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue b/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue index 19e42cb..c2b9926 100644 --- a/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue +++ b/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue @@ -44,7 +44,7 @@ defineProps<{ property: FloatingActionButtonProperty }>() // 是否展开 -const expanded = ref(true) +const expanded = ref(false) // 处理展开/折叠 const handleToggleFab = () => { expanded.value = !expanded.value diff --git a/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue b/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue index 5db08d0..df459ff 100644 --- a/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue +++ b/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue @@ -3,8 +3,8 @@ <el-card header="按钮配置" class="property-group" shadow="never"> <el-form-item label="展开方向" prop="direction"> <el-radio-group v-model="formData.direction"> - <el-radio label="vertical">垂直</el-radio> - <el-radio label="horizontal">水平</el-radio> + <el-radio value="vertical">垂直</el-radio> + <el-radio value="horizontal">水平</el-radio> </el-radio-group> </el-form-item> <el-form-item label="显示文字" prop="showText"> diff --git a/src/components/DiyEditor/components/mobile/MenuGrid/property.vue b/src/components/DiyEditor/components/mobile/MenuGrid/property.vue index 7940fd0..bb944c9 100644 --- a/src/components/DiyEditor/components/mobile/MenuGrid/property.vue +++ b/src/components/DiyEditor/components/mobile/MenuGrid/property.vue @@ -4,8 +4,8 @@ <el-form label-width="80px" :model="formData" class="m-t-8px"> <el-form-item label="每行数量" prop="column"> <el-radio-group v-model="formData.column"> - <el-radio :label="3">3个</el-radio> - <el-radio :label="4">4个</el-radio> + <el-radio :value="3">3个</el-radio> + <el-radio :value="4">4个</el-radio> </el-radio-group> </el-form-item> diff --git a/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue b/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue index 81266bc..fbae83c 100644 --- a/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue +++ b/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue @@ -4,21 +4,21 @@ <el-form label-width="80px" :model="formData" class="m-t-8px"> <el-form-item label="布局" prop="layout"> <el-radio-group v-model="formData.layout"> - <el-radio label="iconText">图标+文字</el-radio> - <el-radio label="icon">仅图标</el-radio> + <el-radio value="iconText">图标+文字</el-radio> + <el-radio value="icon">仅图标</el-radio> </el-radio-group> </el-form-item> <el-form-item label="行数" prop="row"> <el-radio-group v-model="formData.row"> - <el-radio :label="1">1行</el-radio> - <el-radio :label="2">2行</el-radio> + <el-radio :value="1">1行</el-radio> + <el-radio :value="2">2行</el-radio> </el-radio-group> </el-form-item> <el-form-item label="列数" prop="column"> <el-radio-group v-model="formData.column"> - <el-radio :label="3">3列</el-radio> - <el-radio :label="4">4列</el-radio> - <el-radio :label="5">5列</el-radio> + <el-radio :value="3">3列</el-radio> + <el-radio :value="4">4列</el-radio> + <el-radio :value="5">5列</el-radio> </el-radio-group> </el-form-item> diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue b/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue index edc85f1..2c3bd54 100644 --- a/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue +++ b/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue @@ -14,9 +14,9 @@ <template v-if="selectedHotAreaIndex === cellIndex"> <el-form-item label="类型" :prop="`cell[${cellIndex}].type`"> <el-radio-group v-model="cell.type"> - <el-radio label="text">文字</el-radio> - <el-radio label="image">图片</el-radio> - <el-radio label="search">搜索框</el-radio> + <el-radio value="text">文字</el-radio> + <el-radio value="image">图片</el-radio> + <el-radio value="search">搜索框</el-radio> </el-radio-group> </el-form-item> <!-- 1. 文字 --> diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/property.vue b/src/components/DiyEditor/components/mobile/NavigationBar/property.vue index b2bc8c1..5b06772 100644 --- a/src/components/DiyEditor/components/mobile/NavigationBar/property.vue +++ b/src/components/DiyEditor/components/mobile/NavigationBar/property.vue @@ -2,27 +2,27 @@ <el-form label-width="80px" :model="formData" :rules="rules"> <el-form-item label="样式" prop="styleType"> <el-radio-group v-model="formData!.styleType"> - <el-radio label="normal">标准</el-radio> + <el-radio value="normal">标准</el-radio> <el-tooltip content="沉侵式头部仅支持微信小程序、APP,建议页面第一个组件为图片展示类组件" placement="top" > - <el-radio label="inner">沉浸式</el-radio> + <el-radio value="inner">沉浸式</el-radio> </el-tooltip> </el-radio-group> </el-form-item> <el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'"> <el-radio-group v-model="formData!.alwaysShow"> - <el-radio :label="false">关闭</el-radio> + <el-radio :value="false">关闭</el-radio> <el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top"> - <el-radio :label="true">开启</el-radio> + <el-radio :value="true">开启</el-radio> </el-tooltip> </el-radio-group> </el-form-item> <el-form-item label="背景类型" prop="bgType"> <el-radio-group v-model="formData.bgType"> - <el-radio label="color">纯色</el-radio> - <el-radio label="img">图片</el-radio> + <el-radio value="color">纯色</el-radio> + <el-radio value="img">图片</el-radio> </el-radio-group> </el-form-item> <el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'"> diff --git a/src/components/DiyEditor/components/mobile/NoticeBar/config.ts b/src/components/DiyEditor/components/mobile/NoticeBar/config.ts index 3a2ef53..b6b0860 100644 --- a/src/components/DiyEditor/components/mobile/NoticeBar/config.ts +++ b/src/components/DiyEditor/components/mobile/NoticeBar/config.ts @@ -28,7 +28,7 @@ name: '公告栏', icon: 'ep:bell', property: { - iconUrl: 'http://xxxx/static/images/xinjian.png', + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png', contents: [ { text: '', diff --git a/src/components/DiyEditor/components/mobile/Popover/property.vue b/src/components/DiyEditor/components/mobile/Popover/property.vue index 6535e3b..2dd4351 100644 --- a/src/components/DiyEditor/components/mobile/Popover/property.vue +++ b/src/components/DiyEditor/components/mobile/Popover/property.vue @@ -11,10 +11,10 @@ <el-form-item label="显示次数" :prop="`list[${index}].showType`"> <el-radio-group v-model="element.showType"> <el-tooltip content="只显示一次,下次打开时不显示" placement="bottom"> - <el-radio label="once">一次</el-radio> + <el-radio value="once">一次</el-radio> </el-tooltip> <el-tooltip content="每次打开时都会显示" placement="bottom"> - <el-radio label="always">不限</el-radio> + <el-radio value="always">不限</el-radio> </el-tooltip> </el-radio-group> </el-form-item> diff --git a/src/components/DiyEditor/components/mobile/ProductCard/index.vue b/src/components/DiyEditor/components/mobile/ProductCard/index.vue index f05d4fa..25f8cb8 100644 --- a/src/components/DiyEditor/components/mobile/ProductCard/index.vue +++ b/src/components/DiyEditor/components/mobile/ProductCard/index.vue @@ -67,15 +67,15 @@ class="text-16px" :style="{ color: property.fields.price.color }" > - ¥{{ spu.price }} + ¥{{ fenToYuan(spu.price as any) }} </span> <!-- 市场价 --> <span v-if="property.fields.marketPrice.show && spu.marketPrice" class="ml-4px text-10px line-through" :style="{ color: property.fields.marketPrice.color }" - >¥{{ spu.marketPrice }}</span - > + >¥{{ fenToYuan(spu.marketPrice) }} + </span> </div> <div class="text-12px"> <!-- 销量 --> @@ -117,6 +117,7 @@ <script setup lang="ts"> import { ProductCardProperty } from './config' import * as ProductSpuApi from '@/api/mall/product/spu' +import { fenToYuan } from '../../../../../utils' /** 商品卡片 */ defineOptions({ name: 'ProductCard' }) diff --git a/src/components/DiyEditor/components/mobile/ProductCard/property.vue b/src/components/DiyEditor/components/mobile/ProductCard/property.vue index cfa5008..110c8be 100644 --- a/src/components/DiyEditor/components/mobile/ProductCard/property.vue +++ b/src/components/DiyEditor/components/mobile/ProductCard/property.vue @@ -8,17 +8,17 @@ <el-form-item label="布局" prop="type"> <el-radio-group v-model="formData.layoutType"> <el-tooltip class="item" content="单列大图" placement="bottom"> - <el-radio-button label="oneColBigImg"> + <el-radio-button value="oneColBigImg"> <Icon icon="fluent:text-column-one-24-filled" /> </el-radio-button> </el-tooltip> <el-tooltip class="item" content="单列小图" placement="bottom"> - <el-radio-button label="oneColSmallImg"> + <el-radio-button value="oneColSmallImg"> <Icon icon="fluent:text-column-two-left-24-filled" /> </el-radio-button> </el-tooltip> <el-tooltip class="item" content="双列" placement="bottom"> - <el-radio-button label="twoCol"> + <el-radio-button value="twoCol"> <Icon icon="fluent:text-column-two-24-filled" /> </el-radio-button> </el-tooltip> @@ -74,8 +74,8 @@ <el-card header="按钮" class="property-group" shadow="never"> <el-form-item label="按钮类型" prop="btnBuy.type"> <el-radio-group v-model="formData.btnBuy.type"> - <el-radio-button label="text">文字</el-radio-button> - <el-radio-button label="img">图片</el-radio-button> + <el-radio-button value="text">文字</el-radio-button> + <el-radio-button value="img">图片</el-radio-button> </el-radio-group> </el-form-item> <template v-if="formData.btnBuy.type === 'text'"> diff --git a/src/components/DiyEditor/components/mobile/ProductList/index.vue b/src/components/DiyEditor/components/mobile/ProductList/index.vue index 3ba6367..a51fc07 100644 --- a/src/components/DiyEditor/components/mobile/ProductList/index.vue +++ b/src/components/DiyEditor/components/mobile/ProductList/index.vue @@ -54,7 +54,7 @@ class="text-12px" :style="{ color: property.fields.price.color }" > - ¥{{ spu.price }} + ¥{{ fenToYuan(spu.price) }} </span> </div> </div> @@ -65,6 +65,7 @@ <script setup lang="ts"> import { ProductListProperty } from './config' import * as ProductSpuApi from '@/api/mall/product/spu' +import { fenToYuan } from '@/utils' /** 商品栏 */ defineOptions({ name: 'ProductList' }) diff --git a/src/components/DiyEditor/components/mobile/ProductList/property.vue b/src/components/DiyEditor/components/mobile/ProductList/property.vue index e9cf7c0..894687c 100644 --- a/src/components/DiyEditor/components/mobile/ProductList/property.vue +++ b/src/components/DiyEditor/components/mobile/ProductList/property.vue @@ -8,17 +8,17 @@ <el-form-item label="布局" prop="type"> <el-radio-group v-model="formData.layoutType"> <el-tooltip class="item" content="双列" placement="bottom"> - <el-radio-button label="twoCol"> + <el-radio-button value="twoCol"> <Icon icon="fluent:text-column-two-24-filled" /> </el-radio-button> </el-tooltip> <el-tooltip class="item" content="三列" placement="bottom"> - <el-radio-button label="threeCol"> + <el-radio-button value="threeCol"> <Icon icon="fluent:text-column-three-24-filled" /> </el-radio-button> </el-tooltip> <el-tooltip class="item" content="水平滑动" placement="bottom"> - <el-radio-button label="horizSwiper"> + <el-radio-button value="horizSwiper"> <Icon icon="system-uicons:carousel" /> </el-radio-button> </el-tooltip> diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts b/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts index 0c7e9ff..f4fdf6e 100644 --- a/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts +++ b/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts @@ -3,19 +3,40 @@ /** 拼团属性 */ export interface PromotionCombinationProperty { // 布局类型:单列 | 三列 - layoutType: 'oneCol' | 'threeCol' + layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' // 商品字段 fields: { // 商品名称 name: PromotionCombinationFieldProperty + // 商品简介 + introduction: PromotionCombinationFieldProperty // 商品价格 price: PromotionCombinationFieldProperty + // 市场价 + marketPrice: PromotionCombinationFieldProperty + // 商品销量 + salesCount: PromotionCombinationFieldProperty + // 商品库存 + stock: PromotionCombinationFieldProperty } // 角标 badge: { // 是否显示 show: boolean // 角标图片 + imgUrl: string + } + // 按钮 + btnBuy: { + // 类型:文字 | 图片 + type: 'text' | 'img' + // 文字 + text: string + // 文字按钮:背景渐变起始颜色 + bgBeginColor: string + // 文字按钮:背景渐变结束颜色 + bgEndColor: string + // 图片按钮:图片地址 imgUrl: string } // 上圆角 @@ -25,7 +46,7 @@ // 间距 space: number // 拼团活动编号 - activityId: number + activityIds: number[] // 组件样式 style: ComponentStyle } @@ -44,12 +65,23 @@ name: '拼团', icon: 'mdi:account-group', property: { - layoutType: 'oneCol', + layoutType: 'oneColBigImg', fields: { name: { show: true, color: '#000' }, - price: { show: true, color: '#ff3000' } + introduction: { show: true, color: '#999' }, + price: { show: true, color: '#ff3000' }, + marketPrice: { show: true, color: '#c4c4c4' }, + salesCount: { show: true, color: '#c4c4c4' }, + stock: { show: false, color: '#c4c4c4' } }, badge: { show: false, imgUrl: '' }, + btnBuy: { + type: 'text', + text: '去拼团', + bgBeginColor: '#FF6000', + bgEndColor: '#FE832A', + imgUrl: '' + }, borderRadiusTop: 8, borderRadiusBottom: 8, space: 8, diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue b/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue index fe6f3a8..d41bf1c 100644 --- a/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue +++ b/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue @@ -1,125 +1,201 @@ <template> - <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> - <!-- 商品网格 --> + <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef"> <div - class="grid overflow-x-auto" + class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" :style="{ - gridGap: `${property.space}px`, - gridTemplateColumns, - width: scrollbarWidth + ...calculateSpace(index), + ...calculateWidth(), + borderTopLeftRadius: `${property.borderRadiusTop}px`, + borderTopRightRadius: `${property.borderRadiusTop}px`, + borderBottomLeftRadius: `${property.borderRadiusBottom}px`, + borderBottomRightRadius: `${property.borderRadiusBottom}px` }" + v-for="(spu, index) in spuList" + :key="index" > - <!-- 商品 --> + <!-- 角标 --> + <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> + <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> + </div> + <!-- 商品封面图 --> <div - class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" - :style="{ - borderTopLeftRadius: `${property.borderRadiusTop}px`, - borderTopRightRadius: `${property.borderRadiusTop}px`, - borderBottomLeftRadius: `${property.borderRadiusBottom}px`, - borderBottomRightRadius: `${property.borderRadiusBottom}px` - }" - v-for="(spu, index) in spuList" - :key="index" + :class="[ + 'h-140px', + { + 'w-full': property.layoutType !== 'oneColSmallImg', + 'w-140px': property.layoutType === 'oneColSmallImg' + } + ]" > - <!-- 角标 --> + <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> + </div> + <div + :class="[ + ' flex flex-col gap-8px p-8px box-border', + { + 'w-full': property.layoutType !== 'oneColSmallImg', + 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' + } + ]" + > + <!-- 商品名称 --> <div - v-if="property.badge.show" - class="absolute left-0 top-0 z-1 items-center justify-center" - > - <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> - </div> - <!-- 商品封面图 --> - <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" /> - <div + v-if="property.fields.name.show" :class="[ - 'flex flex-col gap-8px p-8px box-border', + 'text-14px ', { - 'w-[calc(100%-64px)]': columns === 2, - 'w-full': columns === 3 + truncate: property.layoutType !== 'oneColSmallImg', + 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' } ]" + :style="{ color: property.fields.name.color }" > - <!-- 商品名称 --> - <div - v-if="property.fields.name.show" - class="truncate text-12px" - :style="{ color: property.fields.name.color }" + {{ spu.name }} + </div> + <!-- 商品简介 --> + <div + v-if="property.fields.introduction.show" + class="truncate text-12px" + :style="{ color: property.fields.introduction.color }" + > + {{ spu.introduction }} + </div> + <div> + <!-- 价格 --> + <span + v-if="property.fields.price.show" + class="text-16px" + :style="{ color: property.fields.price.color }" > - {{ spu.name }} - </div> - <div> - <!-- 商品价格 --> - <span - v-if="property.fields.price.show" - class="text-12px" - :style="{ color: property.fields.price.color }" - > - ¥{{ spu.price }} - </span> - </div> + ¥{{ fenToYuan(spu.price || Infinity) }} + </span> + <!-- 市场价 --> + <span + v-if="property.fields.marketPrice.show && spu.marketPrice" + class="ml-4px text-10px line-through" + :style="{ color: property.fields.marketPrice.color }" + >¥{{ fenToYuan(spu.marketPrice) }}</span + > + </div> + <div class="text-12px"> + <!-- 销量 --> + <span + v-if="property.fields.salesCount.show" + :style="{ color: property.fields.salesCount.color }" + > + 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件 + </span> + <!-- 库存 --> + <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> + 库存{{ spu.stock || 0 }} + </span> </div> </div> + <!-- 购买按钮 --> + <div class="absolute bottom-8px right-8px"> + <!-- 文字按钮 --> + <span + v-if="property.btnBuy.type === 'text'" + class="rounded-full p-x-12px p-y-4px text-12px text-white" + :style="{ + background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` + }" + > + {{ property.btnBuy.text }} + </span> + <!-- 图片按钮 --> + <el-image + v-else + class="h-28px w-28px rounded-full" + fit="cover" + :src="property.btnBuy.imgUrl" + /> + </div> </div> - </el-scrollbar> + </div> </template> <script setup lang="ts"> import { PromotionCombinationProperty } from './config' import * as ProductSpuApi from '@/api/mall/product/spu' import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity' +import { fenToYuan } from '@/utils' -/** 拼团 */ +/** 拼团卡片 */ defineOptions({ name: 'PromotionCombination' }) // 定义属性 const props = defineProps<{ property: PromotionCombinationProperty }>() // 商品列表 const spuList = ref<ProductSpuApi.Spu[]>([]) +const spuIdList = ref<number[]>([]) +const combinationActivityList = ref<CombinationActivityApi.CombinationActivityVO[]>([]) + watch( - () => props.property.activityId, + () => props.property.activityIds, async () => { - if (!props.property.activityId) return - const activity = await CombinationActivityApi.getCombinationActivity(props.property.activityId) - if (!activity?.spuId) return - spuList.value = [await ProductSpuApi.getSpu(activity.spuId)] + try { + // 新添加的拼团组件,是没有活动ID的 + const activityIds = props.property.activityIds + // 检查活动ID的有效性 + if (Array.isArray(activityIds) && activityIds.length > 0) { + // 获取拼团活动详情列表 + combinationActivityList.value = + await CombinationActivityApi.getCombinationActivityListByIds(activityIds) + + // 获取拼团活动的 SPU 详情列表 + spuList.value = [] + spuIdList.value = combinationActivityList.value + .map((activity) => activity.spuId) + .filter((spuId): spuId is number => typeof spuId === 'number') + if (spuIdList.value.length > 0) { + spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) + } + + // 更新 SPU 的最低价格 + combinationActivityList.value.forEach((activity) => { + // 匹配spuId + const spu = spuList.value.find((spu) => spu.id === activity.spuId) + if (spu) { + // 赋值活动价格,哪个最便宜就赋值哪个 + spu.price = Math.min(activity.combinationPrice || Infinity, spu.price || Infinity) + } + }) + } + } catch (error) { + console.error('获取拼团活动细节或 SPU 细节时出错:', error) + } }, { immediate: true, deep: true } ) -// 手机宽度 -const phoneWidth = ref(375) + +/** + * 计算商品的间距 + * @param index 商品索引 + */ +const calculateSpace = (index: number) => { + // 商品的列数 + const columns = props.property.layoutType === 'twoCol' ? 2 : 1 + // 第一列没有左边距 + const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' + // 第一行没有上边距 + const marginTop = index < columns ? '0' : props.property.space + 'px' + + return { marginLeft, marginTop } +} + // 容器 const containerRef = ref() -// 商品的列数 -const columns = ref(2) -// 滚动条宽度 -const scrollbarWidth = ref('100%') -// 商品图大小 -const imageSize = ref('0') -// 商品网络列数 -const gridTemplateColumns = ref('') -// 计算布局参数 -watch( - () => [props.property, phoneWidth, spuList.value.length], - () => { - // 计算列数 - columns.value = props.property.layoutType === 'oneCol' ? 1 : 3 - // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 - const productWidth = - (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value - // 商品图布局:2列时,左右布局 3列时,上下布局 - imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px` - // 指定列数 - gridTemplateColumns.value = `repeat(${columns.value}, auto)` - // 不滚动 - scrollbarWidth.value = '100%' - }, - { immediate: true, deep: true } -) -onMounted(() => { - // 提取手机宽度 - phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375 -}) +// 计算商品的宽度 +const calculateWidth = () => { + let width = '100%' + // 双列时每列的宽度为:(总宽度 - 间距)/ 2 + if (props.property.layoutType === 'twoCol') { + width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` + } + return { width } +} </script> <style scoped lang="scss"></style> diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue b/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue index ec09dc4..ea901a0 100644 --- a/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue +++ b/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue @@ -2,30 +2,31 @@ <ComponentContainerProperty v-model="formData.style"> <el-form label-width="80px" :model="formData"> <el-card header="拼团活动" class="property-group" shadow="never"> - <el-form-item label="拼团活动" prop="activityId"> - <el-select v-model="formData.activityId"> - <el-option - v-for="activity in activityList" - :key="activity.id" - :label="activity.name" - :value="activity.id" - /> - </el-select> - </el-form-item> + <CombinationShowcase v-model="formData.activityIds" /> </el-card> <el-card header="商品样式" class="property-group" shadow="never"> <el-form-item label="布局" prop="type"> <el-radio-group v-model="formData.layoutType"> - <el-tooltip class="item" content="单列" placement="bottom"> - <el-radio-button label="oneCol"> + <el-tooltip class="item" content="单列大图" placement="bottom"> + <el-radio-button value="oneColBigImg"> <Icon icon="fluent:text-column-one-24-filled" /> </el-radio-button> </el-tooltip> - <el-tooltip class="item" content="三列" placement="bottom"> - <el-radio-button label="threeCol"> - <Icon icon="fluent:text-column-three-24-filled" /> + <el-tooltip class="item" content="单列小图" placement="bottom"> + <el-radio-button value="oneColSmallImg"> + <Icon icon="fluent:text-column-two-left-24-filled" /> </el-radio-button> </el-tooltip> + <el-tooltip class="item" content="双列" placement="bottom"> + <el-radio-button value="twoCol"> + <Icon icon="fluent:text-column-two-24-filled" /> + </el-radio-button> + </el-tooltip> + <!--<el-tooltip class="item" content="三列" placement="bottom"> + <el-radio-button value="threeCol"> + <Icon icon="fluent:text-column-three-24-filled" /> + </el-radio-button> + </el-tooltip>--> </el-radio-group> </el-form-item> <el-form-item label="商品名称" prop="fields.name.show"> @@ -34,10 +35,34 @@ <el-checkbox v-model="formData.fields.name.show" /> </div> </el-form-item> + <el-form-item label="商品简介" prop="fields.introduction.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.introduction.color" /> + <el-checkbox v-model="formData.fields.introduction.show" /> + </div> + </el-form-item> <el-form-item label="商品价格" prop="fields.price.show"> <div class="flex gap-8px"> <ColorInput v-model="formData.fields.price.color" /> <el-checkbox v-model="formData.fields.price.show" /> + </div> + </el-form-item> + <el-form-item label="市场价" prop="fields.marketPrice.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.marketPrice.color" /> + <el-checkbox v-model="formData.fields.marketPrice.show" /> + </div> + </el-form-item> + <el-form-item label="商品销量" prop="fields.salesCount.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.salesCount.color" /> + <el-checkbox v-model="formData.fields.salesCount.show" /> + </div> + </el-form-item> + <el-form-item label="商品库存" prop="fields.stock.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.stock.color" /> + <el-checkbox v-model="formData.fields.stock.show" /> </div> </el-form-item> </el-card> @@ -47,9 +72,35 @@ </el-form-item> <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> - <template #tip> 建议尺寸:36 * 22 </template> + <template #tip> 建议尺寸:36 * 22</template> </UploadImg> </el-form-item> + </el-card> + <el-card header="按钮" class="property-group" shadow="never"> + <el-form-item label="按钮类型" prop="btnBuy.type"> + <el-radio-group v-model="formData.btnBuy.type"> + <el-radio-button value="text">文字</el-radio-button> + <el-radio-button value="img">图片</el-radio-button> + </el-radio-group> + </el-form-item> + <template v-if="formData.btnBuy.type === 'text'"> + <el-form-item label="按钮文字" prop="btnBuy.text"> + <el-input v-model="formData.btnBuy.text" /> + </el-form-item> + <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> + <ColorInput v-model="formData.btnBuy.bgBeginColor" /> + </el-form-item> + <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> + <ColorInput v-model="formData.btnBuy.bgEndColor" /> + </el-form-item> + </template> + <template v-else> + <el-form-item label="图片" prop="btnBuy.imgUrl"> + <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> + <template #tip> 建议尺寸:56 * 56</template> + </UploadImg> + </el-form-item> + </template> </el-card> <el-card header="商品样式" class="property-group" shadow="never"> <el-form-item label="上圆角" prop="borderRadiusTop"> @@ -92,6 +143,7 @@ import { usePropertyForm } from '@/components/DiyEditor/util' import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity' import { CommonStatusEnum } from '@/utils/constants' +import CombinationShowcase from '@/views/mall/promotion/combination/components/CombinationShowcase.vue' // 拼团属性面板 defineOptions({ name: 'PromotionCombinationProperty' }) @@ -100,7 +152,7 @@ const emit = defineEmits(['update:modelValue']) const { formData } = usePropertyForm(props.modelValue, emit) // 活动列表 -const activityList = ref<CombinationActivityApi.CombinationActivityVO>([]) +const activityList = ref<CombinationActivityApi.CombinationActivityVO[]>([]) onMounted(async () => { const { list } = await CombinationActivityApi.getCombinationActivityPage({ status: CommonStatusEnum.ENABLE diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts b/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts new file mode 100644 index 0000000..75aa0ff --- /dev/null +++ b/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts @@ -0,0 +1,96 @@ +import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util' + +/** 积分商城属性 */ +export interface PromotionPointProperty { + // 布局类型:单列 | 三列 + layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' + // 商品字段 + fields: { + // 商品名称 + name: PromotionPointFieldProperty + // 商品简介 + introduction: PromotionPointFieldProperty + // 商品价格 + price: PromotionPointFieldProperty + // 市场价 + marketPrice: PromotionPointFieldProperty + // 商品销量 + salesCount: PromotionPointFieldProperty + // 商品库存 + stock: PromotionPointFieldProperty + } + // 角标 + badge: { + // 是否显示 + show: boolean + // 角标图片 + imgUrl: string + } + // 按钮 + btnBuy: { + // 类型:文字 | 图片 + type: 'text' | 'img' + // 文字 + text: string + // 文字按钮:背景渐变起始颜色 + bgBeginColor: string + // 文字按钮:背景渐变结束颜色 + bgEndColor: string + // 图片按钮:图片地址 + imgUrl: string + } + // 上圆角 + borderRadiusTop: number + // 下圆角 + borderRadiusBottom: number + // 间距 + space: number + // 秒杀活动编号 + activityIds: number[] + // 组件样式 + style: ComponentStyle +} + +// 商品字段 +export interface PromotionPointFieldProperty { + // 是否显示 + show: boolean + // 颜色 + color: string +} + +// 定义组件 +export const component = { + id: 'PromotionPoint', + name: '积分商城', + icon: 'ep:present', + property: { + layoutType: 'oneColBigImg', + fields: { + name: { show: true, color: '#000' }, + introduction: { show: true, color: '#999' }, + price: { show: true, color: '#ff3000' }, + marketPrice: { show: true, color: '#c4c4c4' }, + salesCount: { show: true, color: '#c4c4c4' }, + stock: { show: false, color: '#c4c4c4' } + }, + badge: { show: false, imgUrl: '' }, + btnBuy: { + type: 'text', + text: '立即兑换', + bgBeginColor: '#FF6000', + bgEndColor: '#FE832A', + imgUrl: '' + }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent<PromotionPointProperty> diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue b/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue new file mode 100644 index 0000000..4acd93f --- /dev/null +++ b/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue @@ -0,0 +1,202 @@ +<template> + <div ref="containerRef" :class="`box-content min-h-30px w-full flex flex-row flex-wrap`"> + <div + v-for="(spu, index) in spuList" + :key="index" + :style="{ + ...calculateSpace(index), + ...calculateWidth(), + borderTopLeftRadius: `${property.borderRadiusTop}px`, + borderTopRightRadius: `${property.borderRadiusTop}px`, + borderBottomLeftRadius: `${property.borderRadiusBottom}px`, + borderBottomRightRadius: `${property.borderRadiusBottom}px` + }" + class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" + > + <!-- 角标 --> + <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> + <el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" /> + </div> + <!-- 商品封面图 --> + <div + :class="[ + 'h-140px', + { + 'w-full': property.layoutType !== 'oneColSmallImg', + 'w-140px': property.layoutType === 'oneColSmallImg' + } + ]" + > + <el-image :src="spu.picUrl" class="h-full w-full" fit="cover" /> + </div> + <div + :class="[ + ' flex flex-col gap-8px p-8px box-border', + { + 'w-full': property.layoutType !== 'oneColSmallImg', + 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' + } + ]" + > + <!-- 商品名称 --> + <div + v-if="property.fields.name.show" + :class="[ + 'text-14px ', + { + truncate: property.layoutType !== 'oneColSmallImg', + 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' + } + ]" + :style="{ color: property.fields.name.color }" + > + {{ spu.name }} + </div> + <!-- 商品简介 --> + <div + v-if="property.fields.introduction.show" + :style="{ color: property.fields.introduction.color }" + class="truncate text-12px" + > + {{ spu.introduction }} + </div> + <div> + <!-- 积分 --> + <span + v-if="property.fields.price.show" + :style="{ color: property.fields.price.color }" + class="text-16px" + > + {{ spu.point }}积分 + {{ !spu.pointPrice || spu.pointPrice === 0 ? '' : `+${fenToYuan(spu.pointPrice)}元` }} + </span> + <!-- 市场价 --> + <span + v-if="property.fields.marketPrice.show && spu.marketPrice" + :style="{ color: property.fields.marketPrice.color }" + class="ml-4px text-10px line-through" + > + ¥{{ fenToYuan(spu.marketPrice) }} + </span> + </div> + <div class="text-12px"> + <!-- 销量 --> + <span + v-if="property.fields.salesCount.show" + :style="{ color: property.fields.salesCount.color }" + > + 已兑{{ (spu.pointTotalStock || 0) - (spu.pointStock || 0) }}件 + </span> + <!-- 库存 --> + <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> + 库存{{ spu.pointTotalStock || 0 }} + </span> + </div> + </div> + <!-- 购买按钮 --> + <div class="absolute bottom-8px right-8px"> + <!-- 文字按钮 --> + <span + v-if="property.btnBuy.type === 'text'" + :style="{ + background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` + }" + class="rounded-full p-x-12px p-y-4px text-12px text-white" + > + {{ property.btnBuy.text }} + </span> + <!-- 图片按钮 --> + <el-image + v-else + :src="property.btnBuy.imgUrl" + class="h-28px w-28px rounded-full" + fit="cover" + /> + </div> + </div> + </div> +</template> +<script lang="ts" setup> +import { PromotionPointProperty } from './config' +import * as ProductSpuApi from '@/api/mall/product/spu' +import { PointActivityApi, PointActivityVO, SpuExtension0 } from '@/api/mall/promotion/point' +import { fenToYuan } from '@/utils' + +/** 积分商城卡片 */ +defineOptions({ name: 'PromotionPoint' }) +// 定义属性 +const props = defineProps<{ property: PromotionPointProperty }>() +// 商品列表 +const spuList = ref<SpuExtension0[]>([]) +const spuIdList = ref<number[]>([]) +const pointActivityList = ref<PointActivityVO[]>([]) + +watch( + () => props.property.activityIds, + async () => { + try { + // 新添加的积分商城组件,是没有活动ID的 + const activityIds = props.property.activityIds + // 检查活动ID的有效性 + if (Array.isArray(activityIds) && activityIds.length > 0) { + // 获取积分商城活动详情列表 + pointActivityList.value = await PointActivityApi.getPointActivityListByIds(activityIds) + + // 获取积分商城活动的 SPU 详情列表 + spuList.value = [] + spuIdList.value = pointActivityList.value.map((activity) => activity.spuId) + if (spuIdList.value.length > 0) { + spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) + } + + // 更新 SPU 的最低兑换积分和所需兑换金额 + pointActivityList.value.forEach((activity) => { + // 匹配spuId + const spu = spuList.value.find((spu) => spu.id === activity.spuId) + if (spu) { + spu.pointStock = activity.stock + spu.pointTotalStock = activity.totalStock + spu.point = activity.point + spu.pointPrice = activity.price + } + }) + } + } catch (error) { + console.error('获取积分商城活动细节或 SPU 细节时出错:', error) + } + }, + { + immediate: true, + deep: true + } +) + +/** + * 计算商品的间距 + * @param index 商品索引 + */ +const calculateSpace = (index: number) => { + // 商品的列数 + const columns = props.property.layoutType === 'twoCol' ? 2 : 1 + // 第一列没有左边距 + const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' + // 第一行没有上边距 + const marginTop = index < columns ? '0' : props.property.space + 'px' + + return { marginLeft, marginTop } +} + +// 容器 +const containerRef = ref() +// 计算商品的宽度 +const calculateWidth = () => { + let width = '100%' + // 双列时每列的宽度为:(总宽度 - 间距)/ 2 + if (props.property.layoutType === 'twoCol') { + width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` + } + return { width } +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue b/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue new file mode 100644 index 0000000..84a429b --- /dev/null +++ b/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue @@ -0,0 +1,154 @@ +<template> + <ComponentContainerProperty v-model="formData.style"> + <el-form :model="formData" label-width="80px"> + <el-card class="property-group" header="积分商城活动" shadow="never"> + <PointShowcase v-model="formData.activityIds" /> + </el-card> + <el-card class="property-group" header="商品样式" shadow="never"> + <el-form-item label="布局" prop="type"> + <el-radio-group v-model="formData.layoutType"> + <el-tooltip class="item" content="单列大图" placement="bottom"> + <el-radio-button value="oneColBigImg"> + <Icon icon="fluent:text-column-one-24-filled" /> + </el-radio-button> + </el-tooltip> + <el-tooltip class="item" content="单列小图" placement="bottom"> + <el-radio-button value="oneColSmallImg"> + <Icon icon="fluent:text-column-two-left-24-filled" /> + </el-radio-button> + </el-tooltip> + <el-tooltip class="item" content="双列" placement="bottom"> + <el-radio-button value="twoCol"> + <Icon icon="fluent:text-column-two-24-filled" /> + </el-radio-button> + </el-tooltip> + <!--<el-tooltip class="item" content="三列" placement="bottom"> + <el-radio-button value="threeCol"> + <Icon icon="fluent:text-column-three-24-filled" /> + </el-radio-button> + </el-tooltip>--> + </el-radio-group> + </el-form-item> + <el-form-item label="商品名称" prop="fields.name.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.name.color" /> + <el-checkbox v-model="formData.fields.name.show" /> + </div> + </el-form-item> + <el-form-item label="商品简介" prop="fields.introduction.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.introduction.color" /> + <el-checkbox v-model="formData.fields.introduction.show" /> + </div> + </el-form-item> + <el-form-item label="商品价格" prop="fields.price.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.price.color" /> + <el-checkbox v-model="formData.fields.price.show" /> + </div> + </el-form-item> + <el-form-item label="市场价" prop="fields.marketPrice.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.marketPrice.color" /> + <el-checkbox v-model="formData.fields.marketPrice.show" /> + </div> + </el-form-item> + <el-form-item label="商品销量" prop="fields.salesCount.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.salesCount.color" /> + <el-checkbox v-model="formData.fields.salesCount.show" /> + </div> + </el-form-item> + <el-form-item label="商品库存" prop="fields.stock.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.stock.color" /> + <el-checkbox v-model="formData.fields.stock.show" /> + </div> + </el-form-item> + </el-card> + <el-card class="property-group" header="角标" shadow="never"> + <el-form-item label="角标" prop="badge.show"> + <el-switch v-model="formData.badge.show" /> + </el-form-item> + <el-form-item v-if="formData.badge.show" label="角标" prop="badge.imgUrl"> + <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> + <template #tip> 建议尺寸:36 * 22</template> + </UploadImg> + </el-form-item> + </el-card> + <el-card class="property-group" header="按钮" shadow="never"> + <el-form-item label="按钮类型" prop="btnBuy.type"> + <el-radio-group v-model="formData.btnBuy.type"> + <el-radio-button value="text">文字</el-radio-button> + <el-radio-button value="img">图片</el-radio-button> + </el-radio-group> + </el-form-item> + <template v-if="formData.btnBuy.type === 'text'"> + <el-form-item label="按钮文字" prop="btnBuy.text"> + <el-input v-model="formData.btnBuy.text" /> + </el-form-item> + <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> + <ColorInput v-model="formData.btnBuy.bgBeginColor" /> + </el-form-item> + <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> + <ColorInput v-model="formData.btnBuy.bgEndColor" /> + </el-form-item> + </template> + <template v-else> + <el-form-item label="图片" prop="btnBuy.imgUrl"> + <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> + <template #tip> 建议尺寸:56 * 56</template> + </UploadImg> + </el-form-item> + </template> + </el-card> + <el-card class="property-group" header="商品样式" shadow="never"> + <el-form-item label="上圆角" prop="borderRadiusTop"> + <el-slider + v-model="formData.borderRadiusTop" + :max="100" + :min="0" + :show-input-controls="false" + input-size="small" + show-input + /> + </el-form-item> + <el-form-item label="下圆角" prop="borderRadiusBottom"> + <el-slider + v-model="formData.borderRadiusBottom" + :max="100" + :min="0" + :show-input-controls="false" + input-size="small" + show-input + /> + </el-form-item> + <el-form-item label="间隔" prop="space"> + <el-slider + v-model="formData.space" + :max="100" + :min="0" + :show-input-controls="false" + input-size="small" + show-input + /> + </el-form-item> + </el-card> + </el-form> + </ComponentContainerProperty> +</template> + +<script lang="ts" setup> +import { PromotionPointProperty } from './config' +import { usePropertyForm } from '@/components/DiyEditor/util' +import PointShowcase from '@/views/mall/promotion/point/components/PointShowcase.vue' + +// 秒杀属性面板 +defineOptions({ name: 'PromotionPointProperty' }) + +const props = defineProps<{ modelValue: PromotionPointProperty }>() +const emit = defineEmits(['update:modelValue']) +const { formData } = usePropertyForm(props.modelValue, emit) +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts b/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts index 800398b..022be92 100644 --- a/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts +++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts @@ -3,19 +3,40 @@ /** 秒杀属性 */ export interface PromotionSeckillProperty { // 布局类型:单列 | 三列 - layoutType: 'oneCol' | 'threeCol' + layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol' // 商品字段 fields: { // 商品名称 name: PromotionSeckillFieldProperty + // 商品简介 + introduction: PromotionSeckillFieldProperty // 商品价格 price: PromotionSeckillFieldProperty + // 市场价 + marketPrice: PromotionSeckillFieldProperty + // 商品销量 + salesCount: PromotionSeckillFieldProperty + // 商品库存 + stock: PromotionSeckillFieldProperty } // 角标 badge: { // 是否显示 show: boolean // 角标图片 + imgUrl: string + } + // 按钮 + btnBuy: { + // 类型:文字 | 图片 + type: 'text' | 'img' + // 文字 + text: string + // 文字按钮:背景渐变起始颜色 + bgBeginColor: string + // 文字按钮:背景渐变结束颜色 + bgEndColor: string + // 图片按钮:图片地址 imgUrl: string } // 上圆角 @@ -25,10 +46,11 @@ // 间距 space: number // 秒杀活动编号 - activityId: number + activityIds: number[] // 组件样式 style: ComponentStyle } + // 商品字段 export interface PromotionSeckillFieldProperty { // 是否显示 @@ -43,13 +65,23 @@ name: '秒杀', icon: 'mdi:calendar-time', property: { - activityId: undefined, - layoutType: 'oneCol', + layoutType: 'oneColBigImg', fields: { name: { show: true, color: '#000' }, - price: { show: true, color: '#ff3000' } + introduction: { show: true, color: '#999' }, + price: { show: true, color: '#ff3000' }, + marketPrice: { show: true, color: '#c4c4c4' }, + salesCount: { show: true, color: '#c4c4c4' }, + stock: { show: false, color: '#c4c4c4' } }, badge: { show: false, imgUrl: '' }, + btnBuy: { + type: 'text', + text: '立即秒杀', + bgBeginColor: '#FF6000', + bgEndColor: '#FE832A', + imgUrl: '' + }, borderRadiusTop: 8, borderRadiusBottom: 8, space: 8, diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue b/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue index 1b4113b..3d34a3d 100644 --- a/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue +++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue @@ -1,125 +1,201 @@ <template> - <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> - <!-- 商品网格 --> + <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef"> <div - class="grid overflow-x-auto" + class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" :style="{ - gridGap: `${property.space}px`, - gridTemplateColumns, - width: scrollbarWidth + ...calculateSpace(index), + ...calculateWidth(), + borderTopLeftRadius: `${property.borderRadiusTop}px`, + borderTopRightRadius: `${property.borderRadiusTop}px`, + borderBottomLeftRadius: `${property.borderRadiusBottom}px`, + borderBottomRightRadius: `${property.borderRadiusBottom}px` }" + v-for="(spu, index) in spuList" + :key="index" > - <!-- 商品 --> + <!-- 角标 --> + <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center"> + <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> + </div> + <!-- 商品封面图 --> <div - class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" - :style="{ - borderTopLeftRadius: `${property.borderRadiusTop}px`, - borderTopRightRadius: `${property.borderRadiusTop}px`, - borderBottomLeftRadius: `${property.borderRadiusBottom}px`, - borderBottomRightRadius: `${property.borderRadiusBottom}px` - }" - v-for="(spu, index) in spuList" - :key="index" + :class="[ + 'h-140px', + { + 'w-full': property.layoutType !== 'oneColSmallImg', + 'w-140px': property.layoutType === 'oneColSmallImg' + } + ]" > - <!-- 角标 --> + <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" /> + </div> + <div + :class="[ + ' flex flex-col gap-8px p-8px box-border', + { + 'w-full': property.layoutType !== 'oneColSmallImg', + 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg' + } + ]" + > + <!-- 商品名称 --> <div - v-if="property.badge.show" - class="absolute left-0 top-0 z-1 items-center justify-center" - > - <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> - </div> - <!-- 商品封面图 --> - <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" /> - <div + v-if="property.fields.name.show" :class="[ - 'flex flex-col gap-8px p-8px box-border', + 'text-14px ', { - 'w-[calc(100%-64px)]': columns === 2, - 'w-full': columns === 3 + truncate: property.layoutType !== 'oneColSmallImg', + 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg' } ]" + :style="{ color: property.fields.name.color }" > - <!-- 商品名称 --> - <div - v-if="property.fields.name.show" - class="truncate text-12px" - :style="{ color: property.fields.name.color }" + {{ spu.name }} + </div> + <!-- 商品简介 --> + <div + v-if="property.fields.introduction.show" + class="truncate text-12px" + :style="{ color: property.fields.introduction.color }" + > + {{ spu.introduction }} + </div> + <div> + <!-- 价格 --> + <span + v-if="property.fields.price.show" + class="text-16px" + :style="{ color: property.fields.price.color }" > - {{ spu.name }} - </div> - <div> - <!-- 商品价格 --> - <span - v-if="property.fields.price.show" - class="text-12px" - :style="{ color: property.fields.price.color }" - > - ¥{{ spu.price }} - </span> - </div> + ¥{{ fenToYuan(spu.price || Infinity) }} + </span> + <!-- 市场价 --> + <span + v-if="property.fields.marketPrice.show && spu.marketPrice" + class="ml-4px text-10px line-through" + :style="{ color: property.fields.marketPrice.color }" + >¥{{ fenToYuan(spu.marketPrice) }}</span + > + </div> + <div class="text-12px"> + <!-- 销量 --> + <span + v-if="property.fields.salesCount.show" + :style="{ color: property.fields.salesCount.color }" + > + 已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件 + </span> + <!-- 库存 --> + <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }"> + 库存{{ spu.stock || 0 }} + </span> </div> </div> + <!-- 购买按钮 --> + <div class="absolute bottom-8px right-8px"> + <!-- 文字按钮 --> + <span + v-if="property.btnBuy.type === 'text'" + class="rounded-full p-x-12px p-y-4px text-12px text-white" + :style="{ + background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}` + }" + > + {{ property.btnBuy.text }} + </span> + <!-- 图片按钮 --> + <el-image + v-else + class="h-28px w-28px rounded-full" + fit="cover" + :src="property.btnBuy.imgUrl" + /> + </div> </div> - </el-scrollbar> + </div> </template> <script setup lang="ts"> import { PromotionSeckillProperty } from './config' import * as ProductSpuApi from '@/api/mall/product/spu' import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity' +import { fenToYuan } from '@/utils' -/** 秒杀 */ +/** 秒杀卡片 */ defineOptions({ name: 'PromotionSeckill' }) // 定义属性 const props = defineProps<{ property: PromotionSeckillProperty }>() // 商品列表 const spuList = ref<ProductSpuApi.Spu[]>([]) +const spuIdList = ref<number[]>([]) +const seckillActivityList = ref<SeckillActivityApi.SeckillActivityVO[]>([]) + watch( - () => props.property.activityId, + () => props.property.activityIds, async () => { - if (!props.property.activityId) return - const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId) - if (!activity?.spuId) return - spuList.value = [await ProductSpuApi.getSpu(activity.spuId)] + try { + // 新添加的秒杀组件,是没有活动ID的 + const activityIds = props.property.activityIds + // 检查活动ID的有效性 + if (Array.isArray(activityIds) && activityIds.length > 0) { + // 获取秒杀活动详情列表 + seckillActivityList.value = + await SeckillActivityApi.getSeckillActivityListByIds(activityIds) + + // 获取秒杀活动的 SPU 详情列表 + spuList.value = [] + spuIdList.value = seckillActivityList.value + .map((activity) => activity.spuId) + .filter((spuId): spuId is number => typeof spuId === 'number') + if (spuIdList.value.length > 0) { + spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value) + } + + // 更新 SPU 的最低价格 + seckillActivityList.value.forEach((activity) => { + // 匹配spuId + const spu = spuList.value.find((spu) => spu.id === activity.spuId) + if (spu) { + // 赋值活动价格,哪个最便宜就赋值哪个 + spu.price = Math.min(activity.seckillPrice || Infinity, spu.price || Infinity) + } + }) + } + } catch (error) { + console.error('获取秒杀活动细节或 SPU 细节时出错:', error) + } }, { immediate: true, deep: true } ) -// 手机宽度 -const phoneWidth = ref(375) + +/** + * 计算商品的间距 + * @param index 商品索引 + */ +const calculateSpace = (index: number) => { + // 商品的列数 + const columns = props.property.layoutType === 'twoCol' ? 2 : 1 + // 第一列没有左边距 + const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px' + // 第一行没有上边距 + const marginTop = index < columns ? '0' : props.property.space + 'px' + + return { marginLeft, marginTop } +} + // 容器 const containerRef = ref() -// 商品的列数 -const columns = ref(2) -// 滚动条宽度 -const scrollbarWidth = ref('100%') -// 商品图大小 -const imageSize = ref('0') -// 商品网络列数 -const gridTemplateColumns = ref('') -// 计算布局参数 -watch( - () => [props.property, phoneWidth, spuList.value.length], - () => { - // 计算列数 - columns.value = props.property.layoutType === 'oneCol' ? 1 : 3 - // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 - const productWidth = - (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value - // 商品图布局:2列时,左右布局 3列时,上下布局 - imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px` - // 指定列数 - gridTemplateColumns.value = `repeat(${columns.value}, auto)` - // 不滚动 - scrollbarWidth.value = '100%' - }, - { immediate: true, deep: true } -) -onMounted(() => { - // 提取手机宽度 - phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375 -}) +// 计算商品的宽度 +const calculateWidth = () => { + let width = '100%' + // 双列时每列的宽度为:(总宽度 - 间距)/ 2 + if (props.property.layoutType === 'twoCol') { + width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px` + } + return { width } +} </script> <style scoped lang="scss"></style> diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue b/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue index 8753782..6128759 100644 --- a/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue +++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue @@ -2,30 +2,31 @@ <ComponentContainerProperty v-model="formData.style"> <el-form label-width="80px" :model="formData"> <el-card header="秒杀活动" class="property-group" shadow="never"> - <el-form-item label="秒杀活动" prop="activityId"> - <el-select v-model="formData.activityId"> - <el-option - v-for="activity in activityList" - :key="activity.id" - :label="activity.name" - :value="activity.id" - /> - </el-select> - </el-form-item> + <SeckillShowcase v-model="formData.activityIds" /> </el-card> <el-card header="商品样式" class="property-group" shadow="never"> <el-form-item label="布局" prop="type"> <el-radio-group v-model="formData.layoutType"> - <el-tooltip class="item" content="单列" placement="bottom"> - <el-radio-button label="oneCol"> + <el-tooltip class="item" content="单列大图" placement="bottom"> + <el-radio-button value="oneColBigImg"> <Icon icon="fluent:text-column-one-24-filled" /> </el-radio-button> </el-tooltip> - <el-tooltip class="item" content="三列" placement="bottom"> - <el-radio-button label="threeCol"> - <Icon icon="fluent:text-column-three-24-filled" /> + <el-tooltip class="item" content="单列小图" placement="bottom"> + <el-radio-button value="oneColSmallImg"> + <Icon icon="fluent:text-column-two-left-24-filled" /> </el-radio-button> </el-tooltip> + <el-tooltip class="item" content="双列" placement="bottom"> + <el-radio-button value="twoCol"> + <Icon icon="fluent:text-column-two-24-filled" /> + </el-radio-button> + </el-tooltip> + <!--<el-tooltip class="item" content="三列" placement="bottom"> + <el-radio-button value="threeCol"> + <Icon icon="fluent:text-column-three-24-filled" /> + </el-radio-button> + </el-tooltip>--> </el-radio-group> </el-form-item> <el-form-item label="商品名称" prop="fields.name.show"> @@ -34,10 +35,34 @@ <el-checkbox v-model="formData.fields.name.show" /> </div> </el-form-item> + <el-form-item label="商品简介" prop="fields.introduction.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.introduction.color" /> + <el-checkbox v-model="formData.fields.introduction.show" /> + </div> + </el-form-item> <el-form-item label="商品价格" prop="fields.price.show"> <div class="flex gap-8px"> <ColorInput v-model="formData.fields.price.color" /> <el-checkbox v-model="formData.fields.price.show" /> + </div> + </el-form-item> + <el-form-item label="市场价" prop="fields.marketPrice.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.marketPrice.color" /> + <el-checkbox v-model="formData.fields.marketPrice.show" /> + </div> + </el-form-item> + <el-form-item label="商品销量" prop="fields.salesCount.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.salesCount.color" /> + <el-checkbox v-model="formData.fields.salesCount.show" /> + </div> + </el-form-item> + <el-form-item label="商品库存" prop="fields.stock.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.stock.color" /> + <el-checkbox v-model="formData.fields.stock.show" /> </div> </el-form-item> </el-card> @@ -47,9 +72,35 @@ </el-form-item> <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> - <template #tip> 建议尺寸:36 * 22 </template> + <template #tip> 建议尺寸:36 * 22</template> </UploadImg> </el-form-item> + </el-card> + <el-card header="按钮" class="property-group" shadow="never"> + <el-form-item label="按钮类型" prop="btnBuy.type"> + <el-radio-group v-model="formData.btnBuy.type"> + <el-radio-button value="text">文字</el-radio-button> + <el-radio-button value="img">图片</el-radio-button> + </el-radio-group> + </el-form-item> + <template v-if="formData.btnBuy.type === 'text'"> + <el-form-item label="按钮文字" prop="btnBuy.text"> + <el-input v-model="formData.btnBuy.text" /> + </el-form-item> + <el-form-item label="左侧背景" prop="btnBuy.bgBeginColor"> + <ColorInput v-model="formData.btnBuy.bgBeginColor" /> + </el-form-item> + <el-form-item label="右侧背景" prop="btnBuy.bgEndColor"> + <ColorInput v-model="formData.btnBuy.bgEndColor" /> + </el-form-item> + </template> + <template v-else> + <el-form-item label="图片" prop="btnBuy.imgUrl"> + <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px"> + <template #tip> 建议尺寸:56 * 56</template> + </UploadImg> + </el-form-item> + </template> </el-card> <el-card header="商品样式" class="property-group" shadow="never"> <el-form-item label="上圆角" prop="borderRadiusTop"> @@ -92,6 +143,7 @@ import { usePropertyForm } from '@/components/DiyEditor/util' import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity' import { CommonStatusEnum } from '@/utils/constants' +import SeckillShowcase from '@/views/mall/promotion/seckill/components/SeckillShowcase.vue' // 秒杀属性面板 defineOptions({ name: 'PromotionSeckillProperty' }) @@ -100,7 +152,7 @@ const emit = defineEmits(['update:modelValue']) const { formData } = usePropertyForm(props.modelValue, emit) // 活动列表 -const activityList = ref<SeckillActivityApi.SeckillActivityVO>([]) +const activityList = ref<SeckillActivityApi.SeckillActivityVO[]>([]) onMounted(async () => { const { list } = await SeckillActivityApi.getSeckillActivityPage({ status: CommonStatusEnum.ENABLE diff --git a/src/components/DiyEditor/components/mobile/SearchBar/property.vue b/src/components/DiyEditor/components/mobile/SearchBar/property.vue index 9002702..71f9493 100644 --- a/src/components/DiyEditor/components/mobile/SearchBar/property.vue +++ b/src/components/DiyEditor/components/mobile/SearchBar/property.vue @@ -13,12 +13,12 @@ <el-form-item label="框体样式"> <el-radio-group v-model="formData!.borderRadius"> <el-tooltip content="方形" placement="top"> - <el-radio-button :label="0"> + <el-radio-button :value="0"> <Icon icon="tabler:input-search" /> </el-radio-button> </el-tooltip> <el-tooltip content="圆形" placement="top"> - <el-radio-button :label="10"> + <el-radio-button :value="10"> <Icon icon="iconoir:input-search" /> </el-radio-button> </el-tooltip> @@ -30,12 +30,12 @@ <el-form-item label="文本位置" prop="placeholderPosition"> <el-radio-group v-model="formData!.placeholderPosition"> <el-tooltip content="居左" placement="top"> - <el-radio-button label="left"> + <el-radio-button value="left"> <Icon icon="ant-design:align-left-outlined" /> </el-radio-button> </el-tooltip> <el-tooltip content="居中" placement="top"> - <el-radio-button label="center"> + <el-radio-button value="center"> <Icon icon="ant-design:align-center-outlined" /> </el-radio-button> </el-tooltip> diff --git a/src/components/DiyEditor/components/mobile/TabBar/config.ts b/src/components/DiyEditor/components/mobile/TabBar/config.ts index 7e52666..88d706f 100644 --- a/src/components/DiyEditor/components/mobile/TabBar/config.ts +++ b/src/components/DiyEditor/components/mobile/TabBar/config.ts @@ -53,26 +53,26 @@ { text: '首页', url: '/pages/index/index', - iconUrl: 'http://xxxx/static/images/1-001.png', - activeIconUrl: 'http://xxxx/static/images/1-002.png' + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png', + activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png' }, { text: '分类', url: '/pages/index/category?id=3', - iconUrl: 'http://xxxx/static/images/2-001.png', - activeIconUrl: 'http://xxxx/static/images/2-002.png' + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png', + activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png' }, { text: '购物车', url: '/pages/index/cart', - iconUrl: 'http://xxxx/static/images/3-001.png', - activeIconUrl: 'http://xxxx/static/images/3-002.png' + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png', + activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png' }, { text: '我的', url: '/pages/index/user', - iconUrl: 'http://xxxx/static/images/4-001.png', - activeIconUrl: 'http://xxxx/static/images/4-002.png' + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png', + activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png' } ] } diff --git a/src/components/DiyEditor/components/mobile/TabBar/property.vue b/src/components/DiyEditor/components/mobile/TabBar/property.vue index 6ace5af..d1da142 100644 --- a/src/components/DiyEditor/components/mobile/TabBar/property.vue +++ b/src/components/DiyEditor/components/mobile/TabBar/property.vue @@ -27,8 +27,8 @@ </el-form-item> <el-form-item label="导航背景"> <el-radio-group v-model="formData!.style.bgType"> - <el-radio-button label="color">纯色</el-radio-button> - <el-radio-button label="img">图片</el-radio-button> + <el-radio-button value="color">纯色</el-radio-button> + <el-radio-button value="img">图片</el-radio-button> </el-radio-group> </el-form-item> <el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'"> @@ -79,7 +79,7 @@ </template> <script setup lang="ts"> -import { TabBarProperty, THEME_LIST } from './config' +import { TabBarProperty, component, THEME_LIST } from './config' import { usePropertyForm } from '@/components/DiyEditor/util' // 底部导航栏 defineOptions({ name: 'TabBarProperty' }) @@ -88,6 +88,9 @@ const emit = defineEmits(['update:modelValue']) const { formData } = usePropertyForm(props.modelValue, emit) +// 将数据库的值更新到右侧属性栏 +component.property.items = formData.value.items + // 要的主题 const handleThemeChange = () => { const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme) diff --git a/src/components/DiyEditor/components/mobile/TitleBar/property.vue b/src/components/DiyEditor/components/mobile/TitleBar/property.vue index 4eb3259..44d6bb6 100644 --- a/src/components/DiyEditor/components/mobile/TitleBar/property.vue +++ b/src/components/DiyEditor/components/mobile/TitleBar/property.vue @@ -10,12 +10,12 @@ <el-form-item label="标题位置" prop="textAlign"> <el-radio-group v-model="formData!.textAlign"> <el-tooltip content="居左" placement="top"> - <el-radio-button label="left"> + <el-radio-button value="left"> <Icon icon="ant-design:align-left-outlined" /> </el-radio-button> </el-tooltip> <el-tooltip content="居中" placement="top"> - <el-radio-button label="center"> + <el-radio-button value="center"> <Icon icon="ant-design:align-center-outlined" /> </el-radio-button> </el-tooltip> @@ -88,9 +88,9 @@ <template v-if="formData.more.show"> <el-form-item label="样式" prop="more.type"> <el-radio-group v-model="formData.more.type"> - <el-radio label="text">文字</el-radio> - <el-radio label="icon">图标</el-radio> - <el-radio label="all">文字+图标</el-radio> + <el-radio value="text">文字</el-radio> + <el-radio value="icon">图标</el-radio> + <el-radio value="all">文字+图标</el-radio> </el-radio-group> </el-form-item> <el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'"> diff --git a/src/components/DiyEditor/components/mobile/UserCard/index.vue b/src/components/DiyEditor/components/mobile/UserCard/index.vue index 7005b97..14b447c 100644 --- a/src/components/DiyEditor/components/mobile/UserCard/index.vue +++ b/src/components/DiyEditor/components/mobile/UserCard/index.vue @@ -5,7 +5,7 @@ <el-avatar :size="60"> <Icon icon="ep:avatar" :size="60" /> </el-avatar> - <span class="text-18px font-bold">工业互联网平台</span> + <span class="text-18px font-bold">芋道源码</span> </div> <Icon icon="tdesign:qrcode" :size="20" /> </div> diff --git a/src/components/Draggable/index.vue b/src/components/Draggable/index.vue index 2175946..3d7906b 100644 --- a/src/components/Draggable/index.vue +++ b/src/components/Draggable/index.vue @@ -13,9 +13,9 @@ class="mb-4px flex flex-col gap-4px border border-gray-2 border-rounded rounded border-solid p-8px" > <!-- 操作按钮区 --> - <div class="m--8px m-b-4px flex flex-row items-center justify-between bg-gray-1 p-8px"> + <div class="m--8px m-b-4px flex flex-row items-center justify-between p-8px" style="background-color: var(--app-content-bg-color);"> <el-tooltip content="拖动排序"> - <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" /> + <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" style="color: #8a909c;" /> </el-tooltip> <el-tooltip content="删除"> <Icon diff --git a/src/components/Echart/src/Echart.vue b/src/components/Echart/src/Echart.vue index fd3342d..02738ca 100644 --- a/src/components/Echart/src/Echart.vue +++ b/src/components/Echart/src/Echart.vue @@ -9,6 +9,10 @@ import { isString } from '@/utils/is' import { useDesign } from '@/hooks/web/useDesign' +import 'echarts/lib/component/markPoint' +import 'echarts/lib/component/markLine' +import 'echarts/lib/component/markArea' + defineOptions({ name: 'EChart' }) const { getPrefixCls, variables } = useDesign() @@ -94,13 +98,13 @@ contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0] unref(contentEl) && - (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) + (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) }) onBeforeUnmount(() => { window.removeEventListener('resize', resizeHandler) unref(contentEl) && - (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) + (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) }) onActivated(() => { diff --git a/src/components/Editor/src/Editor.vue b/src/components/Editor/src/Editor.vue index 8dd0645..e16776c 100644 --- a/src/components/Editor/src/Editor.vue +++ b/src/components/Editor/src/Editor.vue @@ -7,6 +7,7 @@ import { ElMessage } from 'element-plus' import { useLocaleStore } from '@/store/modules/locale' import { getAccessToken, getTenantId } from '@/utils/auth' +import { getUploadUrl } from '@/components/UploadFile/src/useUpload' defineOptions({ name: 'Editor' }) @@ -88,7 +89,7 @@ scroll: true, MENU_CONF: { ['uploadImage']: { - server: import.meta.env.VITE_UPLOAD_URL, + server: getUploadUrl(), // 单个文件的最大体积限制,默认为 2M maxFileSize: 5 * 1024 * 1024, // 最多可上传几个文件,默认为 100 @@ -136,7 +137,7 @@ } }, ['uploadVideo']: { - server: import.meta.env.VITE_UPLOAD_URL, + server: getUploadUrl(), // 单个文件的最大体积限制,默认为 10M maxFileSize: 10 * 1024 * 1024, // 最多可上传几个文件,默认为 100 diff --git a/src/components/FormCreate/src/components/useApiSelect.tsx b/src/components/FormCreate/src/components/useApiSelect.tsx index d668cb8..8ff95fb 100644 --- a/src/components/FormCreate/src/components/useApiSelect.tsx +++ b/src/components/FormCreate/src/components/useApiSelect.tsx @@ -104,9 +104,9 @@ parseOptions0(data) return } - // 情况三:不是 iailab-plat 标准返回 + // 情况三:不是 yudao-vue-pro 标准返回 console.warn( - `接口[${props.url}] 返回结果不是 iailab-plat 标准返回建议采用自定义解析函数处理` + `接口[${props.url}] 返回结果不是 yudao-vue-pro 标准返回建议采用自定义解析函数处理` ) } @@ -185,7 +185,6 @@ </el-select> ) } - // debugger return ( <el-select class="w-1/1" diff --git a/src/components/FormCreate/src/config/useDictSelectRule.ts b/src/components/FormCreate/src/config/useDictSelectRule.ts index 5c5e8ca..f232f48 100644 --- a/src/components/FormCreate/src/config/useDictSelectRule.ts +++ b/src/components/FormCreate/src/config/useDictSelectRule.ts @@ -48,7 +48,7 @@ }, { type: 'select', - field: 'dictValueType', + field: 'valueType', title: '字典值类型', value: 'str', options: [ diff --git a/src/components/FormCreate/src/utils/index.ts b/src/components/FormCreate/src/utils/index.ts index 2d4a6fd..a2b3e67 100644 --- a/src/components/FormCreate/src/utils/index.ts +++ b/src/components/FormCreate/src/utils/index.ts @@ -16,3 +16,46 @@ return rule }) } + +/** + * 解析表单组件的 field, title 等字段(递归,如果组件包含子组件) + * + * @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule + * @param fields 解析后表单组件字段 + * @param parentTitle 如果是子表单,子表单的标题,默认为空 + */ +export const parseFormFields = ( + rule: Record<string, any>, + fields: Array<Record<string, any>> = [], + parentTitle: string = '' +) => { + const { type, field, $required, title: tempTitle, children } = rule + if (field && tempTitle) { + let title = tempTitle + if (parentTitle) { + title = `${parentTitle}.${tempTitle}` + } + let required = false + if ($required) { + required = true + } + fields.push({ + field, + title, + type, + required + }) + // TODO 子表单 需要处理子表单字段 + // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) { + // // 解析子表单的字段 + // rule.props.rule.forEach((item) => { + // parseFields(item, fieldsPermission, title) + // }) + // } + } + if (children && Array.isArray(children)) { + children.forEach((rule) => { + parseFormFields(rule, fields) + }) + } +} diff --git a/src/components/IFrame/src/IFrame.vue b/src/components/IFrame/src/IFrame.vue index 19de51a..64ffc0e 100644 --- a/src/components/IFrame/src/IFrame.vue +++ b/src/components/IFrame/src/IFrame.vue @@ -7,26 +7,41 @@ src: propTypes.string.def('') }) const loading = ref(true) -const height = ref('') const frameRef = ref<HTMLElement | null>(null) const init = () => { - height.value = document.documentElement.clientHeight - 94.5 + 'px' - loading.value = false + nextTick(() => { + loading.value = true + if (!frameRef.value) return + frameRef.value.onload = () => { + loading.value = false + } + }) } onMounted(() => { - setTimeout(() => { - init() - }, 300) + init() }) +watch( + () => props.src, + () => { + init() + } +) </script> <template> - <div v-loading="loading" :style="'height:' + height"> + <div + v-loading="loading" + class="w-full h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]" + > <iframe ref="frameRef" :src="props.src" - frameborder="no" + frameborder="0" scrolling="auto" - style="width: 100%; height: 100%" + height="100%" + width="100%" + allowfullscreen="true" + webkitallowfullscreen="true" + mozallowfullscreen="true" ></iframe> </div> </template> diff --git a/src/components/Icon/src/IconSelect.vue b/src/components/Icon/src/IconSelect.vue index d4a5b07..76cc6d5 100644 --- a/src/components/Icon/src/IconSelect.vue +++ b/src/components/Icon/src/IconSelect.vue @@ -11,6 +11,10 @@ modelValue: { require: false, type: String + }, + clearable: { + require: false, + type: Boolean } }) const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>() @@ -92,6 +96,12 @@ currentPage.value = page } +function clearIcon() { + icon.value = '' + emit('update:modelValue', '') + visible.value = false +} + watch( () => { return props.modelValue @@ -115,14 +125,14 @@ <template> <div class="selector"> - <ElInput v-model="inputValue" @click="visible = !visible"> + <ElInput v-model="inputValue" @click="visible = !visible" :clearable="props.clearable" @clear="clearIcon"> <template #append> <ElPopover :popper-options="{ placement: 'auto' }" :visible="visible" - :width="350" + :width="355" popper-class="pure-popper" trigger="click" > @@ -147,7 +157,7 @@ > <ElDivider border-style="dashed" class="tab-divider" /> <ElScrollbar height="220px"> - <ul class="ml-2 flex flex-wrap px-2"> + <ul class="ml-2 flex flex-wrap"> <li v-for="(item, key) in pageList" :key="key" @@ -171,7 +181,7 @@ background class="h-10 flex items-center justify-center" layout="prev, pager, next" - small + size="small" @current-change="onCurrentChange" /> </ElPopover> diff --git a/src/components/RouterSearch/index.vue b/src/components/RouterSearch/index.vue index c035242..42a4174 100644 --- a/src/components/RouterSearch/index.vue +++ b/src/components/RouterSearch/index.vue @@ -20,6 +20,7 @@ <div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch"> <Icon icon="ep:search" /> <el-select + @click.stop filterable :reserve-keyword="false" remote @@ -78,7 +79,12 @@ function handleChange(path) { router.push({ path }) + hiddenSearch() hiddenTopSearch() +} + +function hiddenSearch() { + showSearch.value = false } function hiddenTopSearch() { @@ -98,6 +104,8 @@ // 监听 ctrl + k function listenKey(event) { if ((event.ctrlKey || event.metaKey) && event.key === 'k') { + // 阻止触发浏览器默认事件 + event.preventDefault() showSearch.value = !showSearch.value // 这里可以执行相应的操作(例如打开搜索框等) } diff --git a/src/components/ShortcutDateRangePicker/index.vue b/src/components/ShortcutDateRangePicker/index.vue index 117c079..78c5130 100644 --- a/src/components/ShortcutDateRangePicker/index.vue +++ b/src/components/ShortcutDateRangePicker/index.vue @@ -1,9 +1,9 @@ <template> <div class="flex flex-row items-center gap-2"> <el-radio-group v-model="shortcutDays" @change="handleShortcutDaysChange"> - <el-radio-button :label="1">昨天</el-radio-button> - <el-radio-button :label="7">最近7天</el-radio-button> - <el-radio-button :label="30">最近30天</el-radio-button> + <el-radio-button :value="1">昨天</el-radio-button> + <el-radio-button :value="7">最近7天</el-radio-button> + <el-radio-button :value="30">最近30天</el-radio-button> </el-radio-group> <el-date-picker v-model="times" diff --git a/src/components/SimpleProcessDesigner/src/addNode.vue b/src/components/SimpleProcessDesigner/src/addNode.vue deleted file mode 100644 index 6d09ae8..0000000 --- a/src/components/SimpleProcessDesigner/src/addNode.vue +++ /dev/null @@ -1,237 +0,0 @@ -/* 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 deleted file mode 100644 index 3c9d5eb..0000000 --- a/src/components/SimpleProcessDesigner/src/nodeWrap.vue +++ /dev/null @@ -1,297 +0,0 @@ -<!-- 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 deleted file mode 100644 index f4acd76..0000000 --- a/src/components/SimpleProcessDesigner/src/util.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * 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 deleted file mode 100644 index 888b1a8..0000000 --- a/src/components/SimpleProcessDesigner/theme/workflow.css +++ /dev/null @@ -1,1292 +0,0 @@ - -.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/SimpleProcessDesignerV2/src/NodeHandler.vue b/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue new file mode 100644 index 0000000..4dfd51a --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue @@ -0,0 +1,231 @@ +<template> + <div class="node-handler-wrapper"> + <div class="node-handler"> + <el-popover + trigger="hover" + v-model:visible="popoverShow" + placement="right-start" + width="auto" + v-if="!readonly" + > + <div class="handler-item-wrapper"> + <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)"> + <div class="approve handler-item-icon"> + <span class="iconfont icon-approve icon-size"></span> + </div> + <div class="handler-item-text">审批人</div> + </div> + <div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)"> + <div class="handler-item-icon copy"> + <span class="iconfont icon-size icon-copy"></span> + </div> + <div class="handler-item-text">抄送</div> + </div> + <div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)"> + <div class="handler-item-icon condition"> + <span class="iconfont icon-size icon-exclusive"></span> + </div> + <div class="handler-item-text">条件分支</div> + </div> + <div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)"> + <div class="handler-item-icon parallel"> + <span class="iconfont icon-size icon-parallel"></span> + </div> + <div class="handler-item-text">并行分支</div> + </div> + <div class="handler-item" @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)"> + <div class="handler-item-icon inclusive"> + <span class="iconfont icon-size icon-inclusive"></span> + </div> + <div class="handler-item-text">包容分支</div> + </div> + <div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)"> + <!-- TODO @芋艿 需要更换一下iconfont的图标 --> + <div class="handler-item-icon copy"> + <span class="iconfont icon-size icon-copy"></span> + </div> + <div class="handler-item-text">延迟器</div> + </div> + </div> + <template #reference> + <div class="add-icon"><Icon icon="ep:plus" /></div> + </template> + </el-popover> + </div> + </div> +</template> + +<script setup lang="ts"> +import { + ApproveMethodType, + AssignEmptyHandlerType, + AssignStartUserHandlerType, + NODE_DEFAULT_NAME, + NodeType, + RejectHandlerType, + SimpleFlowNode +} from './consts' +import { generateUUID } from '@/utils' + +defineOptions({ + name: 'NodeHandler' +}) + +const message = useMessage() // 消息弹窗 + +const popoverShow = ref(false) +const props = defineProps({ + childNode: { + type: Object as () => SimpleFlowNode, + default: null + }, + currentNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +const emits = defineEmits(['update:childNode']) + +const readonly = inject<Boolean>('readonly') // 是否只读 + +const addNode = (type: number) => { + // 校验:条件分支、包容分支后面,不允许直接添加并行分支 + if ( + type === NodeType.PARALLEL_BRANCH_NODE && + [NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes( + props.currentNode?.type + ) + ) { + message.error('条件分支、包容分支后面,不允许直接添加并行分支') + return + } + + popoverShow.value = false + if (type === NodeType.USER_TASK_NODE) { + const id = 'Activity_' + generateUUID() + const data: SimpleFlowNode = { + id: id, + name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string, + showText: '', + type: NodeType.USER_TASK_NODE, + approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE, + // 超时处理 + rejectHandler: { + type: RejectHandlerType.FINISH_PROCESS + }, + timeoutHandler: { + enable: false + }, + assignEmptyHandler: { + type: AssignEmptyHandlerType.APPROVE + }, + assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT, + childNode: props.childNode + } + emits('update:childNode', data) + } + if (type === NodeType.COPY_TASK_NODE) { + const data: SimpleFlowNode = { + id: 'Activity_' + generateUUID(), + name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string, + showText: '', + type: NodeType.COPY_TASK_NODE, + childNode: props.childNode + } + emits('update:childNode', data) + } + if (type === NodeType.CONDITION_BRANCH_NODE) { + const data: SimpleFlowNode = { + name: '条件分支', + type: NodeType.CONDITION_BRANCH_NODE, + id: 'GateWay_' + generateUUID(), + childNode: props.childNode, + conditionNodes: [ + { + id: 'Flow_' + generateUUID(), + name: '条件1', + showText: '', + type: NodeType.CONDITION_NODE, + childNode: undefined, + conditionType: 1, + defaultFlow: false + }, + { + id: 'Flow_' + generateUUID(), + name: '其它情况', + showText: '未满足其它条件时,将进入此分支', + type: NodeType.CONDITION_NODE, + childNode: undefined, + conditionType: undefined, + defaultFlow: true + } + ] + } + emits('update:childNode', data) + } + if (type === NodeType.PARALLEL_BRANCH_NODE) { + const data: SimpleFlowNode = { + name: '并行分支', + type: NodeType.PARALLEL_BRANCH_NODE, + id: 'GateWay_' + generateUUID(), + childNode: props.childNode, + conditionNodes: [ + { + id: 'Flow_' + generateUUID(), + name: '并行1', + showText: '无需配置条件同时执行', + type: NodeType.CONDITION_NODE, + childNode: undefined + }, + { + id: 'Flow_' + generateUUID(), + name: '并行2', + showText: '无需配置条件同时执行', + type: NodeType.CONDITION_NODE, + childNode: undefined + } + ] + } + emits('update:childNode', data) + } + if (type === NodeType.INCLUSIVE_BRANCH_NODE) { + const data: SimpleFlowNode = { + name: '包容分支', + type: NodeType.INCLUSIVE_BRANCH_NODE, + id: 'GateWay_' + generateUUID(), + childNode: props.childNode, + conditionNodes: [ + { + id: 'Flow_' + generateUUID(), + name: '包容条件1', + showText: '', + type: NodeType.CONDITION_NODE, + childNode: undefined, + defaultFlow: false + }, + { + id: 'Flow_' + generateUUID(), + name: '其它情况', + showText: '未满足其它条件时,将进入此分支', + type: NodeType.CONDITION_NODE, + childNode: undefined, + defaultFlow: true + } + ] + } + emits('update:childNode', data) + } + if (type === NodeType.DELAY_TIMER_NODE) { + const data: SimpleFlowNode = { + id: 'Activity_' + generateUUID(), + name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string, + showText: '', + type: NodeType.DELAY_TIMER_NODE, + childNode: props.childNode + } + emits('update:childNode', data) + } +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue b/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue new file mode 100644 index 0000000..419501a --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue @@ -0,0 +1,125 @@ +<template> + <!-- 发起人节点 --> + <StartUserNode + v-if="currentNode && currentNode.type === NodeType.START_USER_NODE" + :flow-node="currentNode" + /> + <!-- 审批节点 --> + <UserTaskNode + v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE" + :flow-node="currentNode" + @update:flow-node="handleModelValueUpdate" + @find:parent-node="findFromParentNode" + /> + <!-- 抄送节点 --> + <CopyTaskNode + v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE" + :flow-node="currentNode" + @update:flow-node="handleModelValueUpdate" + /> + <!-- 条件节点 --> + <ExclusiveNode + v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE" + :flow-node="currentNode" + @update:model-value="handleModelValueUpdate" + @find:parent-node="findFromParentNode" + /> + <!-- 并行节点 --> + <ParallelNode + v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE" + :flow-node="currentNode" + @update:model-value="handleModelValueUpdate" + @find:parent-node="findFromParentNode" + /> + <!-- 包容分支节点 --> + <InclusiveNode + v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE" + :flow-node="currentNode" + @update:model-value="handleModelValueUpdate" + @find:parent-node="findFromParentNode" + /> + <!-- 延迟器节点 --> + <DelayTimerNode + v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE" + :flow-node="currentNode" + @update:flow-node="handleModelValueUpdate" + /> + <!-- 递归显示孩子节点 --> + <ProcessNodeTree + v-if="currentNode && currentNode.childNode" + v-model:flow-node="currentNode.childNode" + :parent-node="currentNode" + @find:recursive-find-parent-node="recursiveFindParentNode" + /> + + <!-- 结束节点 --> + <EndEventNode + v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" + :flow-node="currentNode" + /> +</template> +<script setup lang="ts"> +import StartUserNode from './nodes/StartUserNode.vue' +import EndEventNode from './nodes/EndEventNode.vue' +import UserTaskNode from './nodes/UserTaskNode.vue' +import CopyTaskNode from './nodes/CopyTaskNode.vue' +import ExclusiveNode from './nodes/ExclusiveNode.vue' +import ParallelNode from './nodes/ParallelNode.vue' +import InclusiveNode from './nodes/InclusiveNode.vue' +import DelayTimerNode from './nodes/DelayTimerNode.vue' +import { SimpleFlowNode, NodeType } from './consts' +import { useWatchNode } from './node' +defineOptions({ + name: 'ProcessNodeTree' +}) +const props = defineProps({ + parentNode: { + type: Object as () => SimpleFlowNode, + default: () => null + }, + flowNode: { + type: Object as () => SimpleFlowNode, + default: () => null + } +}) +const emits = defineEmits<{ + 'update:flowNode': [node: SimpleFlowNode | undefined] + 'find:recursiveFindParentNode': [ + nodeList: SimpleFlowNode[], + curentNode: SimpleFlowNode, + nodeType: number + ] +}>() + +const currentNode = useWatchNode(props) + +// 用于删除节点 +const handleModelValueUpdate = (updateValue) => { + emits('update:flowNode', updateValue) +} + +const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => { + emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType) +} + +// 递归从父节点中查询匹配的节点 +const recursiveFindParentNode = ( + nodeList: SimpleFlowNode[], + findNode: SimpleFlowNode, + nodeType: number +) => { + if (!findNode) { + return + } + if (findNode.type === NodeType.START_USER_NODE) { + nodeList.push(findNode) + return + } + + if (findNode.type === nodeType) { + nodeList.push(findNode) + } + emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType) +} +</script> +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue new file mode 100644 index 0000000..22e6073 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue @@ -0,0 +1,306 @@ +<template> + <div v-loading="loading" class="overflow-auto"> + <SimpleProcessModel + ref="simpleProcessModelRef" + v-if="processNodeTree" + :flow-node="processNodeTree" + :readonly="false" + @save="saveSimpleFlowModel" + /> + <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false"> + <div class="mb-2">以下节点内容不完善,请修改后保存</div> + <div + class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal" + v-for="(item, index) in errorNodes" + :key="index" + > + {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }} + </div> + <template #footer> + <el-button type="primary" @click="errorDialogVisible = false">知道了</el-button> + </template> + </Dialog> + </div> +</template> + +<script setup lang="ts"> +import SimpleProcessModel from './SimpleProcessModel.vue' +import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple' +import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts' +import { getModel } from '@/api/bpm/model' +import { getForm, FormVO } from '@/api/bpm/form' +import { 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' + +defineOptions({ + name: 'SimpleProcessDesigner' +}) + +const emits = defineEmits(['success', 'init-finished']) // 保存成功事件 + +const props = defineProps({ + modelId: { + type: String, + required: false + }, + modelKey: { + type: String, + required: false + }, + modelName: { + type: String, + required: false + }, + // 可发起流程的人员编号 + startUserIds : { + type: Array, + required: false + }, + value: { + type: [String, Object], + required: false + } +}) + +const loading = ref(false) +const formFields = ref<string[]>([]) +const formType = ref(20) +const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表 +const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表 +const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 +const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 +const deptTreeOptions = ref() +const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 + +// 添加当前值的引用 +const currentValue = ref<SimpleFlowNode | undefined>() + +provide('formFields', formFields) +provide('formType', formType) +provide('roleList', roleOptions) +provide('postList', postOptions) +provide('userList', userOptions) +provide('deptList', deptOptions) +provide('userGroupList', userGroupOptions) +provide('deptTree', deptTreeOptions) +provide('startUserIds', props.startUserIds) + +const message = useMessage() // 国际化 +const processNodeTree = ref<SimpleFlowNode | undefined>() +const errorDialogVisible = ref(false) +let errorNodes: SimpleFlowNode[] = [] + +// 添加更新模型的方法 +const updateModel = () => { + if (!processNodeTree.value) { + processNodeTree.value = { + name: '发起人', + type: NodeType.START_USER_NODE, + id: NodeId.START_USER_NODE_ID, + childNode: { + id: NodeId.END_EVENT_NODE_ID, + name: '结束', + type: NodeType.END_EVENT_NODE + } + } + // 初始化时也触发一次保存 + saveSimpleFlowModel(processNodeTree.value) + } +} + +// 加载流程数据 +const loadProcessData = async (data: any) => { + try { + if (data) { + const parsedData = typeof data === 'string' ? JSON.parse(data) : data + processNodeTree.value = parsedData + currentValue.value = parsedData + // 确保数据加载后刷新视图 + await nextTick() + if (simpleProcessModelRef.value?.refresh) { + await simpleProcessModelRef.value.refresh() + } + } + } catch (error) { + console.error('加载流程数据失败:', error) + } +} + +// 监听属性变化 +watch( + () => props.value, + async (newValue, oldValue) => { + if (newValue && newValue !== oldValue) { + await loadProcessData(newValue) + } + }, + { immediate: true, deep: true } +) + +// 监听流程节点树变化,自动保存 +watch( + () => processNodeTree.value, + async (newValue, oldValue) => { + if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) { + await saveSimpleFlowModel(newValue) + } + }, + { deep: true } +) + +const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => { + if (!simpleModelNode) { + return + } + + // 校验节点 + errorNodes = [] + validateNode(simpleModelNode, errorNodes) + if (errorNodes.length > 0) { + errorDialogVisible.value = true + return + } + + try { + if (props.modelId) { + // 编辑模式 + const data = { + id: props.modelId, + simpleModel: simpleModelNode + } + await updateBpmSimpleModel(data) + } + // 无论是编辑还是新建模式,都更新当前值并触发事件 + currentValue.value = simpleModelNode + emits('success', simpleModelNode) + } catch (error) { + console.error('保存失败:', error) + } +} + +// 校验节点设置。 暂时以 showText 为空 未节点错误配置 +const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { + if (node) { + const { type, showText, conditionNodes } = node + if (type == NodeType.END_EVENT_NODE) { + return + } + if (type == NodeType.START_USER_NODE) { + // 发起人节点暂时不用校验,直接校验孩子节点 + validateNode(node.childNode, errorNodes) + } + + if ( + type === NodeType.USER_TASK_NODE || + type === NodeType.COPY_TASK_NODE || + type === NodeType.CONDITION_NODE + ) { + if (!showText) { + errorNodes.push(node) + } + validateNode(node.childNode, errorNodes) + } + + if ( + type == NodeType.CONDITION_BRANCH_NODE || + type == NodeType.PARALLEL_BRANCH_NODE || + type == NodeType.INCLUSIVE_BRANCH_NODE + ) { + // 分支节点 + // 1. 先校验各个分支 + conditionNodes?.forEach((item) => { + validateNode(item, errorNodes) + }) + // 2. 校验孩子节点 + validateNode(node.childNode, errorNodes) + } + } +} + +onMounted(async () => { + try { + loading.value = true + // 获取表单字段 + if (props.modelId) { + const bpmnModel = await getModel(props.modelId) + if (bpmnModel) { + formType.value = bpmnModel.formType + if (formType.value === 10) { + const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO + formFields.value = bpmnForm?.fields + } + } + } + // 获得角色列表 + roleOptions.value = await RoleApi.getSimpleRoleList() + // 获得岗位列表 + postOptions.value = await PostApi.getSimplePostList() + // 获得用户列表 + userOptions.value = await UserApi.getSimpleUserList() + // 获得部门列表 + deptOptions.value = await DeptApi.getSimpleDeptList() + deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id') + // 获取用户组列表 + userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList() + + // 加载流程数据 + if (props.modelId) { + // 获取 SIMPLE 设计器模型 + const result = await getBpmSimpleModel(props.modelId) + if (result) { + await loadProcessData(result) + } else { + updateModel() + } + } else if (props.value) { + await loadProcessData(props.value) + } else { + updateModel() + } + } finally { + loading.value = false + emits('init-finished') + } +}) + +const simpleProcessModelRef = ref() + +/** 获取当前流程数据 */ +const getCurrentFlowData = async () => { + try { + if (simpleProcessModelRef.value) { + const data = await simpleProcessModelRef.value.getCurrentFlowData() + if (data) { + currentValue.value = data + return data + } + } + return currentValue.value + } catch (error) { + console.error('获取流程数据失败:', error) + return currentValue.value + } +} + +// 刷新方法 +const refresh = async () => { + try { + if (currentValue.value) { + await loadProcessData(currentValue.value) + } + } catch (error) { + console.error('刷新失败:', error) + } +} + +defineExpose({ + getCurrentFlowData, + updateModel, + loadProcessData, + refresh +}) +</script> diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue new file mode 100644 index 0000000..ccd1f10 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue @@ -0,0 +1,148 @@ +<template> + <div class="simple-process-model-container position-relative"> + <div class="position-absolute top-0px right-0px bg-#fff"> + <el-row type="flex" justify="end"> + <el-button-group key="scale-control" size="default"> + <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" /> + <el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" /> + <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button> + <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" /> + </el-button-group> + </el-row> + </div> + <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`"> + <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" /> + </div> + </div> + <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false"> + <div class="mb-2">以下节点内容不完善,请修改后保存</div> + <div + class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal" + v-for="(item, index) in errorNodes" + :key="index" + > + {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }} + </div> + <template #footer> + <el-button type="primary" @click="errorDialogVisible = false">知道了</el-button> + </template> + </Dialog> +</template> + +<script setup lang="ts"> +import ProcessNodeTree from './ProcessNodeTree.vue' +import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts' +import { useWatchNode } from './node' +import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' + +defineOptions({ + name: 'SimpleProcessModel' +}) + +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + }, + readonly: { + type: Boolean, + required: false, + default: true + } +}) + +const emits = defineEmits<{ + 'save': [node: SimpleFlowNode | undefined] +}>() + +const processNodeTree = useWatchNode(props) + +provide('readonly', props.readonly) +let scaleValue = ref(100) +const MAX_SCALE_VALUE = 200 +const MIN_SCALE_VALUE = 50 + +// 放大 +const zoomIn = () => { + if (scaleValue.value == MAX_SCALE_VALUE) { + return + } + scaleValue.value += 10 +} + +// 缩小 +const zoomOut = () => { + if (scaleValue.value == MIN_SCALE_VALUE) { + return + } + scaleValue.value -= 10 +} + +const processReZoom = () => { + scaleValue.value = 100 +} + +const errorDialogVisible = ref(false) +let errorNodes: SimpleFlowNode[] = [] + +// 校验节点设置。 暂时以 showText 为空 未节点错误配置 +const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { + if (node) { + const { type, showText, conditionNodes } = node + if (type == NodeType.END_EVENT_NODE) { + return + } + if (type == NodeType.START_USER_NODE) { + // 发起人节点暂时不用校验,直接校验孩子节点 + validateNode(node.childNode, errorNodes) + } + + if ( + type === NodeType.USER_TASK_NODE || + type === NodeType.COPY_TASK_NODE || + type === NodeType.CONDITION_NODE + ) { + if (!showText) { + errorNodes.push(node) + } + validateNode(node.childNode, errorNodes) + } + + if ( + type == NodeType.CONDITION_BRANCH_NODE || + type == NodeType.PARALLEL_BRANCH_NODE || + type == NodeType.INCLUSIVE_BRANCH_NODE + ) { + // 分支节点 + // 1. 先校验各个分支 + conditionNodes?.forEach((item) => { + validateNode(item, errorNodes) + }) + // 2. 校验孩子节点 + validateNode(node.childNode, errorNodes) + } + } +} + +/** 获取当前流程数据 */ +const getCurrentFlowData = async () => { + try { + errorNodes = [] + validateNode(processNodeTree.value, errorNodes) + if (errorNodes.length > 0) { + errorDialogVisible.value = true + return undefined + } + return processNodeTree.value + } catch (error) { + console.error('获取流程数据失败:', error) + return undefined + } +} + +defineExpose({ + getCurrentFlowData +}) +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue new file mode 100644 index 0000000..abf73b4 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue @@ -0,0 +1,48 @@ +<template> + <SimpleProcessModel :flow-node="simpleModel" :readonly="true" /> +</template> + +<script setup lang="ts"> +import { useWatchNode } from './node' +import { SimpleFlowNode } from './consts' + +defineOptions({ + name: 'SimpleProcessViewer' +}) + +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + }, + // 流程任务 + tasks: { + type: Array, + default: () => [] as any[] + }, + // 流程实例 + processInstance: { + type: Object, + default: () => undefined + } +}) +const approveTasks = ref<any[]>(props.tasks) +const currentProcessInstance = ref(props.processInstance) +const simpleModel = useWatchNode(props) +watch( + () => props.tasks, + (newValue) => { + approveTasks.value = newValue + } +) +watch( + () => props.processInstance, + (newValue) => { + currentProcessInstance.value = newValue + } +) + +provide('tasks', approveTasks) +provide('processInstance', currentProcessInstance) +</script> +p diff --git a/src/components/SimpleProcessDesignerV2/src/consts.ts b/src/components/SimpleProcessDesignerV2/src/consts.ts new file mode 100644 index 0000000..10d8a21 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/consts.ts @@ -0,0 +1,606 @@ +// @ts-ignore +import { DictDataVO } from '@/api/system/dict/types' +import { TaskStatusEnum } from '@/api/bpm/task' +/** + * 节点类型 + */ +export enum NodeType { + /** + * 结束节点 + */ + END_EVENT_NODE = 1, + /** + * 发起人节点 + */ + START_USER_NODE = 10, + /** + * 审批人节点 + */ + USER_TASK_NODE = 11, + + /** + * 抄送人节点 + */ + COPY_TASK_NODE = 12, + + /** + * 延迟器节点 + */ + DELAY_TIMER_NODE = 14, + + /** + * 条件节点 + */ + CONDITION_NODE = 50, + /** + * 条件分支节点 (对应排他网关) + */ + CONDITION_BRANCH_NODE = 51, + /** + * 并行分支节点 (对应并行网关) + */ + PARALLEL_BRANCH_NODE = 52, + + /** + * 包容分支节点 (对应包容网关) + */ + INCLUSIVE_BRANCH_NODE = 53 +} + +export enum NodeId { + /** + * 发起人节点 Id + */ + START_USER_NODE_ID = 'StartUserNode', + + /** + * 发起人节点 Id + */ + END_EVENT_NODE_ID = 'EndEvent' +} + +/** + * 节点结构定义 + */ +export interface SimpleFlowNode { + id: string + type: NodeType + name: string + showText?: string + // 孩子节点 + childNode?: SimpleFlowNode + // 条件节点 + conditionNodes?: SimpleFlowNode[] + // 审批类型 + approveType?: ApproveType + // 候选人策略 + candidateStrategy?: number + // 候选人参数 + candidateParam?: string + // 多人审批方式 + approveMethod?: ApproveMethodType + //通过比例 + approveRatio?: number + // 审批按钮设置 + buttonsSetting?: any[] + // 表单权限 + fieldsPermission?: Array<Record<string, any>> + // 审批任务超时处理 + timeoutHandler?: TimeoutHandler + // 审批任务拒绝处理 + rejectHandler?: RejectHandler + // 审批人为空的处理 + assignEmptyHandler?: AssignEmptyHandler + // 审批节点的审批人与发起人相同时,对应的处理类型 + assignStartUserHandlerType?: number + // 条件类型 + conditionType?: ConditionType + // 条件表达式 + conditionExpression?: string + // 条件组 + conditionGroups?: ConditionGroup + // 是否默认的条件 + defaultFlow?: boolean + // 活动的状态,用于前端节点状态展示 + activityStatus?: TaskStatusEnum + // 延迟设置 + delaySetting?: DelaySetting +} +// 候选人策略枚举 ( 用于审批节点。抄送节点 ) +export enum CandidateStrategy { + /** + * 指定角色 + */ + ROLE = 10, + /** + * 部门成员 + */ + DEPT_MEMBER = 20, + /** + * 部门的负责人 + */ + DEPT_LEADER = 21, + /** + * 连续多级部门的负责人 + */ + MULTI_LEVEL_DEPT_LEADER = 23, + /** + * 指定岗位 + */ + POST = 22, + /** + * 指定用户 + */ + USER = 30, + /** + * 发起人自选 + */ + START_USER_SELECT = 35, + /** + * 发起人自己 + */ + START_USER = 36, + /** + * 发起人部门负责人 + */ + START_USER_DEPT_LEADER = 37, + /** + * 发起人连续多级部门的负责人 + */ + START_USER_MULTI_LEVEL_DEPT_LEADER = 38, + /** + * 指定用户组 + */ + USER_GROUP = 40, + /** + * 表单内用户字段 + */ + FORM_USER = 50, + /** + * 表单内部门负责人 + */ + FORM_DEPT_LEADER = 51, + /** + * 流程表达式 + */ + EXPRESSION = 60 +} + +// 多人审批方式类型枚举 ( 用于审批节点 ) +export enum ApproveMethodType { + /** + * 随机挑选一人审批 + */ + RANDOM_SELECT_ONE_APPROVE = 1, + + /** + * 多人会签(按通过比例) + */ + APPROVE_BY_RATIO = 2, + + /** + * 多人或签(通过只需一人,拒绝只需一人) + */ + ANY_APPROVE = 3, + /** + * 多人依次审批 + */ + SEQUENTIAL_APPROVE = 4 +} + +/** + * 审批拒绝结构定义 + */ +export type RejectHandler = { + // 审批拒绝类型 + type: RejectHandlerType + // 退回节点 Id + returnNodeId?: string +} + +/** + * 审批超时结构定义 + */ +export type TimeoutHandler = { + // 是否开启超时处理 + enable: boolean + // 超时执行的动作 + type?: number + // 超时时间设置 + timeDuration?: string + // 执行动作是自动提醒, 最大提醒次数 + maxRemindCount?: number +} + +/** + * 审批人为空的结构定义 + */ +export type AssignEmptyHandler = { + // 审批人为空的处理类型 + type: AssignEmptyHandlerType + // 指定用户的编号数组 + userIds?: number[] +} + +// 审批拒绝类型枚举 +export enum RejectHandlerType { + /** + * 结束流程 + */ + FINISH_PROCESS = 1, + /** + * 驳回到指定节点 + */ + RETURN_USER_TASK = 2 +} +// 用户任务超时处理类型枚举 +export enum TimeoutHandlerType { + /** + * 自动提醒 + */ + REMINDER = 1, + /** + * 自动同意 + */ + APPROVE = 2, + /** + * 自动拒绝 + */ + REJECT = 3 +} +// 用户任务的审批人为空时,处理类型枚举 +export enum AssignEmptyHandlerType { + /** + * 自动通过 + */ + APPROVE = 1, + /** + * 自动拒绝 + */ + REJECT = 2, + /** + * 指定人员审批 + */ + ASSIGN_USER, + /** + * 转交给流程管理员 + */ + ASSIGN_ADMIN = 4 +} +// 用户任务的审批人与发起人相同时,处理类型枚举 +export enum AssignStartUserHandlerType { + /** + * 由发起人对自己审批 + */ + START_USER_AUDIT = 1, + /** + * 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过 + */ + SKIP = 2, + /** + * 转交给部门负责人审批 + */ + ASSIGN_DEPT_LEADER = 3 +} + +// 用户任务的审批类型。 【参考飞书】 +export enum ApproveType { + /** + * 人工审批 + */ + USER = 1, + /** + * 自动通过 + */ + AUTO_APPROVE = 2, + /** + * 自动拒绝 + */ + AUTO_REJECT = 3 +} + +// 时间单位枚举 +export enum TimeUnitType { + /** + * 分钟 + */ + MINUTE = 1, + /** + * 小时 + */ + HOUR = 2, + /** + * 天 + */ + DAY = 3 +} + +// 条件配置类型 ( 用于条件节点配置 ) +export enum ConditionType { + /** + * 条件表达式 + */ + EXPRESSION = 1, + + /** + * 条件规则 + */ + RULE = 2 +} +/** + * 表单权限的枚举 + */ +export enum FieldPermissionType { + /** + * 只读 + */ + READ = '1', + /** + * 编辑 + */ + WRITE = '2', + /** + * 隐藏 + */ + NONE = '3' +} +/** + * 操作按钮权限结构定义 + */ +export type ButtonSetting = { + id: OperationButtonType + displayName: string + enable: boolean +} + +// 操作按钮类型枚举 (用于审批节点) +export enum OperationButtonType { + /** + * 通过 + */ + APPROVE = 1, + /** + * 拒绝 + */ + REJECT = 2, + /** + * 转办 + */ + TRANSFER = 3, + /** + * 委派 + */ + DELEGATE = 4, + /** + * 加签 + */ + ADD_SIGN = 5, + /** + * 退回 + */ + RETURN = 6, + /** + * 抄送 + */ + COPY = 7 +} + +/** + * 条件规则结构定义 + */ +export type ConditionRule = { + type: number + opName: string + opCode: string + leftSide: string + rightSide: string +} + +/** + * 条件组结构定义 + */ +export type ConditionGroup = { + // 条件组的逻辑关系是否为且 + and: boolean + // 条件数组 + conditions: Condition[] +} + +/** + * 条件结构定义 + */ +export type Condition = { + // 条件规则的逻辑关系是否为且 + and: boolean + rules: ConditionRule[] +} + +export const NODE_DEFAULT_TEXT = new Map<number, string>() +NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人') +NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人') +NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件') +NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人') +NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器') + +export const NODE_DEFAULT_NAME = new Map<number, string>() +NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人') +NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人') +NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件') +NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人') +NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器') + +// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序 +export const CANDIDATE_STRATEGY: DictDataVO[] = [ + { label: '指定成员', value: CandidateStrategy.USER }, + { label: '指定角色', value: CandidateStrategy.ROLE }, + { label: '部门成员', value: CandidateStrategy.DEPT_MEMBER }, + { label: '部门负责人', value: CandidateStrategy.DEPT_LEADER }, + { label: '连续多级部门负责人', value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER }, + { label: '发起人自选', value: CandidateStrategy.START_USER_SELECT }, + { label: '发起人本人', value: CandidateStrategy.START_USER }, + { label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER }, + { label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER }, + { label: '用户组', value: CandidateStrategy.USER_GROUP }, + { label: '表单内用户字段', value: CandidateStrategy.FORM_USER }, + { label: '表单内部门负责人', value: CandidateStrategy.FORM_DEPT_LEADER }, + { label: '流程表达式', value: CandidateStrategy.EXPRESSION } +] +// 审批节点 的审批类型 +export const APPROVE_TYPE: DictDataVO[] = [ + { label: '人工审批', value: ApproveType.USER }, + { label: '自动通过', value: ApproveType.AUTO_APPROVE }, + { label: '自动拒绝', value: ApproveType.AUTO_REJECT } +] + +export const APPROVE_METHODS: DictDataVO[] = [ + { label: '按顺序依次审批', value: ApproveMethodType.SEQUENTIAL_APPROVE }, + { label: '会签(可同时审批,至少 % 人必须审批通过)', value: ApproveMethodType.APPROVE_BY_RATIO }, + { label: '或签(可同时审批,有一人通过即可)', value: ApproveMethodType.ANY_APPROVE }, + { label: '随机挑选一人审批', value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE } +] + +export const CONDITION_CONFIG_TYPES: DictDataVO[] = [ + { label: '条件表达式', value: ConditionType.EXPRESSION }, + { label: '条件规则', value: ConditionType.RULE } +] + +// 时间单位类型 +export const TIME_UNIT_TYPES: DictDataVO[] = [ + { label: '分钟', value: TimeUnitType.MINUTE }, + { label: '小时', value: TimeUnitType.HOUR }, + { label: '天', value: TimeUnitType.DAY } +] +// 超时处理执行动作类型 +export const TIMEOUT_HANDLER_TYPES: DictDataVO[] = [ + { label: '自动提醒', value: 1 }, + { label: '自动同意', value: 2 }, + { label: '自动拒绝', value: 3 } +] +export const REJECT_HANDLER_TYPES: DictDataVO[] = [ + { label: '终止流程', value: RejectHandlerType.FINISH_PROCESS }, + { label: '驳回到指定节点', value: RejectHandlerType.RETURN_USER_TASK } + // { label: '结束任务', value: RejectHandlerType.FINISH_TASK } +] +export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataVO[] = [ + { label: '自动通过', value: 1 }, + { label: '自动拒绝', value: 2 }, + { label: '指定成员审批', value: 3 }, + { label: '转交给流程管理员', value: 4 } +] +export const ASSIGN_START_USER_HANDLER_TYPES: DictDataVO[] = [ + { label: '由发起人对自己审批', value: 1 }, + { label: '自动跳过', value: 2 }, + { label: '转交给部门负责人审批', value: 3 } +] + +// 比较运算符 +export const COMPARISON_OPERATORS: DictDataVO = [ + { + value: '==', + label: '等于' + }, + { + value: '!=', + label: '不等于' + }, + { + value: '>', + label: '大于' + }, + { + value: '>=', + label: '大于等于' + }, + { + value: '<', + label: '小于' + }, + { + value: '<=', + label: '小于等于' + } +] +// 审批操作按钮名称 +export const OPERATION_BUTTON_NAME = new Map<number, string>() +OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '通过') +OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝') +OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办') +OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派') +OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签') +OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退回') +OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送') + +// 默认的按钮权限设置 +export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [ + { id: OperationButtonType.APPROVE, displayName: '通过', enable: true }, + { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true }, + { id: OperationButtonType.TRANSFER, displayName: '转办', enable: true }, + { id: OperationButtonType.DELEGATE, displayName: '委派', enable: true }, + { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true }, + { id: OperationButtonType.RETURN, displayName: '退回', enable: true } +] + +// 发起人的按钮权限。暂时定死,不可以编辑 +export const START_USER_BUTTON_SETTING: ButtonSetting[] = [ + { id: OperationButtonType.APPROVE, displayName: '提交', enable: true }, + { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false }, + { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false }, + { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false }, + { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false }, + { id: OperationButtonType.RETURN, displayName: '退回', enable: false } +] + +export const MULTI_LEVEL_DEPT: DictDataVO = [ + { label: '第 1 级部门', value: 1 }, + { label: '第 2 级部门', value: 2 }, + { label: '第 3 级部门', value: 3 }, + { label: '第 4 级部门', value: 4 }, + { label: '第 5 级部门', value: 5 }, + { label: '第 6 级部门', value: 6 }, + { label: '第 7 级部门', value: 7 }, + { label: '第 8 级部门', value: 8 }, + { label: '第 9 级部门', value: 9 }, + { label: '第 10 级部门', value: 10 }, + { label: '第 11 级部门', value: 11 }, + { label: '第 12 级部门', value: 12 }, + { label: '第 13 级部门', value: 13 }, + { label: '第 14 级部门', value: 14 }, + { label: '第 15 级部门', value: 15 } +] + +/** + * 流程实例的变量枚举 + */ +export enum ProcessVariableEnum { + /** + * 发起用户 ID + */ + START_USER_ID = 'PROCESS_START_USER_ID' +} + +/** + * 延迟设置 + */ +export type DelaySetting = { + // 延迟类型 + delayType: number + // 延迟时间表达式 + delayTime: string +} +/** + * 延迟类型 + */ +export enum DelayTypeEnum { + /** + * 固定时长 + */ + FIXED_TIME_DURATION = 1, + /** + * 固定日期时间 + */ + FIXED_DATE_TIME = 2 +} +export const DELAY_TYPE = [ + { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION }, + { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME } +] diff --git a/src/components/SimpleProcessDesignerV2/src/index.ts b/src/components/SimpleProcessDesignerV2/src/index.ts new file mode 100644 index 0000000..88de07f --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/index.ts @@ -0,0 +1,5 @@ +import SimpleProcessDesigner from './SimpleProcessDesigner.vue' +import SimpleProcessViewer from './SimpleProcessViewer.vue' +import '../theme/simple-process-designer.scss' + +export { SimpleProcessDesigner, SimpleProcessViewer} diff --git a/src/components/SimpleProcessDesignerV2/src/node.ts b/src/components/SimpleProcessDesignerV2/src/node.ts new file mode 100644 index 0000000..282e81b --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/node.ts @@ -0,0 +1,510 @@ +import { TaskStatusEnum } from '@/api/bpm/task' +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 { + SimpleFlowNode, + CandidateStrategy, + NodeType, + ApproveMethodType, + RejectHandlerType, + NODE_DEFAULT_NAME, + AssignStartUserHandlerType, + AssignEmptyHandlerType, + FieldPermissionType +} from './consts' +import { parseFormFields } from '@/components/FormCreate/src/utils/index' +export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { + const node = ref<SimpleFlowNode>(props.flowNode) + watch( + () => props.flowNode, + (newValue) => { + node.value = newValue + } + ) + return node +} + +// 解析 formCreate 所有表单字段, 并返回 +const parseFormCreateFields = (formFields?: string[]) => { + const result: Array<Record<string, any>> = [] + if (formFields) { + formFields.forEach((fieldStr: string) => { + parseFormFields(JSON.parse(fieldStr), result) + }) + } + return result +} + +/** + * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点 + */ +export function useFormFieldsPermission(defaultPermission: FieldPermissionType) { + // 字段权限配置. 需要有 field, title, permissioin 属性 + const fieldsPermissionConfig = ref<Array<Record<string, any>>>([]) + + const formType = inject<Ref<number>>('formType') // 表单类型 + + const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段 + + const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => { + nodeFormFields = toRaw(nodeFormFields) + if (!nodeFormFields || nodeFormFields.length === 0) { + fieldsPermissionConfig.value = getDefaultFieldsPermission(unref(formFields)) + } else { + fieldsPermissionConfig.value = mergeFieldsPermission(nodeFormFields, unref(formFields)) + } + } + // 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段) + const mergeFieldsPermission = ( + formFieldsPermisson: Array<Record<string, string>>, + formFields?: string[] + ) => { + let mergedFieldsPermission: Array<Record<string, any>> = [] + if (formFields) { + mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => { + const found = formFieldsPermisson.find( + (fieldPermission) => fieldPermission.field == item.field + ) + return { + field: item.field, + title: item.title, + permission: found ? found.permission : defaultPermission + } + }) + } + return mergedFieldsPermission + } + + // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读 + const getDefaultFieldsPermission = (formFields?: string[]) => { + let defaultFieldsPermission: Array<Record<string, any>> = [] + if (formFields) { + defaultFieldsPermission = parseFormCreateFields(formFields).map((item) => { + return { + field: item.field, + title: item.title, + permission: defaultPermission + } + }) + } + return defaultFieldsPermission + } + + // 获取表单的所有字段,作为下拉框选项 + const formFieldOptions = parseFormCreateFields(unref(formFields)) + + return { + formType, + fieldsPermissionConfig, + formFieldOptions, + getNodeConfigFormFields + } +} +/** + * @description 获取表单的字段 + */ +export function useFormFields() { + const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段 + return parseFormCreateFields(unref(formFields)) +} + +export type UserTaskFormType = { + //candidateParamArray: any[] + candidateStrategy: CandidateStrategy + approveMethod: ApproveMethodType + roleIds?: number[] // 角色 + deptIds?: number[] // 部门 + deptLevel?: number // 部门层级 + userIds?: number[] // 用户 + userGroups?: number[] // 用户组 + postIds?: number[] // 岗位 + expression?: string // 流程表达式 + formUser?: string // 表单内用户字段 + formDept?: string // 表单内部门字段 + approveRatio?: number + rejectHandlerType?: RejectHandlerType + returnNodeId?: string + timeoutHandlerEnable?: boolean + timeoutHandlerType?: number + assignEmptyHandlerType?: AssignEmptyHandlerType + assignEmptyHandlerUserIds?: number[] + assignStartUserHandlerType?: AssignStartUserHandlerType + timeDuration?: number + maxRemindCount?: number + buttonsSetting: any[] +} + +export type CopyTaskFormType = { + // candidateParamArray: any[] + candidateStrategy: CandidateStrategy + roleIds?: number[] // 角色 + deptIds?: number[] // 部门 + deptLevel?: number // 部门层级 + userIds?: number[] // 用户 + userGroups?: number[] // 用户组 + postIds?: number[] // 岗位 + formUser?: string // 表单内用户字段 + formDept?: string // 表单内部门字段 + expression?: string // 流程表达式 +} + +/** + * @description 节点表单数据。 用于审批节点、抄送节点 + */ +export function useNodeForm(nodeType: NodeType) { + const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表 + const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表 + const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表 + const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表 + const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表 + const deptTreeOptions = inject('deptTree') // 部门树 + const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段 + const configForm = ref<UserTaskFormType | CopyTaskFormType>() + if (nodeType === NodeType.USER_TASK_NODE) { + configForm.value = { + candidateStrategy: CandidateStrategy.USER, + approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE, + approveRatio: 100, + rejectHandlerType: RejectHandlerType.FINISH_PROCESS, + assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT, + returnNodeId: '', + timeoutHandlerEnable: false, + timeoutHandlerType: 1, + timeDuration: 6, // 默认 6小时 + maxRemindCount: 1, // 默认 提醒 1次 + buttonsSetting: [] + } + } else { + configForm.value = { + candidateStrategy: CandidateStrategy.USER + } + } + + const getShowText = (): string => { + let showText = '' + // 指定成员 + if (configForm.value?.candidateStrategy === CandidateStrategy.USER) { + if (configForm.value?.userIds!.length > 0) { + const candidateNames: string[] = [] + userOptions?.value.forEach((item) => { + if (configForm.value?.userIds!.includes(item.id)) { + candidateNames.push(item.nickname) + } + }) + showText = `指定成员:${candidateNames.join(',')}` + } + } + // 指定角色 + if (configForm.value?.candidateStrategy === CandidateStrategy.ROLE) { + if (configForm.value.roleIds!.length > 0) { + const candidateNames: string[] = [] + roleOptions?.value.forEach((item) => { + if (configForm.value?.roleIds!.includes(item.id)) { + candidateNames.push(item.name) + } + }) + showText = `指定角色:${candidateNames.join(',')}` + } + } + // 指定部门 + if ( + configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER || + configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER || + configForm.value?.candidateStrategy === CandidateStrategy.MULTI_LEVEL_DEPT_LEADER + ) { + if (configForm.value?.deptIds!.length > 0) { + const candidateNames: string[] = [] + deptOptions?.value.forEach((item) => { + if (configForm.value?.deptIds!.includes(item.id!)) { + candidateNames.push(item.name) + } + }) + if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER) { + showText = `部门成员:${candidateNames.join(',')}` + } else if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER) { + showText = `部门的负责人:${candidateNames.join(',')}` + } else { + showText = `多级部门的负责人:${candidateNames.join(',')}` + } + } + } + + // 指定岗位 + if (configForm.value?.candidateStrategy === CandidateStrategy.POST) { + if (configForm.value.postIds!.length > 0) { + const candidateNames: string[] = [] + postOptions?.value.forEach((item) => { + if (configForm.value?.postIds!.includes(item.id!)) { + candidateNames.push(item.name) + } + }) + showText = `指定岗位: ${candidateNames.join(',')}` + } + } + // 指定用户组 + if (configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP) { + if (configForm.value?.userGroups!.length > 0) { + const candidateNames: string[] = [] + userGroupOptions?.value.forEach((item) => { + if (configForm.value?.userGroups!.includes(item.id)) { + candidateNames.push(item.name) + } + }) + showText = `指定用户组: ${candidateNames.join(',')}` + } + } + + // 表单内用户字段 + if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) { + const formFieldOptions = parseFormCreateFields(unref(formFields)) + const item = formFieldOptions.find((item) => item.field === configForm.value?.formUser) + showText = `表单用户:${item?.title}` + } + + // 表单内部门负责人 + if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER) { + showText = `表单内部门负责人` + } + + // 发起人自选 + if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) { + showText = `发起人自选` + } + // 发起人自己 + if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) { + showText = `发起人自己` + } + // 发起人的部门负责人 + if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_DEPT_LEADER) { + showText = `发起人的部门负责人` + } + // 发起人的部门负责人 + if ( + configForm.value?.candidateStrategy === CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER + ) { + showText = `发起人连续部门负责人` + } + // 流程表达式 + if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) { + showText = `流程表达式:${configForm.value.expression}` + } + return showText + } + + /** + * 处理候选人参数的赋值 + */ + const handleCandidateParam = () => { + let candidateParam: undefined | string = undefined + if (!configForm.value) { + return candidateParam + } + switch (configForm.value.candidateStrategy) { + case CandidateStrategy.USER: + candidateParam = configForm.value.userIds!.join(',') + break + case CandidateStrategy.ROLE: + candidateParam = configForm.value.roleIds!.join(',') + break + case CandidateStrategy.POST: + candidateParam = configForm.value.postIds!.join(',') + break + case CandidateStrategy.USER_GROUP: + candidateParam = configForm.value.userGroups!.join(',') + break + case CandidateStrategy.FORM_USER: + candidateParam = configForm.value.formUser! + break + case CandidateStrategy.EXPRESSION: + candidateParam = configForm.value.expression! + break + case CandidateStrategy.DEPT_MEMBER: + case CandidateStrategy.DEPT_LEADER: + candidateParam = configForm.value.deptIds!.join(',') + break + // 发起人部门负责人 + case CandidateStrategy.START_USER_DEPT_LEADER: + case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: + candidateParam = configForm.value.deptLevel + '' + break + // 指定连续多级部门的负责人 + case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: { + // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级 + const deptIds = configForm.value.deptIds!.join(',') + candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '') + break + } + // 表单内部门的负责人 + case CandidateStrategy.FORM_DEPT_LEADER: { + // 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级 + const deptFieldOnForm = configForm.value.formDept! + candidateParam = deptFieldOnForm.concat('|' + configForm.value.deptLevel + '') + break + } + default: + break + } + return candidateParam + } + /** + * 解析候选人参数 + */ + const parseCandidateParam = ( + candidateStrategy: CandidateStrategy, + candidateParam: string | undefined + ) => { + if (!configForm.value || !candidateParam) { + return + } + switch (candidateStrategy) { + case CandidateStrategy.USER: { + configForm.value.userIds = candidateParam.split(',').map((item) => +item) + break + } + case CandidateStrategy.ROLE: + configForm.value.roleIds = candidateParam.split(',').map((item) => +item) + break + case CandidateStrategy.POST: + configForm.value.postIds = candidateParam.split(',').map((item) => +item) + break + case CandidateStrategy.USER_GROUP: + configForm.value.userGroups = candidateParam.split(',').map((item) => +item) + break + case CandidateStrategy.FORM_USER: + configForm.value.formUser = candidateParam + break + case CandidateStrategy.EXPRESSION: + configForm.value.expression = candidateParam + break + case CandidateStrategy.DEPT_MEMBER: + case CandidateStrategy.DEPT_LEADER: + configForm.value.deptIds = candidateParam.split(',').map((item) => +item) + break + // 发起人部门负责人 + case CandidateStrategy.START_USER_DEPT_LEADER: + case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: + configForm.value.deptLevel = +candidateParam + break + // 指定连续多级部门的负责人 + case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: { + // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级 + const paramArray = candidateParam.split('|') + configForm.value.deptIds = paramArray[0].split(',').map((item) => +item) + configForm.value.deptLevel = +paramArray[1] + break + } + // 表单内的部门负责人 + case CandidateStrategy.FORM_DEPT_LEADER: { + // 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级 + const paramArray = candidateParam.split('|') + configForm.value.formDept = paramArray[0] + configForm.value.deptLevel = +paramArray[1] + break + } + default: + break + } + } + return { + configForm, + roleOptions, + postOptions, + userOptions, + userGroupOptions, + deptTreeOptions, + handleCandidateParam, + parseCandidateParam, + getShowText + } +} + +/** + * @description 抽屉配置 + */ +export function useDrawer() { + // 抽屉配置是否可见 + const settingVisible = ref(false) + // 关闭配置抽屉 + const closeDrawer = () => { + settingVisible.value = false + } + // 打开配置抽屉 + const openDrawer = () => { + settingVisible.value = true + } + return { + settingVisible, + closeDrawer, + openDrawer + } +} + +/** + * @description 节点名称配置 + */ +export function useNodeName(nodeType: NodeType) { + // 节点名称 + const nodeName = ref<string>() + // 节点名称输入框 + const showInput = ref(false) + // 点击节点名称编辑图标 + const clickIcon = () => { + showInput.value = true + } + // 节点名称输入框失去焦点 + const blurEvent = () => { + showInput.value = false + nodeName.value = nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string) + } + return { + nodeName, + showInput, + clickIcon, + blurEvent + } +} + +export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) { + // 显示节点名称输入框 + const showInput = ref(false) + // 节点名称输入框失去焦点 + const blurEvent = () => { + showInput.value = false + node.value.name = node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string) + } + // 点击节点标题进行输入 + const clickTitle = () => { + showInput.value = true + } + return { + showInput, + clickTitle, + blurEvent + } +} + +/** + * @description 根据节点任务状态,获取节点任务状态样式 + */ +export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): string { + if (!taskStatus) { + return '' + } + if (taskStatus === TaskStatusEnum.APPROVE) { + return 'status-pass' + } + if (taskStatus === TaskStatusEnum.RUNNING) { + return 'status-running' + } + if (taskStatus === TaskStatusEnum.REJECT) { + return 'status-reject' + } + if (taskStatus === TaskStatusEnum.CANCEL) { + return 'status-cancel' + } + + return '' +} diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue new file mode 100644 index 0000000..ae93172 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue @@ -0,0 +1,429 @@ +<template> + <el-drawer + :append-to-body="true" + v-model="settingVisible" + :show-close="false" + :size="588" + :before-close="handleClose" + > + <template #header> + <div class="config-header"> + <input + v-if="showInput" + type="text" + class="config-editable-input" + @blur="blurEvent()" + v-mountedFocus + v-model="currentNode.name" + :placeholder="currentNode.name" + /> + <div v-else class="node-name" + >{{ currentNode.name }} + <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" + /></div> + + <div class="divide-line"></div> + </div> + </template> + <div> + <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow" + >未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div + > + <div v-else> + <el-form ref="formRef" :model="currentNode" :rules="formRules" label-position="top"> + <el-form-item label="配置方式" prop="conditionType"> + <el-radio-group v-model="currentNode.conditionType" @change="changeConditionType"> + <el-radio + v-for="(dict, index) in conditionConfigTypes" + :key="index" + :value="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item + v-if="currentNode.conditionType === 1" + label="条件表达式" + prop="conditionExpression" + > + <el-input + type="textarea" + v-model="currentNode.conditionExpression" + clearable + style="width: 100%" + /> + </el-form-item> + <el-form-item v-if="currentNode.conditionType === 2" label="条件规则"> + <div class="condition-group-tool"> + <div class="flex items-center"> + <div class="mr-4">条件组关系</div> + <el-switch + v-model="conditionGroups.and" + inline-prompt + active-text="且" + inactive-text="或" + /> + </div> + </div> + <el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'"> + <el-card + class="condition-group" + style="width: 530px" + v-for="(condition, cIdx) in conditionGroups.conditions" + :key="cIdx" + > + <div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1"> + <Icon + color="#0089ff" + icon="ep:circle-close-filled" + :size="18" + @click="deleteConditionGroup(cIdx)" + /> + </div> + <template #header> + <div class="flex items-center justify-between"> + <div>条件组</div> + <div class="flex"> + <div class="mr-4">规则关系</div> + <el-switch + v-model="condition.and" + inline-prompt + active-text="且" + inactive-text="或" + /> + </div> + </div> + </template> + + <div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx"> + <div class="mr-2"> + <el-select style="width: 160px" v-model="rule.leftSide"> + <el-option + v-for="(item, index) in fieldOptions" + :key="index" + :label="item.title" + :value="item.field" + :disabled="!item.required" + /> + </el-select> + </div> + <div class="mr-2"> + <el-select v-model="rule.opCode" style="width: 100px"> + <el-option + v-for="item in COMPARISON_OPERATORS" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + </div> + <div class="mr-2"> + <el-input v-model="rule.rightSide" style="width: 160px" /> + </div> + <div class="mr-1 flex items-center" v-if="condition.rules.length > 1"> + <Icon + icon="ep:delete" + :size="18" + @click="deleteConditionRule(condition, rIdx)" + /> + </div> + <div class="flex items-center"> + <Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" /> + </div> + </div> + </el-card> + </el-space> + <div title="添加条件组" class="mt-4 cursor-pointer"> + <Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" /> + </div> + </el-form-item> + </el-form> + </div> + </div> + <template #footer> + <el-divider /> + <div> + <el-button type="primary" @click="saveConfig">确 定</el-button> + <el-button @click="closeDrawer">取 消</el-button> + </div> + </template> + </el-drawer> +</template> +<script setup lang="ts"> +import { + SimpleFlowNode, + CONDITION_CONFIG_TYPES, + ConditionType, + COMPARISON_OPERATORS, + ConditionGroup, + Condition, + ConditionRule, + ProcessVariableEnum +} from '../consts' +import { getDefaultConditionNodeName } from '../utils' +import { useFormFields } from '../node' +import { BpmModelFormType } from '@/utils/constants' +const message = useMessage() // 消息弹窗 +defineOptions({ + name: 'ConditionNodeConfig' +}) +const formType = inject<Ref<number>>('formType') // 表单类型 +const conditionConfigTypes = computed(() => { + return CONDITION_CONFIG_TYPES.filter((item) => { + // 业务表单暂时去掉条件规则选项 + if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) { + return false + } else { + return true + } + }) +}) + +const props = defineProps({ + conditionNode: { + type: Object as () => SimpleFlowNode, + required: true + }, + nodeIndex: { + type: Number, + required: true + } +}) +const settingVisible = ref(false) +const open = () => { + if (currentNode.value.conditionType === ConditionType.RULE) { + if (currentNode.value.conditionGroups) { + conditionGroups.value = currentNode.value.conditionGroups + } + } + settingVisible.value = true +} + +watch( + () => props.conditionNode, + (newValue) => { + currentNode.value = newValue + } +) +// 显示名称输入框 +const showInput = ref(false) + +const clickIcon = () => { + showInput.value = true +} +// 输入框失去焦点 +const blurEvent = () => { + showInput.value = false + currentNode.value.name = + currentNode.value.name || + getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow) +} + +const currentNode = ref<SimpleFlowNode>(props.conditionNode) + +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +// 关闭 +const closeDrawer = () => { + settingVisible.value = false +} + +const handleClose = async (done: (cancel?: boolean) => void) => { + const isSuccess = await saveConfig() + if (!isSuccess) { + done(true) // 传入 true 阻止关闭 + } else { + done() + } +} +// 表单校验规则 +const formRules = reactive({ + conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }], + conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +// 保存配置 +const saveConfig = async () => { + if (!currentNode.value.defaultFlow) { + // 校验表单 + if (!formRef) return false + const valid = await formRef.value.validate() + if (!valid) return false + const showText = getShowText() + if (!showText) { + return false + } + currentNode.value.showText = showText + if (currentNode.value.conditionType === ConditionType.EXPRESSION) { + currentNode.value.conditionGroups = undefined + } + if (currentNode.value.conditionType === ConditionType.RULE) { + currentNode.value.conditionExpression = undefined + currentNode.value.conditionGroups = conditionGroups.value + } + } + settingVisible.value = false + return true +} +const getShowText = (): string => { + let showText = '' + if (currentNode.value.conditionType === ConditionType.EXPRESSION) { + if (currentNode.value.conditionExpression) { + showText = `表达式:${currentNode.value.conditionExpression}` + } + } + if (currentNode.value.conditionType === ConditionType.RULE) { + // 条件组是否为与关系 + const groupAnd = conditionGroups.value.and + let warningMesg: undefined | string = undefined + const conditionGroup = conditionGroups.value.conditions.map((item) => { + return ( + '(' + + item.rules + .map((rule) => { + if (rule.leftSide && rule.rightSide) { + return ( + getFieldTitle(rule.leftSide) + ' ' + getOpName(rule.opCode) + ' ' + rule.rightSide + ) + } else { + // 有一条规则不完善。提示错误 + warningMesg = '请完善条件规则' + return '' + } + }) + .join(item.and ? ' 且 ' : ' 或 ') + + ' ) ' + ) + }) + if (warningMesg) { + message.warning(warningMesg) + showText = '' + } else { + showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ') + } + } + return showText +} + +// 改变条件配置方式 +const changeConditionType = () => {} + +const conditionGroups = ref<ConditionGroup>({ + and: true, + conditions: [ + { + and: true, + rules: [ + { + type: 1, + opName: '等于', + opCode: '==', + leftSide: '', + rightSide: '' + } + ] + } + ] +}) +// 添加条件组 +const addConditionGroup = () => { + const condition = { + and: true, + rules: [ + { + type: 1, + opName: '等于', + opCode: '==', + leftSide: '', + rightSide: '' + } + ] + } + conditionGroups.value.conditions.push(condition) +} +// 删除条件组 +const deleteConditionGroup = (idx: number) => { + conditionGroups.value.conditions.splice(idx, 1) +} + +// 添加条件规则 +const addConditionRule = (condition: Condition, idx: number) => { + const rule: ConditionRule = { + type: 1, + opName: '等于', + opCode: '==', + leftSide: '', + rightSide: '' + } + condition.rules.splice(idx + 1, 0, rule) +} + +const deleteConditionRule = (condition: Condition, idx: number) => { + condition.rules.splice(idx, 1) +} +const fieldsInfo = useFormFields() + +/** 条件规则可选择的表单字段 */ +const fieldOptions = computed(() => { + const fieldsCopy = fieldsInfo.slice() + // 固定添加发起人 ID 字段 + fieldsCopy.unshift({ + field: ProcessVariableEnum.START_USER_ID, + title: '发起人', + required: true + }) + return fieldsCopy +}) + +/** 获取字段名称 */ +const getFieldTitle = (field: string) => { + const item = fieldOptions.value.find((item) => item.field === field) + return item?.title +} + +/** 获取操作符名称 */ +const getOpName = (opCode: string): string => { + const opName = COMPARISON_OPERATORS.find((item: any) => item.value === opCode) + return opName?.label +} +</script> + +<style lang="scss" scoped> +.condition-group-tool { + display: flex; + justify-content: space-between; + width: 500px; + margin-bottom: 20px; +} + +.condition-group { + position: relative; + + &:hover { + border-color: #0089ff; + + .condition-group-delete { + opacity: 1; + } + } + + .condition-group-delete { + position: absolute; + top: 0; + left: 0; + display: flex; + cursor: pointer; + opacity: 0; + } +} + +::v-deep(.el-card__header) { + padding: 8px var(--el-card-padding); + border-bottom: 1px solid var(--el-card-border-color); + box-sizing: border-box; +} +</style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue new file mode 100644 index 0000000..f83f185 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue @@ -0,0 +1,374 @@ +<template> + <el-drawer + :append-to-body="true" + v-model="settingVisible" + :show-close="false" + :size="550" + :before-close="saveConfig" + > + <template #header> + <div class="config-header"> + <input + v-if="showInput" + type="text" + class="config-editable-input" + @blur="blurEvent()" + v-mountedFocus + v-model="nodeName" + :placeholder="nodeName" + /> + <div v-else class="node-name"> + {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> + </div> + <div class="divide-line"></div> + </div> + </template> + <el-tabs type="border-card" v-model="activeTabName"> + <el-tab-pane label="抄送人" name="user"> + <div> + <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules"> + <el-form-item label="抄送人设置" prop="candidateStrategy"> + <el-radio-group + v-model="configForm.candidateStrategy" + @change="changeCandidateStrategy" + > + <el-radio + v-for="(dict, index) in copyUserStrategies" + :key="index" + :value="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + + <el-form-item + v-if="configForm.candidateStrategy == CandidateStrategy.ROLE" + label="指定角色" + prop="roleIds" + > + <el-select v-model="configForm.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=" + configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER || + configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER || + configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER + " + label="指定部门" + prop="deptIds" + span="24" + > + <el-tree-select + ref="treeRef" + v-model="configForm.deptIds" + :data="deptTreeOptions" + :props="defaultProps" + empty-text="加载中,请稍后" + multiple + node-key="id" + style="width: 100%" + show-checkbox + /> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy == CandidateStrategy.POST" + label="指定岗位" + prop="postIds" + span="24" + > + <el-select v-model="configForm.postIds" clearable multiple style="width: 100%"> + <el-option + v-for="item in postOptions" + :key="item.id" + :label="item.name" + :value="item.id!" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy == CandidateStrategy.USER" + label="指定用户" + prop="userIds" + span="24" + > + <el-select v-model="configForm.userIds" clearable multiple style="width: 100%"> + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP" + label="指定用户组" + prop="userGroups" + > + <el-select v-model="configForm.userGroups" clearable multiple style="width: 100%"> + <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="configForm.candidateStrategy === CandidateStrategy.FORM_USER" + label="表单内用户字段" + prop="formUser" + > + <el-select v-model="configForm.formUser" clearable style="width: 100%"> + <el-option + v-for="(item, idx) in userFieldOnFormOptions" + :key="idx" + :label="item.title" + :value="item.field" + :disabled ="!item.required" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER" + label="表单内部门字段" + prop="formDept" + > + <el-select v-model="configForm.formDept" clearable style="width: 100%"> + <el-option + v-for="(item, idx) in deptFieldOnFormOptions" + :key="idx" + :label="item.title" + :value="item.field" + :disabled ="!item.required" + /> + </el-select> + </el-form-item> + <el-form-item + v-if=" + configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || + configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || + configForm.candidateStrategy == + CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER || + configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER + " + :label="deptLevelLabel!" + prop="deptLevel" + span="24" + > + <el-select v-model="configForm.deptLevel" clearable> + <el-option + v-for="(item, index) in MULTI_LEVEL_DEPT" + :key="index" + :label="item.label" + :value="item.value" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION" + label="流程表达式" + prop="expression" + > + <el-input + type="textarea" + v-model="configForm.expression" + clearable + style="width: 100%" + /> + </el-form-item> + </el-form> + </div> + </el-tab-pane> + <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> + <div class="field-setting-pane"> + <div class="field-setting-desc">字段权限</div> + <div class="field-permit-title"> + <div class="setting-title-label first-title"> 字段名称 </div> + <div class="other-titles"> + <span class="setting-title-label">只读</span> + <span class="setting-title-label">可编辑</span> + <span class="setting-title-label">隐藏</span> + </div> + </div> + <div + class="field-setting-item" + v-for="(item, index) in fieldsPermissionConfig" + :key="index" + > + <div class="field-setting-item-label"> {{ item.title }} </div> + <el-radio-group class="field-setting-item-group" v-model="item.permission"> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.READ" + size="large" + :label="FieldPermissionType.WRITE" + ><span></span + ></el-radio> + </div> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.WRITE" + size="large" + :label="FieldPermissionType.WRITE" + disabled + ><span></span + ></el-radio> + </div> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.NONE" + size="large" + :label="FieldPermissionType.NONE" + ><span></span + ></el-radio> + </div> + </el-radio-group> + </div> + </div> + </el-tab-pane> + </el-tabs> + <template #footer> + <el-divider /> + <div> + <el-button type="primary" @click="saveConfig">确 定</el-button> + <el-button @click="closeDrawer">取 消</el-button> + </div> + </template> + </el-drawer> +</template> +<script setup lang="ts"> +import { + SimpleFlowNode, + CandidateStrategy, + NodeType, + CANDIDATE_STRATEGY, + FieldPermissionType, + MULTI_LEVEL_DEPT +} from '../consts' +import { + useWatchNode, + useDrawer, + useNodeName, + useFormFieldsPermission, + useNodeForm, + CopyTaskFormType +} from '../node' +import { defaultProps } from '@/utils/tree' +defineOptions({ + name: 'CopyTaskNodeConfig' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +const deptLevelLabel = computed(() => { + let label = '部门负责人来源' + if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { + label = label + '(指定部门向上)' + } else { + label = label + '(发起人部门向上)' + } + return label +}) +// 抽屉配置 +const { settingVisible, closeDrawer, openDrawer } = useDrawer() +// 当前节点 +const currentNode = useWatchNode(props) +// 节点名称 +const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE) +// 激活的 Tab 标签页 +const activeTabName = ref('user') +// 表单字段权限配置 +const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } = + useFormFieldsPermission(FieldPermissionType.READ) +// 表单内用户字段选项, 必须是必填和用户选择器 +const userFieldOnFormOptions = computed(() => { + return formFieldOptions.filter((item) => item.type === 'UserSelect') +}) +// 表单内部门字段选项, 必须是必填和部门选择器 +const deptFieldOnFormOptions = computed(() => { + return formFieldOptions.filter((item) => item.type === 'DeptSelect') +}) +// 抄送人表单配置 +const formRef = ref() // 表单 Ref +// 表单校验规则 +const formRules = reactive({ + candidateStrategy: [{ required: true, message: '抄送人设置不能为空', trigger: 'change' }], + userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }], + roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }], + deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }], + userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }], + postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }], + formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }], + formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }], + expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }] +}) + +const { + configForm: tempConfigForm, + roleOptions, + postOptions, + userOptions, + userGroupOptions, + deptTreeOptions, + getShowText, + handleCandidateParam, + parseCandidateParam +} = useNodeForm(NodeType.COPY_TASK_NODE) +const configForm = tempConfigForm as Ref<CopyTaskFormType> +// 抄送人策略, 去掉发起人自选 和 发起人自己 +const copyUserStrategies = computed(() => { + return CANDIDATE_STRATEGY.filter((item) => item.value !== CandidateStrategy.START_USER) +}) +// 改变抄送人设置策略 +const changeCandidateStrategy = () => { + configForm.value.userIds = [] + configForm.value.deptIds = [] + configForm.value.roleIds = [] + configForm.value.postIds = [] + configForm.value.userGroups = [] + configForm.value.deptLevel = 1 + configForm.value.formUser = '' +} +// 保存配置 +const saveConfig = async () => { + activeTabName.value = 'user' + if (!formRef) return false + const valid = await formRef.value.validate() + if (!valid) return false + const showText = getShowText() + if (!showText) return false + currentNode.value.name = nodeName.value! + currentNode.value.candidateParam = handleCandidateParam() + currentNode.value.candidateStrategy = configForm.value.candidateStrategy + currentNode.value.showText = showText + currentNode.value.fieldsPermission = fieldsPermissionConfig.value + settingVisible.value = false + return true +} +// 显示抄送节点配置, 由父组件传过来 +const showCopyTaskNodeConfig = (node: SimpleFlowNode) => { + nodeName.value = node.name + // 抄送人设置 + configForm.value.candidateStrategy = node.candidateStrategy! + parseCandidateParam(node.candidateStrategy!, node?.candidateParam) + // 表单字段权限 + getNodeConfigFormFields(node.fieldsPermission) +} + +defineExpose({ openDrawer, showCopyTaskNodeConfig }) // 暴露方法给父组件 +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue new file mode 100644 index 0000000..27a351b --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue @@ -0,0 +1,189 @@ +<template> + <el-drawer + :append-to-body="true" + v-model="settingVisible" + :show-close="false" + :size="550" + :before-close="saveConfig" + > + <template #header> + <div class="config-header"> + <input + v-if="showInput" + type="text" + class="config-editable-input" + @blur="blurEvent()" + v-mountedFocus + v-model="nodeName" + :placeholder="nodeName" + /> + <div v-else class="node-name"> + {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> + </div> + <div class="divide-line"></div> + </div> + </template> + <div> + <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules"> + <el-form-item label="延迟时间" prop="delayType"> + <el-radio-group v-model="configForm.delayType"> + <el-radio-button + v-for="item in DELAY_TYPE" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-radio-group> + </el-form-item> + <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION"> + <el-form-item prop="timeDuration"> + <el-input-number + class="mr-2" + :style="{ width: '100px' }" + v-model="configForm.timeDuration" + :min="1" + controls-position="right" + /> + </el-form-item> + <el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }"> + <el-option + v-for="item in TIME_UNIT_TYPES" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + <el-text>后进入下一节点</el-text> + </el-form-item> + <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME" prop="dateTime"> + <el-date-picker + class="mr-2" + v-model="configForm.dateTime" + type="datetime" + placeholder="请选择日期和时间" + value-format="YYYY-MM-DDTHH:mm:ss" + /> + <el-text>后进入下一节点</el-text> + </el-form-item> + </el-form> + </div> + <template #footer> + <el-divider /> + <div> + <el-button type="primary" @click="saveConfig">确 定</el-button> + <el-button @click="closeDrawer">取 消</el-button> + </div> + </template> + </el-drawer> +</template> +<script setup lang="ts"> +import { + SimpleFlowNode, + NodeType, + TIME_UNIT_TYPES, + TimeUnitType, + DelayTypeEnum, + DELAY_TYPE +} from '../consts' +import { useWatchNode, useDrawer, useNodeName } from '../node' +import { convertTimeUnit } from '../utils' +defineOptions({ + name: 'DelayTimerNodeConfig' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 抽屉配置 +const { settingVisible, closeDrawer, openDrawer } = useDrawer() +// 当前节点 +const currentNode = useWatchNode(props) +// 节点名称 +const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.DELAY_TIMER_NODE) +// 抄送人表单配置 +const formRef = ref() // 表单 Ref +// 表单校验规则 +const formRules = reactive({ + delayType: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }], + timeDuration: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }], + dateTime: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }] +}) +// 配置表单数据 +const configForm = ref({ + delayType: DelayTypeEnum.FIXED_TIME_DURATION, + timeDuration: 1, + timeUnit: TimeUnitType.HOUR, + dateTime: '' +}) +// 保存配置 +const saveConfig = async () => { + if (!formRef) return false + const valid = await formRef.value.validate() + if (!valid) return false + const showText = getShowText() + if (!showText) return false + currentNode.value.showText = showText + if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { + currentNode.value.delaySetting = { + delayType: configForm.value.delayType, + delayTime: getIsoTimeDuration() + } + } + if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { + currentNode.value.delaySetting = { + delayType: configForm.value.delayType, + delayTime: configForm.value.dateTime + } + } + settingVisible.value = false + return true +} +const getShowText = (): string => { + let showText = '' + if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { + showText = `延迟${configForm.value.timeDuration}${TIME_UNIT_TYPES.find((item) => item.value === configForm.value.timeUnit).label}` + } + if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { + showText = `延迟至${configForm.value.dateTime.replace('T', ' ')}` + } + return showText +} +const getIsoTimeDuration = () => { + let strTimeDuration = 'PT' + if (configForm.value.timeUnit === TimeUnitType.MINUTE) { + strTimeDuration += configForm.value.timeDuration + 'M' + } + if (configForm.value.timeUnit === TimeUnitType.HOUR) { + strTimeDuration += configForm.value.timeDuration + 'H' + } + if (configForm.value.timeUnit === TimeUnitType.DAY) { + strTimeDuration += configForm.value.timeDuration + 'D' + } + return strTimeDuration +} +// 显示延迟器节点配置, 由父组件传过来 +const showDelayTimerNodeConfig = (node: SimpleFlowNode) => { + nodeName.value = node.name + if (node.delaySetting) { + configForm.value.delayType = node.delaySetting.delayType + // 固定时长 + if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { + const strTimeDuration = node.delaySetting.delayTime + let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1) + let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1) + configForm.value.timeDuration = parseInt(parseTime) + configForm.value.timeUnit = convertTimeUnit(parseTimeUnit) + } + // 固定日期时间 + if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { + configForm.value.dateTime = node.delaySetting.delayTime + } + } +} + +defineExpose({ openDrawer, showDelayTimerNodeConfig }) // 暴露方法给父组件 +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue new file mode 100644 index 0000000..26c8e13 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue @@ -0,0 +1,163 @@ +<template> + <el-drawer + :append-to-body="true" + v-model="settingVisible" + :show-close="false" + :size="550" + :before-close="saveConfig" + > + <template #header> + <div class="config-header"> + <input + v-if="showInput" + type="text" + class="config-editable-input" + @blur="blurEvent()" + v-mountedFocus + v-model="nodeName" + :placeholder="nodeName" + /> + <div v-else class="node-name"> + {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> + </div> + <div class="divide-line"></div> + </div> + </template> + <el-tabs type="border-card" v-model="activeTabName"> + <el-tab-pane label="权限" name="user"> + <el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text> + <el-text v-else-if="startUserIds.length == 1"> + {{ getUserNicknames(startUserIds) }} 可发起流程 + </el-text> + <el-text v-else> + <el-tooltip + class="box-item" + effect="dark" + placement="top" + :content="getUserNicknames(startUserIds)" + > + {{ getUserNicknames(startUserIds.slice(0,2)) }} 等 {{ startUserIds.length }} 人可发起流程 + </el-tooltip> + </el-text> + </el-tab-pane> + <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> + <div class="field-setting-pane"> + <div class="field-setting-desc">字段权限</div> + <div class="field-permit-title"> + <div class="setting-title-label first-title"> 字段名称 </div> + <div class="other-titles"> + <span class="setting-title-label">只读</span> + <span class="setting-title-label">可编辑</span> + <span class="setting-title-label">隐藏</span> + </div> + </div> + <div + class="field-setting-item" + v-for="(item, index) in fieldsPermissionConfig" + :key="index" + > + <div class="field-setting-item-label"> {{ item.title }} </div> + <el-radio-group class="field-setting-item-group" v-model="item.permission"> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.READ" + size="large" + :label="FieldPermissionType.READ" + ><span></span + ></el-radio> + </div> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.WRITE" + size="large" + :label="FieldPermissionType.WRITE" + ><span></span + ></el-radio> + </div> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.NONE" + size="large" + :label="FieldPermissionType.NONE" + ><span></span + ></el-radio> + </div> + </el-radio-group> + </div> + </div> + </el-tab-pane> + </el-tabs> + <template #footer> + <el-divider /> + <div> + <el-button type="primary" @click="saveConfig">确 定</el-button> + <el-button @click="closeDrawer">取 消</el-button> + </div> + </template> + </el-drawer> +</template> +<script setup lang="ts"> +import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts' +import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node' +import * as UserApi from '@/api/system/user' +defineOptions({ + name: 'StartUserNodeConfig' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 可发起流程的用户编号 +const startUserIds = inject<Ref<any[]>>('startUserIds') +// 用户列表 +const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') +// 抽屉配置 +const { settingVisible, closeDrawer, openDrawer } = useDrawer() +// 当前节点 +const currentNode = useWatchNode(props) +// 节点名称 +const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE) +// 激活的 Tab 标签页 +const activeTabName = ref('user') +// 表单字段权限配置 +const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( + FieldPermissionType.WRITE +) +const getUserNicknames = (userIds: number[]): string => { + if (!userIds || userIds.length === 0) { + return '' + } + const nicknames: string[] = [] + userIds.forEach((userId) => { + const found = userOptions?.value.find((item) => item.id === userId) + if (found && found.nickname) { + nicknames.push(found.nickname) + } + }) + return nicknames.join(',') +} +// 保存配置 +const saveConfig = async () => { + activeTabName.value = 'user' + currentNode.value.name = nodeName.value! + currentNode.value.showText = '已设置' + // 设置表单权限 + currentNode.value.fieldsPermission = fieldsPermissionConfig.value + // 设置发起人的按钮权限 + currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING + settingVisible.value = false + return true +} +// 显示发起人节点配置, 由父组件传过来 +const showStartUserNodeConfig = (node: SimpleFlowNode) => { + nodeName.value = node.name + // 表单字段权限 + getNodeConfigFormFields(node.fieldsPermission) +} + +defineExpose({ openDrawer, showStartUserNodeConfig }) // 暴露方法给父组件 +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue new file mode 100644 index 0000000..fb5e780 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue @@ -0,0 +1,913 @@ +<template> + <el-drawer + :append-to-body="true" + v-model="settingVisible" + :show-close="false" + :size="550" + :before-close="saveConfig" + class="justify-start" + > + <template #header> + <div class="config-header"> + <input + v-if="showInput" + type="text" + class="config-editable-input" + @blur="blurEvent()" + v-mountedFocus + v-model="nodeName" + :placeholder="nodeName" + /> + <div v-else class="node-name"> + {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> + </div> + <div class="divide-line"></div> + </div> + </template> + <div class="flex flex-items-center mb-3"> + <span class="font-size-16px mr-3">审批类型 :</span> + <el-radio-group v-model="approveType"> + <el-radio + v-for="(item, index) in APPROVE_TYPE" + :key="index" + :value="item.value" + :label="item.value" + > + {{ item.label }} + </el-radio> + </el-radio-group> + </div> + <el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER"> + <el-tab-pane label="审批人" name="user"> + <div> + <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules"> + <el-form-item label="审批人设置" prop="candidateStrategy"> + <el-radio-group + v-model="configForm.candidateStrategy" + @change="changeCandidateStrategy" + > + <el-radio + v-for="(dict, index) in CANDIDATE_STRATEGY" + :key="index" + :value="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy == CandidateStrategy.ROLE" + label="指定角色" + prop="roleIds" + > + <el-select v-model="configForm.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=" + configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER || + configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER || + configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER + " + label="指定部门" + prop="deptIds" + span="24" + > + <el-tree-select + ref="treeRef" + v-model="configForm.deptIds" + :data="deptTreeOptions" + :props="defaultProps" + empty-text="加载中,请稍后" + multiple + node-key="id" + :check-strictly="true" + style="width: 100%" + show-checkbox + /> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy == CandidateStrategy.POST" + label="指定岗位" + prop="postIds" + span="24" + > + <el-select v-model="configForm.postIds" clearable multiple style="width: 100%"> + <el-option + v-for="item in postOptions" + :key="item.id" + :label="item.name" + :value="item.id!" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy == CandidateStrategy.USER" + label="指定用户" + prop="userIds" + span="24" + > + <el-select v-model="configForm.userIds" clearable multiple style="width: 100%"> + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP" + label="指定用户组" + prop="userGroups" + > + <el-select v-model="configForm.userGroups" clearable multiple style="width: 100%"> + <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="configForm.candidateStrategy === CandidateStrategy.FORM_USER" + label="表单内用户字段" + prop="formUser" + > + <el-select v-model="configForm.formUser" clearable style="width: 100%"> + <el-option + v-for="(item, idx) in userFieldOnFormOptions" + :key="idx" + :label="item.title" + :value="item.field" + :disabled ="!item.required" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER" + label="表单内部门字段" + prop="formDept" + > + <el-select v-model="configForm.formDept" clearable style="width: 100%"> + <el-option + v-for="(item, idx) in deptFieldOnFormOptions" + :key="idx" + :label="item.title" + :value="item.field" + :disabled ="!item.required" + /> + </el-select> + </el-form-item> + <el-form-item + v-if=" + configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || + configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || + configForm.candidateStrategy == + CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER || + configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER + " + :label="deptLevelLabel!" + prop="deptLevel" + span="24" + > + <el-select v-model="configForm.deptLevel" clearable> + <el-option + v-for="(item, index) in MULTI_LEVEL_DEPT" + :key="index" + :label="item.label" + :value="item.value" + /> + </el-select> + </el-form-item> + <!-- TODO @jason:后续要支持选择已经存好的表达式 --> + <el-form-item + v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION" + label="流程表达式" + prop="expression" + > + <el-input + type="textarea" + v-model="configForm.expression" + clearable + style="width: 100%" + /> + </el-form-item> + <el-form-item label="多人审批方式" prop="approveMethod"> + <el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged"> + <div class="flex-col"> + <div + v-for="(item, index) in APPROVE_METHODS" + :key="index" + class="flex items-center" + > + <el-radio :value="item.value" :label="item.value"> + {{ item.label }} + </el-radio> + <el-form-item prop="approveRatio"> + <el-input-number + v-model="configForm.approveRatio" + :min="10" + :max="100" + :step="10" + size="small" + v-if=" + item.value === ApproveMethodType.APPROVE_BY_RATIO && + configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO + " + /> + </el-form-item> + </div> + </div> + </el-radio-group> + </el-form-item> + + <el-divider content-position="left">审批人拒绝时</el-divider> + <el-form-item prop="rejectHandlerType"> + <el-radio-group v-model="configForm.rejectHandlerType"> + <div class="flex-col"> + <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index"> + <el-radio :key="item.value" :value="item.value" :label="item.label" /> + </div> + </div> + </el-radio-group> + </el-form-item> + <el-form-item + v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK" + label="驳回节点" + prop="returnNodeId" + > + <el-select v-model="configForm.returnNodeId" clearable style="width: 100%"> + <el-option + v-for="item in returnTaskList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + + <el-divider content-position="left">审批人超时未处理时</el-divider> + <el-form-item label="启用开关" prop="timeoutHandlerEnable"> + <el-switch + v-model="configForm.timeoutHandlerEnable" + active-text="开启" + inactive-text="关闭" + @change="timeoutHandlerChange" + /> + </el-form-item> + <el-form-item + label="执行动作" + prop="timeoutHandlerType" + v-if="configForm.timeoutHandlerEnable" + > + <el-radio-group + v-model="configForm.timeoutHandlerType" + @change="timeoutHandlerTypeChanged" + > + <el-radio-button + v-for="item in TIMEOUT_HANDLER_TYPES" + :key="item.value" + :value="item.value" + :label="item.label" + /> + </el-radio-group> + </el-form-item> + <el-form-item label="超时时间设置" v-if="configForm.timeoutHandlerEnable"> + <span class="mr-2">当超过</span> + <el-form-item prop="timeDuration"> + <el-input-number + class="mr-2" + :style="{ width: '100px' }" + v-model="configForm.timeDuration" + :min="1" + controls-position="right" + /> + </el-form-item> + <el-select + v-model="timeUnit" + class="mr-2" + :style="{ width: '100px' }" + @change="timeUnitChange" + > + <el-option + v-for="item in TIME_UNIT_TYPES" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + 未处理 + </el-form-item> + <el-form-item + label="最大提醒次数" + prop="maxRemindCount" + v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1" + > + <el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" /> + </el-form-item> + + <el-divider content-position="left">审批人为空时</el-divider> + <el-form-item prop="assignEmptyHandlerType"> + <el-radio-group v-model="configForm.assignEmptyHandlerType"> + <div class="flex-col"> + <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index"> + <el-radio :key="item.value" :value="item.value" :label="item.label" /> + </div> + </div> + </el-radio-group> + </el-form-item> + <el-form-item + v-if="configForm.assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER" + label="指定用户" + prop="assignEmptyHandlerUserIds" + span="24" + > + <el-select + v-model="configForm.assignEmptyHandlerUserIds" + clearable + multiple + style="width: 100%" + > + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + + <el-divider content-position="left">审批人与提交人为同一人时</el-divider> + <el-form-item prop="assignStartUserHandlerType"> + <el-radio-group v-model="configForm.assignStartUserHandlerType"> + <div class="flex-col"> + <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index"> + <el-radio :key="item.value" :value="item.value" :label="item.label" /> + </div> + </div> + </el-radio-group> + </el-form-item> + </el-form> + </div> + </el-tab-pane> + <el-tab-pane label="操作按钮设置" name="buttons"> + <div class="button-setting-pane"> + <div class="button-setting-desc">操作按钮</div> + <div class="button-setting-title"> + <div class="button-title-label">操作按钮</div> + <div class="pl-4 button-title-label">显示名称</div> + <div class="button-title-label">启用</div> + </div> + <div class="button-setting-item" v-for="(item, index) in buttonsSetting" :key="index"> + <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div> + <div class="button-setting-item-label"> + <input + type="text" + class="editable-title-input" + @blur="btnDisplayNameBlurEvent(index)" + v-mountedFocus + v-model="item.displayName" + :placeholder="item.displayName" + v-if="btnDisplayNameEdit[index]" + /> + <el-button v-else text @click="changeBtnDisplayName(index)" + >{{ item.displayName }} <Icon icon="ep:edit" + /></el-button> + </div> + <div class="button-setting-item-label"> + <el-switch v-model="item.enable" /> + </div> + </div> + </div> + </el-tab-pane> + <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> + <div class="field-setting-pane"> + <div class="field-setting-desc">字段权限</div> + <div class="field-permit-title"> + <div class="setting-title-label first-title"> 字段名称 </div> + <div class="other-titles"> + <span class="setting-title-label">只读</span> + <span class="setting-title-label">可编辑</span> + <span class="setting-title-label">隐藏</span> + </div> + </div> + <div + class="field-setting-item" + v-for="(item, index) in fieldsPermissionConfig" + :key="index" + > + <div class="field-setting-item-label"> {{ item.title }} </div> + <el-radio-group class="field-setting-item-group" v-model="item.permission"> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.READ" + size="large" + :label="FieldPermissionType.READ" + ><span></span + ></el-radio> + </div> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.WRITE" + size="large" + :label="FieldPermissionType.WRITE" + ><span></span + ></el-radio> + </div> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.NONE" + size="large" + :label="FieldPermissionType.NONE" + ><span></span + ></el-radio> + </div> + </el-radio-group> + </div> + </div> + </el-tab-pane> + </el-tabs> + <template #footer> + <el-divider /> + <div> + <el-button type="primary" @click="saveConfig">确 定</el-button> + <el-button @click="closeDrawer">取 消</el-button> + </div> + </template> + </el-drawer> +</template> + +<script setup lang="ts"> +import { + SimpleFlowNode, + APPROVE_TYPE, + ApproveType, + APPROVE_METHODS, + CandidateStrategy, + NodeType, + ApproveMethodType, + TimeUnitType, + RejectHandlerType, + TIMEOUT_HANDLER_TYPES, + TIME_UNIT_TYPES, + REJECT_HANDLER_TYPES, + DEFAULT_BUTTON_SETTING, + OPERATION_BUTTON_NAME, + ButtonSetting, + MULTI_LEVEL_DEPT, + CANDIDATE_STRATEGY, + ASSIGN_START_USER_HANDLER_TYPES, + TimeoutHandlerType, + ASSIGN_EMPTY_HANDLER_TYPES, + AssignEmptyHandlerType, + FieldPermissionType, + ProcessVariableEnum +} from '../consts' + +import { + useWatchNode, + useNodeName, + useFormFieldsPermission, + useNodeForm, + UserTaskFormType, + useDrawer +} from '../node' +import { defaultProps } from '@/utils/tree' +import { cloneDeep } from 'lodash-es' +import { convertTimeUnit, getApproveTypeText } from '../utils' +defineOptions({ + name: 'UserTaskNodeConfig' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +const emits = defineEmits<{ + 'find:returnTaskNodes': [nodeList: SimpleFlowNode[]] +}>() +const deptLevelLabel = computed(() => { + let label = '部门负责人来源' + if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { + label = label + '(指定部门向上)' + } else if (configForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) { + label = label + '(表单内部门向上)' + } else { + label = label + '(发起人部门向上)' + } + return label +}) +// 监控节点的变化 +const currentNode = useWatchNode(props) +// 抽屉配置 +const { settingVisible, closeDrawer, openDrawer } = useDrawer() +// 节点名称配置 +const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_TASK_NODE) +// 激活的 Tab 标签页 +const activeTabName = ref('user') +// 表单字段权限设置 +const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } = + useFormFieldsPermission(FieldPermissionType.READ) +// 表单内用户字段选项, 必须是必填和用户选择器 +const userFieldOnFormOptions = computed(() => { + // 固定添加发起人 ID 字段 + formFieldOptions.unshift({ + field: ProcessVariableEnum.START_USER_ID, + title: '发起人', + type: 'UserSelect', + required: true + }) + return formFieldOptions.filter((item) => item.type === 'UserSelect') +}) +// 表单内部门字段选项, 必须是必填和部门选择器 +const deptFieldOnFormOptions = computed(() => { + return formFieldOptions.filter((item) => item.type === 'DeptSelect') +}) +// 操作按钮设置 +const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } = + useButtonsSetting() +const approveType = ref(ApproveType.USER) +// 审批人表单设置 +const formRef = ref() // 表单 Ref +// 表单校验规则 +const formRules = reactive({ + candidateStrategy: [{ required: true, message: '审批人设置不能为空', trigger: 'change' }], + userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }], + roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }], + deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }], + userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }], + formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }], + formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }], + postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }], + expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }], + approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }], + approveRatio: [{ required: true, message: '通过比例不能为空', trigger: 'blur' }], + returnNodeId: [{ required: true, message: '驳回节点不能为空', trigger: 'change' }], + timeoutHandlerEnable: [{ required: true }], + timeoutHandlerType: [{ required: true }], + timeDuration: [{ required: true, message: '超时时间不能为空', trigger: 'blur' }], + maxRemindCount: [{ required: true, message: '提醒次数不能为空', trigger: 'blur' }], + assignEmptyHandlerType: [{ required: true }], + assignEmptyHandlerUserIds: [{ required: true, message: '用户不能为空', trigger: 'change' }], + assignStartUserHandlerType: [{ required: true }] +}) + +const { + configForm: tempConfigForm, + roleOptions, + postOptions, + userOptions, + userGroupOptions, + deptTreeOptions, + handleCandidateParam, + parseCandidateParam, + getShowText +} = useNodeForm(NodeType.USER_TASK_NODE) +const configForm = tempConfigForm as Ref<UserTaskFormType> + +// 改变审批人设置策略 +const changeCandidateStrategy = () => { + configForm.value.userIds = [] + configForm.value.deptIds = [] + configForm.value.roleIds = [] + configForm.value.postIds = [] + configForm.value.userGroups = [] + configForm.value.deptLevel = 1 + configForm.value.formUser = '' + configForm.value.formDept = '' + configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE +} + +// 审批方式改变 +const approveMethodChanged = () => { + configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS + if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) { + configForm.value.approveRatio = 100 + } + formRef.value.clearValidate('approveRatio') +} +// 审批拒绝 可退回的节点 +const returnTaskList = ref<SimpleFlowNode[]>([]) +// 审批人超时未处理设置 +const { + timeoutHandlerChange, + cTimeoutType, + timeoutHandlerTypeChanged, + timeUnit, + timeUnitChange, + isoTimeDuration, + cTimeoutMaxRemindCount +} = useTimeoutHandler() + +// 保存配置 +const saveConfig = async () => { + activeTabName.value = 'user' + // 设置审批节点名称 + currentNode.value.name = nodeName.value! + // 设置审批类型 + currentNode.value.approveType = approveType.value + // 如果不是人工审批。返回 + if (approveType.value !== ApproveType.USER) { + currentNode.value.showText = getApproveTypeText(approveType.value) + settingVisible.value = false + return true + } + + if (!formRef) return false + const valid = await formRef.value.validate() + if (!valid) return false + const showText = getShowText() + if (!showText) return false + + currentNode.value.candidateStrategy = configForm.value.candidateStrategy + // 处理 candidateParam 参数 + currentNode.value.candidateParam = handleCandidateParam() + // 设置审批方式 + currentNode.value.approveMethod = configForm.value.approveMethod + if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) { + currentNode.value.approveRatio = configForm.value.approveRatio + } + // 设置拒绝处理 + currentNode.value.rejectHandler = { + type: configForm.value.rejectHandlerType!, + returnNodeId: configForm.value.returnNodeId + } + // 设置超时处理 + currentNode.value.timeoutHandler = { + enable: configForm.value.timeoutHandlerEnable!, + type: cTimeoutType.value, + timeDuration: isoTimeDuration.value, + maxRemindCount: cTimeoutMaxRemindCount.value + } + // 设置审批人为空时 + currentNode.value.assignEmptyHandler = { + type: configForm.value.assignEmptyHandlerType!, + userIds: + configForm.value.assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER + ? configForm.value.assignEmptyHandlerUserIds + : undefined + } + // 设置审批人与发起人相同时 + currentNode.value.assignStartUserHandlerType = configForm.value.assignStartUserHandlerType + // 设置表单权限 + currentNode.value.fieldsPermission = fieldsPermissionConfig.value + // 设置按钮权限 + currentNode.value.buttonsSetting = buttonsSetting.value + + currentNode.value.showText = showText + settingVisible.value = false + return true +} + +// 显示审批节点配置, 由父组件传过来 +const showUserTaskNodeConfig = (node: SimpleFlowNode) => { + nodeName.value = node.name + // 1 审批类型 + approveType.value = node.approveType ? node.approveType : ApproveType.USER + // 如果审批类型不是人工审批返回 + if (approveType.value !== ApproveType.USER) { + return + } + + //2.1 审批人设置 + configForm.value.candidateStrategy = node.candidateStrategy! + // 解析候选人参数 + parseCandidateParam(node.candidateStrategy!, node?.candidateParam) + // 2.2 设置审批方式 + configForm.value.approveMethod = node.approveMethod! + if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) { + configForm.value.approveRatio = node.approveRatio! + } + // 2.3 设置审批拒绝处理 + configForm.value.rejectHandlerType = node.rejectHandler!.type + configForm.value.returnNodeId = node.rejectHandler?.returnNodeId + const matchNodeList = [] + emits('find:returnTaskNodes', matchNodeList) + returnTaskList.value = matchNodeList + // 2.4 设置审批超时处理 + configForm.value.timeoutHandlerEnable = node.timeoutHandler!.enable + if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) { + const strTimeDuration = node.timeoutHandler.timeDuration + let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1) + let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1) + configForm.value.timeDuration = parseInt(parseTime) + timeUnit.value = convertTimeUnit(parseTimeUnit) + } + configForm.value.timeoutHandlerType = node.timeoutHandler?.type + configForm.value.maxRemindCount = node.timeoutHandler?.maxRemindCount + // 2.5 设置审批人为空时 + configForm.value.assignEmptyHandlerType = node.assignEmptyHandler?.type + configForm.value.assignEmptyHandlerUserIds = node.assignEmptyHandler?.userIds + // 2.6 设置用户任务的审批人与发起人相同时 + configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType + // 3. 操作按钮设置 + buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING + // 4. 表单字段权限配置 + getNodeConfigFormFields(node.fieldsPermission) +} + +defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件 + +/** + * @description 操作按钮设置 + */ +function useButtonsSetting() { + const buttonsSetting = ref<ButtonSetting[]>() + // 操作按钮显示名称可编辑 + const btnDisplayNameEdit = ref<boolean[]>([]) + const changeBtnDisplayName = (index: number) => { + btnDisplayNameEdit.value[index] = true + } + const btnDisplayNameBlurEvent = (index: number) => { + btnDisplayNameEdit.value[index] = false + const buttonItem = buttonsSetting.value![index] + buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)! + } + return { + buttonsSetting, + btnDisplayNameEdit, + changeBtnDisplayName, + btnDisplayNameBlurEvent + } +} + +/** + * @description 审批人超时未处理配置 + */ +function useTimeoutHandler() { + // 时间单位 + const timeUnit = ref(TimeUnitType.HOUR) + + // 超时开关改变 + const timeoutHandlerChange = () => { + if (configForm.value.timeoutHandlerEnable) { + timeUnit.value = 2 + configForm.value.timeDuration = 6 + configForm.value.timeoutHandlerType = 1 + configForm.value.maxRemindCount = 1 + } + } + // 超时执行的动作 + const cTimeoutType = computed(() => { + if (!configForm.value.timeoutHandlerEnable) { + return undefined + } + return configForm.value.timeoutHandlerType + }) + + // 超时处理动作改变 + const timeoutHandlerTypeChanged = () => { + if (configForm.value.timeoutHandlerType === TimeoutHandlerType.REMINDER) { + configForm.value.maxRemindCount = 1 // 超时提醒次数,默认为1 + } + } + + // 时间单位改变 + const timeUnitChange = () => { + // 分钟,默认是 60 分钟 + if (timeUnit.value === TimeUnitType.MINUTE) { + configForm.value.timeDuration = 60 + } + // 小时,默认是 6 个小时 + if (timeUnit.value === TimeUnitType.HOUR) { + configForm.value.timeDuration = 6 + } + // 天, 默认 1天 + if (timeUnit.value === TimeUnitType.DAY) { + configForm.value.timeDuration = 1 + } + } + // 超时时间的 ISO 表示 + const isoTimeDuration = computed(() => { + if (!configForm.value.timeoutHandlerEnable) { + return undefined + } + let strTimeDuration = 'PT' + if (timeUnit.value === TimeUnitType.MINUTE) { + strTimeDuration += configForm.value.timeDuration + 'M' + } + if (timeUnit.value === TimeUnitType.HOUR) { + strTimeDuration += configForm.value.timeDuration + 'H' + } + if (timeUnit.value === TimeUnitType.DAY) { + strTimeDuration += configForm.value.timeDuration + 'D' + } + return strTimeDuration + }) + + // 超时最大提醒次数 + const cTimeoutMaxRemindCount = computed(() => { + if (!configForm.value.timeoutHandlerEnable) { + return undefined + } + if (configForm.value.timeoutHandlerType !== TimeoutHandlerType.REMINDER) { + return undefined + } + return configForm.value.maxRemindCount + }) + + return { + timeoutHandlerChange, + cTimeoutType, + timeoutHandlerTypeChanged, + timeUnit, + timeUnitChange, + isoTimeDuration, + cTimeoutMaxRemindCount + } +} +</script> + +<style lang="scss" scoped> +.button-setting-pane { + display: flex; + flex-direction: column; + font-size: 14px; + + .button-setting-desc { + padding-right: 8px; + margin-bottom: 16px; + font-size: 16px; + font-weight: 700; + } + + .button-setting-title { + display: flex; + justify-content: space-between; + align-items: center; + height: 45px; + padding-left: 12px; + background-color: #f8fafc0a; + border: 1px solid #1f38581a; + + & > :first-child { + width: 100px !important; + text-align: left !important; + } + + & > :last-child { + text-align: center !important; + } + + .button-title-label { + width: 150px; + font-size: 13px; + font-weight: 700; + color: #000; + text-align: left; + } + } + + .button-setting-item { + align-items: center; + display: flex; + justify-content: space-between; + height: 38px; + padding-left: 12px; + border: 1px solid #1f38581a; + border-top: 0; + + & > :first-child { + width: 100px !important; + } + + & > :last-child { + text-align: center !important; + } + + .button-setting-item-label { + width: 150px; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + } + + .editable-title-input { + height: 24px; + max-width: 130px; + margin-left: 4px; + line-height: 24px; + border: 1px solid #d9d9d9; + border-radius: 4px; + transition: all 0.3s; + + &:focus { + border-color: #40a9ff; + outline: 0; + box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); + } + } + } +} +</style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue new file mode 100644 index 0000000..8b97ee5 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue @@ -0,0 +1,97 @@ +<template> + <div class="node-wrapper"> + <div class="node-container"> + <div + class="node-box" + :class="[ + { 'node-config-error': !currentNode.showText }, + `${useTaskStatusClass(currentNode?.activityStatus)}` + ]" + > + <div class="node-title-container"> + <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div> + <input + v-if="!readonly && showInput" + type="text" + class="editable-title-input" + @blur="blurEvent()" + v-mountedFocus + v-model="currentNode.name" + :placeholder="currentNode.name" + /> + <div v-else class="node-title" @click="clickTitle"> + {{ currentNode.name }} + </div> + </div> + <div class="node-content" @click="openNodeConfig"> + <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> + {{ currentNode.showText }} + </div> + <div class="node-text" v-else> + {{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }} + </div> + <Icon v-if="!readonly" icon="ep:arrow-right-bold" /> + </div> + <div v-if="!readonly" class="node-toolbar"> + <div class="toolbar-icon" + ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" + /></div> + </div> + </div> + + <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> + <NodeHandler + v-if="currentNode" + v-model:child-node="currentNode.childNode" + :current-node="currentNode" + /> + </div> + <CopyTaskNodeConfig + v-if="!readonly && currentNode" + ref="nodeSetting" + :flow-node="currentNode" + /> + </div> +</template> +<script setup lang="ts"> +import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' +import NodeHandler from '../NodeHandler.vue' +import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node' +import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue' +defineOptions({ + name: 'CopyTaskNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 定义事件,更新父组件。 +const emits = defineEmits<{ + 'update:flowNode': [node: SimpleFlowNode | undefined] +}>() +// 是否只读 +const readonly = inject<Boolean>('readonly') +// 监控节点的变化 +const currentNode = useWatchNode(props) +// 节点名称编辑 +const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE) + +const nodeSetting = ref() +// 打开节点配置 +const openNodeConfig = () => { + if (readonly) { + return + } + nodeSetting.value.showCopyTaskNodeConfig(currentNode.value) + nodeSetting.value.openDrawer() +} + +// 删除节点。更新当前节点为孩子节点 +const deleteNode = () => { + emits('update:flowNode', currentNode.value.childNode) +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue new file mode 100644 index 0000000..94f9c41 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue @@ -0,0 +1,98 @@ +<template> + <div class="node-wrapper"> + <div class="node-container"> + <div + class="node-box" + :class="[ + { 'node-config-error': !currentNode.showText }, + `${useTaskStatusClass(currentNode?.activityStatus)}` + ]" + > + <div class="node-title-container"> + <!-- TODO @芋艿 需要更换图标 --> + <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div> + <input + v-if="!readonly && showInput" + type="text" + class="editable-title-input" + @blur="blurEvent()" + v-mountedFocus + v-model="currentNode.name" + :placeholder="currentNode.name" + /> + <div v-else class="node-title" @click="clickTitle"> + {{ currentNode.name }} + </div> + </div> + <div class="node-content" @click="openNodeConfig"> + <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> + {{ currentNode.showText }} + </div> + <div class="node-text" v-else> + {{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }} + </div> + <Icon v-if="!readonly" icon="ep:arrow-right-bold" /> + </div> + <div v-if="!readonly" class="node-toolbar"> + <div class="toolbar-icon" + ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" + /></div> + </div> + </div> + + <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> + <NodeHandler + v-if="currentNode" + v-model:child-node="currentNode.childNode" + :current-node="currentNode" + /> + </div> + <DelayTimerNodeConfig + v-if="!readonly && currentNode" + ref="nodeSetting" + :flow-node="currentNode" + /> + </div> +</template> +<script setup lang="ts"> +import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' +import NodeHandler from '../NodeHandler.vue' +import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node' +import DelayTimerNodeConfig from '../nodes-config/DelayTimerNodeConfig.vue' +defineOptions({ + name: 'DelayTimerNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 定义事件,更新父组件。 +const emits = defineEmits<{ + 'update:flowNode': [node: SimpleFlowNode | undefined] +}>() +// 是否只读 +const readonly = inject<Boolean>('readonly') +// 监控节点的变化 +const currentNode = useWatchNode(props) +// 节点名称编辑 +const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.DELAY_TIMER_NODE) + +const nodeSetting = ref() +// 打开节点配置 +const openNodeConfig = () => { + if (readonly) { + return + } + nodeSetting.value.showDelayTimerNodeConfig(currentNode.value) + nodeSetting.value.openDrawer() +} + +// 删除节点。更新当前节点为孩子节点 +const deleteNode = () => { + emits('update:flowNode', currentNode.value.childNode) +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue new file mode 100644 index 0000000..63aa24e --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue @@ -0,0 +1,102 @@ +<template> + <div class="end-node-wrapper"> + <div class="end-node-box cursor-pointer" :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" @click="nodeClick"> + <span class="node-fixed-name" title="结束">结束</span> + </div> + </div> + <el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body> + <el-row> + <el-table + :data="processInstanceInfos" + size="small" + border + header-cell-class-name="table-header-gray" + > + <el-table-column + label="序号" + header-align="center" + align="center" + type="index" + width="50" + /> + <el-table-column + label="发起人" + prop="assigneeUser.nickname" + min-width="100" + align="center" + /> + <el-table-column label="部门" min-width="100" align="center"> + <template #default="scope"> + {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} + </template> + </el-table-column> + <el-table-column + :formatter="dateFormatter" + align="center" + label="开始时间" + prop="createTime" + min-width="140" + /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="结束时间" + prop="endTime" + min-width="140" + /> + <el-table-column align="center" label="审批状态" prop="status" min-width="90"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + + <el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> + <template #default="scope"> + {{ formatPast2(scope.row.durationInMillis) }} + </template> + </el-table-column> + </el-table> + </el-row> + </el-dialog> +</template> +<script setup lang="ts"> +import { SimpleFlowNode } from '../consts' +import { useWatchNode, useTaskStatusClass } from '../node' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' +import { DICT_TYPE } from '@/utils/dict' +defineOptions({ + name: 'EndEventNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + default: () => null + } +}) +// 监控节点变化 +const currentNode = useWatchNode(props) +// 是否只读 +const readonly = inject<Boolean>('readonly') +const processInstance = inject<Ref<any>>('processInstance') +// 审批信息的弹窗显示,用于只读模式 +const dialogVisible = ref(false) // 弹窗可见性 +const processInstanceInfos = ref<any[]>([]) // 流程的审批信息 + +const nodeClick = () => { + if (readonly) { + if(processInstance && processInstance.value){ + processInstanceInfos.value = [ + { + assigneeUser: processInstance.value.startUser, + createTime: processInstance.value.startTime, + endTime: processInstance.value.endTime, + status: processInstance.value.status, + durationInMillis: processInstance.value.durationInMillis + } + ] + dialogVisible.value = true + } + } +} +</script> +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue new file mode 100644 index 0000000..adeae77 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue @@ -0,0 +1,229 @@ +<template> + <div class="branch-node-wrapper"> + <div class="branch-node-container"> + <div + v-if="readonly" + class="branch-node-readonly" + :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" + > + <span class="iconfont icon-exclusive icon-size condition"></span> + </div> + <el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain + >添加条件</el-button + > + + <div + class="branch-node-item" + v-for="(item, index) in currentNode.conditionNodes" + :key="index" + > + <template v-if="index == 0"> + <div class="branch-line-first-top"> </div> + <div class="branch-line-first-bottom"></div> + </template> + <template v-if="index + 1 == currentNode.conditionNodes?.length"> + <div class="branch-line-last-top"></div> + <div class="branch-line-last-bottom"></div> + </template> + <div class="node-wrapper"> + <div class="node-container"> + <div + class="node-box" + :class="[ + { 'node-config-error': !item.showText }, + `${useTaskStatusClass(item.activityStatus)}` + ]" + > + <div class="branch-node-title-container"> + <div v-if="!readonly && showInputs[index]"> + <input + type="text" + class="input-max-width editable-title-input" + @blur="blurEvent(index)" + v-mountedFocus + v-model="item.name" + /> + </div> + <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div> + <div class="branch-priority"> 优先级{{ index + 1 }} </div> + </div> + <div class="branch-node-content" @click="conditionNodeConfig(item.id)"> + <div class="branch-node-text" :title="item.showText" v-if="item.showText"> + {{ item.showText }} + </div> + <div class="branch-node-text" v-else> + {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }} + </div> + </div> + <div + class="node-toolbar" + v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length" + > + <div class="toolbar-icon"> + <Icon + color="#0089ff" + icon="ep:circle-close-filled" + :size="18" + @click="deleteCondition(index)" + /> + </div> + </div> + <div + class="branch-node-move move-node-left" + v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" + @click="moveNode(index, -1)" + > + <Icon icon="ep:arrow-left" /> + </div> + + <div + class="branch-node-move move-node-right" + v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2" + @click="moveNode(index, 1)" + > + <Icon icon="ep:arrow-right" /> + </div> + </div> + <NodeHandler v-model:child-node="item.childNode" :current-node="item" /> + </div> + </div> + <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" /> + <!-- 递归显示子节点 --> + <ProcessNodeTree + v-if="item && item.childNode" + :parent-node="item" + v-model:flow-node="item.childNode" + @find:recursive-find-parent-node="recursiveFindParentNode" + /> + </div> + </div> + <NodeHandler + v-if="currentNode" + v-model:child-node="currentNode.childNode" + :current-node="currentNode" + /> + </div> +</template> + +<script setup lang="ts"> +import NodeHandler from '../NodeHandler.vue' +import ProcessNodeTree from '../ProcessNodeTree.vue' +import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' +import { getDefaultConditionNodeName } from '../utils' +import { useTaskStatusClass } from '../node' +import { generateUUID } from '@/utils' +import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue' +const { proxy } = getCurrentInstance() as any +defineOptions({ + name: 'ExclusiveNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 定义事件,更新父组件 +const emits = defineEmits<{ + 'update:modelValue': [node: SimpleFlowNode | undefined] + 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number] + 'find:recursiveFindParentNode': [ + nodeList: SimpleFlowNode[], + curentNode: SimpleFlowNode, + nodeType: number + ] +}>() +// 是否只读 +const readonly = inject<Boolean>('readonly') +const currentNode = ref<SimpleFlowNode>(props.flowNode) +watch( + () => props.flowNode, + (newValue) => { + currentNode.value = newValue + } +) + +const showInputs = ref<boolean[]>([]) +// 失去焦点 +const blurEvent = (index: number) => { + showInputs.value[index] = false + const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode + conditionNode.name = + conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow) +} + +// 点击条件名称 +const clickEvent = (index: number) => { + showInputs.value[index] = true +} + +const conditionNodeConfig = (nodeId: string) => { + if (readonly) { + return + } + const conditionNode = proxy.$refs[nodeId][0] + conditionNode.open() +} + +// 新增条件 +const addCondition = () => { + const conditionNodes = currentNode.value.conditionNodes + if (conditionNodes) { + const len = conditionNodes.length + let lastIndex = len - 1 + const conditionData: SimpleFlowNode = { + id: 'Flow_' + generateUUID(), + name: '条件' + len, + showText: '', + type: NodeType.CONDITION_NODE, + childNode: undefined, + conditionNodes: [], + conditionType: 1, + defaultFlow: false + } + conditionNodes.splice(lastIndex, 0, conditionData) + } +} + +// 删除条件 +const deleteCondition = (index: number) => { + const conditionNodes = currentNode.value.conditionNodes + if (conditionNodes) { + conditionNodes.splice(index, 1) + if (conditionNodes.length == 1) { + const childNode = currentNode.value.childNode + // 更新此节点为后续孩子节点 + emits('update:modelValue', childNode) + } + } +} + +// 移动节点 +const moveNode = (index: number, to: number) => { + // -1 :向左 1: 向右 + if (currentNode.value.conditionNodes) { + currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice( + index + to, + 1, + currentNode.value.conditionNodes[index] + )[0] + } +} +// 递归从父节点中查询匹配的节点 +const recursiveFindParentNode = ( + nodeList: SimpleFlowNode[], + node: SimpleFlowNode, + nodeType: number +) => { + if (!node || node.type === NodeType.START_USER_NODE) { + return + } + if (node.type === nodeType) { + nodeList.push(node) + } + // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.EXCLUSIVE_NODE) 继续查找 + emits('find:parentNode', nodeList, nodeType) +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue new file mode 100644 index 0000000..f1445d8 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue @@ -0,0 +1,233 @@ +<template> + <div class="branch-node-wrapper"> + <div class="branch-node-container"> + <div + v-if="readonly" + class="branch-node-readonly" + :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" + > + <span class="iconfont icon-inclusive icon-size inclusive"></span> + </div> + <el-button v-else class="branch-node-add" color="#345da2" @click="addCondition" plain + >添加条件</el-button + > + <div + class="branch-node-item" + v-for="(item, index) in currentNode.conditionNodes" + :key="index" + > + <template v-if="index == 0"> + <div class="branch-line-first-top"> </div> + <div class="branch-line-first-bottom"></div> + </template> + <template v-if="index + 1 == currentNode.conditionNodes?.length"> + <div class="branch-line-last-top"></div> + <div class="branch-line-last-bottom"></div> + </template> + <div class="node-wrapper"> + <div class="node-container"> + <div + class="node-box" + :class="[ + { 'node-config-error': !item.showText }, + `${useTaskStatusClass(item.activityStatus)}` + ]" + > + <div class="branch-node-title-container"> + <div v-if="showInputs[index]"> + <input + type="text" + class="editable-title-input" + @blur="blurEvent(index)" + v-mountedFocus + v-model="item.name" + /> + </div> + <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div> + </div> + <div class="branch-node-content" @click="conditionNodeConfig(item.id)"> + <div class="branch-node-text" :title="item.showText" v-if="item.showText"> + {{ item.showText }} + </div> + <div class="branch-node-text" v-else> + {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }} + </div> + </div> + <div + class="node-toolbar" + v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length" + > + <div class="toolbar-icon"> + <Icon + color="#0089ff" + icon="ep:circle-close-filled" + :size="18" + @click="deleteCondition(index)" + /> + </div> + </div> + <div + class="branch-node-move move-node-left" + v-if="!readonly && index != 0 && index + 1 !== currentNode.conditionNodes?.length" + @click="moveNode(index, -1)" + > + <Icon icon="ep:arrow-left" /> + </div> + + <div + class="branch-node-move move-node-right" + v-if=" + !readonly && + currentNode.conditionNodes && + index < currentNode.conditionNodes.length - 2 + " + @click="moveNode(index, 1)" + > + <Icon icon="ep:arrow-right" /> + </div> + </div> + <NodeHandler v-model:child-node="item.childNode" :current-node="item" /> + </div> + </div> + <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" /> + <!-- 递归显示子节点 --> + <ProcessNodeTree + v-if="item && item.childNode" + :parent-node="item" + v-model:flow-node="item.childNode" + @find:recursive-find-parent-node="recursiveFindParentNode" + /> + </div> + </div> + <NodeHandler + v-if="currentNode" + v-model:child-node="currentNode.childNode" + :current-node="currentNode" + /> + </div> +</template> + +<script setup lang="ts"> +import NodeHandler from '../NodeHandler.vue' +import ProcessNodeTree from '../ProcessNodeTree.vue' +import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' +import { useTaskStatusClass } from '../node' +import { getDefaultInclusiveConditionNodeName } from '../utils' +import { generateUUID } from '@/utils' +import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue' +const { proxy } = getCurrentInstance() as any +defineOptions({ + name: 'InclusiveNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 定义事件,更新父组件 +const emits = defineEmits<{ + 'update:modelValue': [node: SimpleFlowNode | undefined] + 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number] + 'find:recursiveFindParentNode': [ + nodeList: SimpleFlowNode[], + curentNode: SimpleFlowNode, + nodeType: number + ] +}>() +// 是否只读 +const readonly = inject<Boolean>('readonly') + +const currentNode = ref<SimpleFlowNode>(props.flowNode) + +watch( + () => props.flowNode, + (newValue) => { + currentNode.value = newValue + } +) + +const showInputs = ref<boolean[]>([]) +// 失去焦点 +const blurEvent = (index: number) => { + showInputs.value[index] = false + const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode + conditionNode.name = + conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.defaultFlow) +} + +// 点击条件名称 +const clickEvent = (index: number) => { + showInputs.value[index] = true +} + +const conditionNodeConfig = (nodeId: string) => { + if (readonly) { + return + } + const conditionNode = proxy.$refs[nodeId][0] + conditionNode.open() +} + +// 新增条件 +const addCondition = () => { + const conditionNodes = currentNode.value.conditionNodes + if (conditionNodes) { + const len = conditionNodes.length + let lastIndex = len - 1 + const conditionData: SimpleFlowNode = { + id: 'Flow_' + generateUUID(), + name: '包容条件' + len, + showText: '', + type: NodeType.CONDITION_NODE, + childNode: undefined, + conditionNodes: [], + conditionType: 1, + defaultFlow: false + } + conditionNodes.splice(lastIndex, 0, conditionData) + } +} + +// 删除条件 +const deleteCondition = (index: number) => { + const conditionNodes = currentNode.value.conditionNodes + if (conditionNodes) { + conditionNodes.splice(index, 1) + if (conditionNodes.length == 1) { + const childNode = currentNode.value.childNode + // 更新此节点为后续孩子节点 + emits('update:modelValue', childNode) + } + } +} + +// 移动节点 +const moveNode = (index: number, to: number) => { + // -1 :向左 1: 向右 + if (currentNode.value.conditionNodes) { + currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice( + index + to, + 1, + currentNode.value.conditionNodes[index] + )[0] + } +} +// 递归从父节点中查询匹配的节点 +const recursiveFindParentNode = ( + nodeList: SimpleFlowNode[], + node: SimpleFlowNode, + nodeType: number +) => { + if (!node || node.type === NodeType.START_USER_NODE) { + return + } + if (node.type === nodeType) { + nodeList.push(node) + } + // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.INCLUSIVE_BRANCH_NODE) 继续查找 + emits('find:parentNode', nodeList, nodeType) +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue new file mode 100644 index 0000000..7aa6793 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue @@ -0,0 +1,184 @@ +<template> + <div class="branch-node-wrapper"> + <div class="branch-node-container"> + <div + v-if="readonly" + class="branch-node-readonly" + :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" + > + <span class="iconfont icon-parallel icon-size parallel"></span> + </div> + <el-button v-else class="branch-node-add" color="#626aef" @click="addCondition" plain + >添加分支</el-button + > + <div + class="branch-node-item" + v-for="(item, index) in currentNode.conditionNodes" + :key="index" + > + <template v-if="index == 0"> + <div class="branch-line-first-top"></div> + <div class="branch-line-first-bottom"></div> + </template> + <template v-if="index + 1 == currentNode.conditionNodes?.length"> + <div class="branch-line-last-top"></div> + <div class="branch-line-last-bottom"></div> + </template> + <div class="node-wrapper"> + <div class="node-container"> + <div class="node-box" :class="`${useTaskStatusClass(item.activityStatus)}`"> + <div class="branch-node-title-container"> + <div v-if="showInputs[index]"> + <input + type="text" + class="input-max-width editable-title-input" + @blur="blurEvent(index)" + v-mountedFocus + v-model="item.name" + /> + </div> + <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div> + <div class="branch-priority">无优先级</div> + </div> + <div class="branch-node-content" @click="conditionNodeConfig(item.id)"> + <div class="branch-node-text" :title="item.showText" v-if="item.showText"> + {{ item.showText }} + </div> + <div class="branch-node-text" v-else> + {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }} + </div> + </div> + <div v-if="!readonly" class="node-toolbar"> + <div class="toolbar-icon"> + <Icon + color="#0089ff" + icon="ep:circle-close-filled" + :size="18" + @click="deleteCondition(index)" + /> + </div> + </div> + </div> + <NodeHandler v-model:child-node="item.childNode" :current-node="item" /> + </div> + </div> + <!-- 递归显示子节点 --> + <ProcessNodeTree + v-if="item && item.childNode" + :parent-node="item" + v-model:flow-node="item.childNode" + @find:recursive-find-parent-node="recursiveFindParentNode" + /> + </div> + </div> + <NodeHandler + v-if="currentNode" + v-model:child-node="currentNode.childNode" + :current-node="currentNode" + /> + </div> +</template> + +<script setup lang="ts"> +import NodeHandler from '../NodeHandler.vue' +import ProcessNodeTree from '../ProcessNodeTree.vue' +import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' +import { useTaskStatusClass } from '../node' +import { generateUUID } from '@/utils' +const { proxy } = getCurrentInstance() as any +defineOptions({ + name: 'ParallelNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 定义事件,更新父组件 +const emits = defineEmits<{ + 'update:modelValue': [node: SimpleFlowNode | undefined] + 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number] + 'find:recursiveFindParentNode': [ + nodeList: SimpleFlowNode[], + curentNode: SimpleFlowNode, + nodeType: number + ] +}>() + +const currentNode = ref<SimpleFlowNode>(props.flowNode) +// 是否只读 +const readonly = inject<Boolean>('readonly') + +watch( + () => props.flowNode, + (newValue) => { + currentNode.value = newValue + } +) + +const showInputs = ref<boolean[]>([]) +// 失去焦点 +const blurEvent = (index: number) => { + showInputs.value[index] = false + const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode + conditionNode.name = conditionNode.name || `并行${index + 1}` +} + +// 点击条件名称 +const clickEvent = (index: number) => { + showInputs.value[index] = true +} + +const conditionNodeConfig = (nodeId: string) => { + const conditionNode = proxy.$refs[nodeId][0] + conditionNode.open() +} + +// 新增条件 +const addCondition = () => { + const conditionNodes = currentNode.value.conditionNodes + if (conditionNodes) { + const len = conditionNodes.length + let lastIndex = len - 1 + const conditionData: SimpleFlowNode = { + id: 'Flow_' + generateUUID(), + name: '并行' + len, + showText: '无需配置条件同时执行', + type: NodeType.CONDITION_NODE, + childNode: undefined, + conditionNodes: [] + } + conditionNodes.splice(lastIndex, 0, conditionData) + } +} + +// 删除条件 +const deleteCondition = (index: number) => { + const conditionNodes = currentNode.value.conditionNodes + if (conditionNodes) { + conditionNodes.splice(index, 1) + if (conditionNodes.length == 1) { + const childNode = currentNode.value.childNode + // 更新此节点为后续孩子节点 + emits('update:modelValue', childNode) + } + } +} + +// 递归从父节点中查询匹配的节点 +const recursiveFindParentNode = ( + nodeList: SimpleFlowNode[], + node: SimpleFlowNode, + nodeType: number +) => { + if (!node || node.type === NodeType.START_USER_NODE) { + return + } + if (node.type === nodeType) { + nodeList.push(node) + } + // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找 + emits('find:parentNode', nodeList, nodeType) +} +</script> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue new file mode 100644 index 0000000..89a57d0 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue @@ -0,0 +1,154 @@ +<template> + <div class="node-wrapper"> + <div class="node-container"> + <div + class="node-box" + :class="[ + { 'node-config-error': !currentNode.showText }, + `${useTaskStatusClass(currentNode?.activityStatus)}` + ]" + > + <div class="node-title-container"> + <div class="node-title-icon start-user" + ><span class="iconfont icon-start-user"></span + ></div> + <input + v-if="showInput" + type="text" + class="editable-title-input" + @blur="blurEvent()" + v-mountedFocus + v-model="currentNode.name" + :placeholder="currentNode.name" + /> + <div v-else class="node-title" @click="clickTitle"> + {{ currentNode.name }} + </div> + </div> + <div class="node-content" @click="nodeClick"> + <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> + {{ currentNode.showText }} + </div> + <div class="node-text" v-else> + {{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }} + </div> + <Icon icon="ep:arrow-right-bold" v-if="!readonly" /> + </div> + </div> + <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> + <NodeHandler + v-if="currentNode" + v-model:child-node="currentNode.childNode" + :current-node="currentNode" + /> + </div> + </div> + <StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" /> + <!-- 审批记录 --> + <el-dialog + :title="dialogTitle || '审批记录'" + v-model="dialogVisible" + width="1000px" + append-to-body + > + <el-row> + <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray"> + <el-table-column + label="序号" + header-align="center" + align="center" + type="index" + width="50" + /> + <el-table-column label="审批人" min-width="100" align="center"> + <template #default="scope"> + {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} + </template> + </el-table-column> + + <el-table-column label="部门" min-width="100" align="center"> + <template #default="scope"> + {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} + </template> + </el-table-column> + <el-table-column + :formatter="dateFormatter" + align="center" + label="开始时间" + prop="createTime" + min-width="140" + /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="结束时间" + prop="endTime" + min-width="140" + /> + <el-table-column align="center" label="审批状态" prop="status" min-width="90"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column align="center" label="审批建议" prop="reason" min-width="120" /> + <el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> + <template #default="scope"> + {{ formatPast2(scope.row.durationInMillis) }} + </template> + </el-table-column> + </el-table> + </el-row> + </el-dialog> +</template> +<script setup lang="ts"> +import NodeHandler from '../NodeHandler.vue' +import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node' +import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts' +import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' +import { DICT_TYPE } from '@/utils/dict' +defineOptions({ + name: 'StartEventNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + default: () => null + } +}) +const readonly = inject<Boolean>('readonly') // 是否只读 +const tasks = inject<Ref<any[]>>('tasks') +// 定义事件,更新父组件。 +const emits = defineEmits<{ + 'update:modelValue': [node: SimpleFlowNode | undefined] +}>() +// 监控节点变化 +const currentNode = useWatchNode(props) +// 节点名称编辑 +const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE) + +const nodeSetting = ref() +// +const nodeClick = () => { + if (readonly) { + // 只读模式,弹窗显示任务信息 + if (tasks && tasks.value) { + dialogTitle.value = currentNode.value.name + selectTasks.value = tasks.value.filter( + (item: any) => item?.taskDefinitionKey === currentNode.value.id + ) + dialogVisible.value = true + } + } else { + // 编辑模式,打开节点配置、把当前节点传递给配置组件 + nodeSetting.value.showStartUserNodeConfig(currentNode.value) + nodeSetting.value.openDrawer() + } +} + +// 任务的弹窗显示,用于只读模式 +const dialogVisible = ref(false) // 弹窗可见性 +const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题 +const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组 +</script> +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue new file mode 100644 index 0000000..761a674 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue @@ -0,0 +1,174 @@ +<template> + <div class="node-wrapper"> + <div class="node-container"> + <div + class="node-box" + :class="[ + { 'node-config-error': !currentNode.showText }, + `${useTaskStatusClass(currentNode?.activityStatus)}` + ]" + > + <div class="node-title-container"> + <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div> + <input + v-if="!readonly && showInput" + type="text" + class="editable-title-input" + @blur="blurEvent()" + v-mountedFocus + v-model="currentNode.name" + :placeholder="currentNode.name" + /> + <div v-else class="node-title" @click="clickTitle"> + {{ currentNode.name }} + </div> + </div> + <div class="node-content" @click="nodeClick"> + <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> + {{ currentNode.showText }} + </div> + <div class="node-text" v-else> + {{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }} + </div> + <Icon icon="ep:arrow-right-bold" v-if="!readonly" /> + </div> + <div v-if="!readonly" class="node-toolbar"> + <div class="toolbar-icon" + ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" + /></div> + </div> + </div> + <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> + <NodeHandler + v-if="currentNode" + v-model:child-node="currentNode.childNode" + :current-node="currentNode" + /> + </div> + </div> + <UserTaskNodeConfig + v-if="currentNode" + ref="nodeSetting" + :flow-node="currentNode" + @find:return-task-nodes="findReturnTaskNodes" + /> + <!-- 审批记录 --> + <el-dialog + :title="dialogTitle || '审批记录'" + v-model="dialogVisible" + width="1000px" + append-to-body + > + <el-row> + <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray"> + <el-table-column + label="序号" + header-align="center" + align="center" + type="index" + width="50" + /> + <el-table-column label="审批人" min-width="100" align="center"> + <template #default="scope"> + {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} + </template> + </el-table-column> + + <el-table-column label="部门" min-width="100" align="center"> + <template #default="scope"> + {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} + </template> + </el-table-column> + <el-table-column + :formatter="dateFormatter" + align="center" + label="开始时间" + prop="createTime" + min-width="140" + /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="结束时间" + prop="endTime" + min-width="140" + /> + <el-table-column align="center" label="审批状态" prop="status" min-width="90"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column align="center" label="审批建议" prop="reason" min-width="120" /> + <el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> + <template #default="scope"> + {{ formatPast2(scope.row.durationInMillis) }} + </template> + </el-table-column> + </el-table> + </el-row> + </el-dialog> +</template> +<script setup lang="ts"> +import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' +import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node' +import NodeHandler from '../NodeHandler.vue' +import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' +import { DICT_TYPE } from '@/utils/dict' +defineOptions({ + name: 'UserTaskNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +const emits = defineEmits<{ + 'update:flowNode': [node: SimpleFlowNode | undefined] + 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType] +}>() + +// 是否只读 +const readonly = inject<Boolean>('readonly') +const tasks = inject<Ref<any[]>>('tasks') +// 监控节点变化 +const currentNode = useWatchNode(props) +// 节点名称编辑 +const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE) +const nodeSetting = ref() + +const nodeClick = () => { + if (readonly) { + if (tasks && tasks.value) { + dialogTitle.value = currentNode.value.name + // 只读模式,弹窗显示任务信息 + selectTasks.value = tasks.value.filter( + (item: any) => item?.taskDefinitionKey === currentNode.value.id + ) + dialogVisible.value = true + } + } else { + // 编辑模式,打开节点配置、把当前节点传递给配置组件 + nodeSetting.value.showUserTaskNodeConfig(currentNode.value) + nodeSetting.value.openDrawer() + } +} + +const deleteNode = () => { + emits('update:flowNode', currentNode.value.childNode) +} +// 查找可以驳回用户节点 +const findReturnTaskNodes = ( + matchNodeList: SimpleFlowNode[] // 匹配的节点 +) => { + // 从父节点查找 + emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE) +} + +// 任务的弹窗显示,用于只读模式 +const dialogVisible = ref(false) // 弹窗可见性 +const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题 +const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组 +</script> +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/utils.ts b/src/components/SimpleProcessDesignerV2/src/utils.ts new file mode 100644 index 0000000..8e715b4 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/utils.ts @@ -0,0 +1,41 @@ +import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts' + +// 获取条件节点默认的名称 +export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => { + if (defaultFlow) { + return '其它情况' + } + return '条件' + (index + 1) +} + +// 获取包容分支条件节点默认的名称 +export const getDefaultInclusiveConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => { + if (defaultFlow) { + return '其它情况' + } + return '包容条件' + (index + 1) +} + +export const convertTimeUnit = (strTimeUnit: string) => { + if (strTimeUnit === 'M') { + return TimeUnitType.MINUTE + } + if (strTimeUnit === 'H') { + return TimeUnitType.HOUR + } + if (strTimeUnit === 'D') { + return TimeUnitType.DAY + } + return TimeUnitType.HOUR +} + +export const getApproveTypeText = (approveType: ApproveType): string => { + let approveTypeText = '' + APPROVE_TYPE.forEach((item) => { + if (item.value === approveType) { + approveTypeText = item.label + return + } + }) + return approveTypeText +} diff --git a/src/components/SimpleProcessDesignerV2/theme/iconfont.ttf b/src/components/SimpleProcessDesignerV2/theme/iconfont.ttf new file mode 100644 index 0000000..bb85b35 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/theme/iconfont.ttf Binary files differ diff --git a/src/components/SimpleProcessDesignerV2/theme/iconfont.woff b/src/components/SimpleProcessDesignerV2/theme/iconfont.woff new file mode 100644 index 0000000..94befbd --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/theme/iconfont.woff Binary files differ diff --git a/src/components/SimpleProcessDesignerV2/theme/iconfont.woff2 b/src/components/SimpleProcessDesignerV2/theme/iconfont.woff2 new file mode 100644 index 0000000..e8f95c8 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/theme/iconfont.woff2 Binary files differ diff --git a/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss b/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss new file mode 100644 index 0000000..8cf2681 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss @@ -0,0 +1,755 @@ +// 配置节点头部 +.config-header { + display: flex; + flex-direction: column; + + .node-name { + display: flex; + height: 24px; + line-height: 24px; + font-size: 16px; + cursor: pointer; + align-items: center; + } + + .divide-line { + width: 100%; + height: 1px; + margin-top: 16px; + background: #eee; + } + + .config-editable-input { + height: 24px; + max-width: 510px; + font-size: 16px; + line-height: 24px; + border: 1px solid #d9d9d9; + border-radius: 4px; + transition: all 0.3s; + + &:focus { + border-color: #40a9ff; + outline: 0; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } +} + +// 表单字段权限 +.field-setting-pane { + display: flex; + flex-direction: column; + font-size: 14px; + + .field-setting-desc { + padding-right: 8px; + margin-bottom: 16px; + font-size: 16px; + font-weight: 700; + } + + .field-permit-title { + display: flex; + justify-content: space-between; + align-items: center; + height: 45px; + padding-left: 12px; + line-height: 45px; + background-color: #f8fafc0a; + border: 1px solid #1f38581a; + + .first-title { + text-align: left !important; + } + + .other-titles { + display: flex; + justify-content: space-between; + } + + .setting-title-label { + display: inline-block; + width: 110px; + padding: 5px 0; + font-size: 13px; + font-weight: 700; + color: #000; + text-align: center; + } + } + + .field-setting-item { + align-items: center; + display: flex; + justify-content: space-between; + height: 38px; + padding-left: 12px; + border: 1px solid #1f38581a; + border-top: 0; + + .field-setting-item-label { + display: inline-block; + width: 110px; + min-height: 16px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: text; + } + + .field-setting-item-group { + display: flex; + justify-content: space-between; + + .item-radio-wrap { + display: inline-block; + width: 110px; + text-align: center; + } + } + } +} + +// 节点连线气泡卡片样式 +.handler-item-wrapper { + display: flex; + cursor: pointer; + + .handler-item { + display: flex; + flex-direction: column; + align-items: center; + } + + .handler-item-icon { + width: 60px; + height: 60px; + background: #fff; + border: 1px solid #e2e2e2; + border-radius: 50%; + user-select: none; + text-align: center; + + &:hover { + background: #e2e2e2; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1); + } + + .icon-size { + font-size: 25px; + line-height: 60px; + } + } + + .approve { + color: #ff943e; + } + .copy { + color: #3296fa; + } + + .condition { + color: #67c23a; + } + + .parallel { + color: #626aef; + } + + .inclusive { + color: #345da2; + } + + .handler-item-text { + margin-top: 4px; + width: 80px; + text-align: center; + font-size: 13px; + } +} +// Simple 流程模型样式 +.simple-process-model-container { + height: 100%; + padding-top: 32px; + background-color: #fafafa; + overflow-x: auto; + width: 100%; + + .simple-process-model { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + transform-origin: 50% 0 0; + min-width: fit-content; + transform: scale(1); + transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat; + // 节点容器 定义节点宽度 + .node-container { + width: 200px; + } + // 节点 + .node-box { + position: relative; + display: flex; + min-height: 70px; + padding: 5px 10px 8px; + cursor: pointer; + background-color: #fff; + flex-direction: column; + border: 2px solid transparent; + border-radius: 8px; + box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%); + transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1); + + &.status-pass { + background-color: #a9da90; + border-color: #67c23a; + } + + &.status-pass:hover { + border-color: #67c23a; + } + + &.status-running { + background-color: #e7f0fe; + border-color: #5a9cf8; + } + + &.status-running:hover { + border-color: #5a9cf8; + } + + &.status-reject { + background-color: #f6e5e5; + border-color: #e47470; + } + + &.status-reject:hover { + border-color: #e47470; + } + + &:hover { + border-color: #0089ff; + + .node-toolbar { + opacity: 1; + } + + .branch-node-move { + display: flex; + } + } + + // 普通节点标题 + .node-title-container { + display: flex; + padding: 4px; + cursor: pointer; + border-radius: 4px 4px 0 0; + align-items: center; + + .node-title-icon { + display: flex; + align-items: center; + + &.user-task { + color: #ff943e; + } + + &.copy-task { + color: #3296fa; + } + + &.start-user { + color: #676565; + } + } + + .node-title { + margin-left: 4px; + overflow: hidden; + font-size: 14px; + font-weight: 600; + line-height: 18px; + color: #1f1f1f; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover { + border-bottom: 1px dashed #f60; + } + } + } + + // 条件节点标题 + .branch-node-title-container { + display: flex; + padding: 4px 0; + cursor: pointer; + border-radius: 4px 4px 0 0; + align-items: center; + justify-content: space-between; + + .input-max-width { + max-width: 115px !important; + } + + .branch-title { + overflow: hidden; + font-size: 13px; + font-weight: 600; + color: #f60; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover { + border-bottom: 1px dashed #000; + } + } + + .branch-priority { + min-width: 50px; + font-size: 12px; + } + } + + .node-content { + display: flex; + min-height: 32px; + padding: 4px 8px; + margin-top: 4px; + line-height: 32px; + justify-content: space-between; + align-items: center; + color: #111f2c; + background: rgb(0 0 0 / 3%); + border-radius: 4px; + + .node-text { + display: -webkit-box; + overflow: hidden; + font-size: 14px; + line-height: 24px; + text-overflow: ellipsis; + word-break: break-all; + -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */ + -webkit-box-orient: vertical; + } + } + + //条件节点内容 + .branch-node-content { + display: flex; + min-height: 32px; + padding: 4px 0; + margin-top: 4px; + line-height: 32px; + align-items: center; + color: #111f2c; + border-radius: 4px; + + .branch-node-text { + overflow: hidden; + font-size: 12px; + line-height: 24px; + text-overflow: ellipsis; + word-break: break-all; + -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */ + -webkit-box-orient: vertical; + } + } + + // 节点操作 :删除 + .node-toolbar { + position: absolute; + top: -20px; + right: 0; + display: flex; + opacity: 0; + + .toolbar-icon { + text-align: center; + vertical-align: middle; + } + } + + // 条件节点左右移动 + .branch-node-move { + position: absolute; + display: none; + width: 10px; + height: 100%; + cursor: pointer; + align-items: center; + justify-content: center; + } + + .move-node-left { + top: 0; + left: -2px; + background: rgb(126 134 142 / 8%); + border-bottom-left-radius: 8px; + border-top-left-radius: 8px; + } + + .move-node-right { + top: 0; + right: -2px; + background: rgb(126 134 142 / 8%); + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + } + + .node-config-error { + border-color: #ff5219 !important; + } + // 普通节点包装 + .node-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + // 节点连线处理 + .node-handler-wrapper { + position: relative; + display: flex; + height: 70px; + align-items: center; + user-select: none; + justify-content: center; + flex-direction: column; + + &::before { + position: absolute; + top: 0; + z-index: 0; + width: 2px; + height: 100%; + margin: auto; + background-color: #dedede; + content: ''; + } + + .node-handler { + .add-icon { + position: relative; + top: -5px; + display: flex; + width: 25px; + height: 25px; + color: #fff; + cursor: pointer; + background-color: #0089ff; + border-radius: 50%; + align-items: center; + justify-content: center; + + &:hover { + transform: scale(1.1); + } + } + } + + .node-handler-arrow { + position: absolute; + bottom: 0; + left: 50%; + display: flex; + transform: translateX(-50%); + } + } + + // 条件节点包装 + .branch-node-wrapper { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-top: 16px; + + .branch-node-container { + position: relative; + display: flex; + min-width: fit-content; + + &::before { + position: absolute; + left: 50%; + width: 4px; + height: 100%; + background-color: #fafafa; + content: ''; + transform: translate(-50%); + } + + .branch-node-add { + position: absolute; + top: -18px; + left: 50%; + z-index: 1; + height: 36px; + padding: 0 10px; + font-size: 12px; + line-height: 36px; + border: 2px solid #dedede; + border-radius: 18px; + transform: translateX(-50%); + transform-origin: center center; + } + + .branch-node-readonly { + position: absolute; + top: -18px; + left: 50%; + z-index: 1; + display: flex; + width: 36px; + height: 36px; + background-color: #fff; + border: 2px solid #dedede; + border-radius: 50%; + transform: translateX(-50%); + align-items: center; + justify-content: center; + transform-origin: center center; + + &.status-pass { + background-color: #e9f4e2; + border-color: #6bb63c; + } + + &.status-pass:hover { + border-color: #6bb63c; + } + + .icon-size { + font-size: 22px; + &.condition { + color: #67c23a; + } + &.parallel { + color: #626aef; + } + &.inclusive { + color: #345da2; + } + } + } + + .branch-node-item { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + min-width: 280px; + padding: 40px 40px 0; + background: transparent; + border-top: 2px solid #dedede; + border-bottom: 2px solid #dedede; + flex-shrink: 0; + + &::before { + position: absolute; + width: 2px; + height: 100%; + margin: auto; + inset: 0; + background-color: #dedede; + content: ''; + } + } + // 覆盖条件节点第一个节点左上角的线 + .branch-line-first-top { + position: absolute; + top: -5px; + left: -1px; + width: 50%; + height: 7px; + background-color: #fafafa; + content: ''; + } + // 覆盖条件节点第一个节点左下角的线 + .branch-line-first-bottom { + position: absolute; + bottom: -5px; + left: -1px; + width: 50%; + height: 7px; + background-color: #fafafa; + content: ''; + } + // 覆盖条件节点最后一个节点右上角的线 + .branch-line-last-top { + position: absolute; + top: -5px; + right: -1px; + width: 50%; + height: 7px; + background-color: #fafafa; + content: ''; + } + // 覆盖条件节点最后一个节点右下角的线 + .branch-line-last-bottom { + position: absolute; + right: -1px; + bottom: -5px; + width: 50%; + height: 7px; + background-color: #fafafa; + content: ''; + } + } + } + + .node-fixed-name { + display: inline-block; + width: auto; + padding: 0 4px; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + } + // 开始节点包装 + .start-node-wrapper { + position: relative; + margin-top: 16px; + + .start-node-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .start-node-box { + display: flex; + justify-content: center; + align-items: center; + width: 90px; + height: 36px; + padding: 3px 4px; + color: #212121; + cursor: pointer; + background: #fafafa; + border-radius: 30px; + box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%); + box-sizing: border-box; + } + } + } + + // 结束节点包装 + .end-node-wrapper { + margin-bottom: 16px; + + .end-node-box { + display: flex; + width: 80px; + height: 36px; + color: #212121; + border: 2px solid #fafafa; + border-radius: 30px; + box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%); + box-sizing: border-box; + justify-content: center; + align-items: center; + + &.status-pass { + background-color: #a9da90; + border-color: #6bb63c; + } + + &.status-pass:hover { + border-color: #6bb63c; + } + + &.status-reject { + background-color: #f6e5e5; + border-color: #e47470; + } + + &.status-reject:hover { + border-color: #e47470; + } + + &.status-cancel { + background-color: #eaeaeb; + border-color: #919398; + } + + &.status-cancel:hover { + border-color: #919398; + } + } + } + + // 可编辑的 title 输入框 + .editable-title-input { + height: 20px; + max-width: 145px; + margin-left: 4px; + font-size: 12px; + line-height: 20px; + border: 1px solid #d9d9d9; + border-radius: 4px; + transition: all 0.3s; + + &:focus { + border-color: #40a9ff; + outline: 0; + box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); + } + } + } +} + +// iconfont 样式 +@font-face { + font-family: 'iconfont'; /* Project id 4495938 */ + src: + url('iconfont.woff2?t=1724339470412') format('woff2'), + url('iconfont.woff?t=1724339470412') format('woff'), + url('iconfont.ttf?t=1724339470412') format('truetype'); +} + +.iconfont { + font-family: 'iconfont' !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-start-user:before { + content: '\e679'; +} + +.icon-inclusive:before { + content: '\e602'; +} + +.icon-copy:before { + content: '\e7eb'; +} + +.icon-handle:before { + content: '\e61c'; +} + +.icon-exclusive:before { + content: '\e717'; +} + +.icon-approve:before { + content: '\e715'; +} + +.icon-parallel:before { + content: '\e688'; +} diff --git a/src/components/UploadFile/src/UploadFile.vue b/src/components/UploadFile/src/UploadFile.vue index 3beb377..9d0a904 100644 --- a/src/components/UploadFile/src/UploadFile.vue +++ b/src/components/UploadFile/src/UploadFile.vue @@ -1,5 +1,5 @@ <template> - <div class="upload-file"> + <div v-if="!disabled" class="upload-file"> <el-upload ref="uploadRef" v-model:file-list="fileList" @@ -20,11 +20,11 @@ class="upload-file-uploader" name="file" > - <el-button v-if="!disabled" type="primary"> + <el-button type="primary"> <Icon icon="ep:upload-filled" /> 选取文件 </el-button> - <template v-if="isShowTip && !disabled" #tip> + <template v-if="isShowTip" #tip> <div style="font-size: 8px"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </div> @@ -32,7 +32,6 @@ 格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件 </div> </template> - <!-- TODO @puhui999:1)表单展示的时候,位置会偏掉,已发微信;2)disable 的时候,应该把【删除】按钮也隐藏掉? --> <template #file="row"> <div class="flex items-center"> <span>{{ row.file.name }}</span> @@ -53,6 +52,18 @@ </div> </template> </el-upload> + </div> + + <!-- 上传操作禁用时 --> + <div v-if="disabled" class="upload-file"> + <div v-for="(file, index) in fileList" :key="index" class="flex items-center file-list-item"> + <span>{{ file.name }}</span> + <div class="ml-10px"> + <el-link :href="file.url" :underline="false" download target="_blank" type="primary"> + 下载 + </el-link> + </div> + </div> </div> </template> <script lang="ts" setup> @@ -211,4 +222,9 @@ :deep(.ele-upload-list__item-content-action .el-link) { margin-right: 10px; } + +.file-list-item { + border: 1px dashed var(--el-border-color-darker); + border-radius: 8px; +} </style> diff --git a/src/components/UploadFile/src/UploadImgs.vue b/src/components/UploadFile/src/UploadImgs.vue index 85da64c..59857a9 100644 --- a/src/components/UploadFile/src/UploadImgs.vue +++ b/src/components/UploadFile/src/UploadImgs.vue @@ -25,7 +25,7 @@ <template #file="{ file }"> <img :src="file.url" class="upload-image" /> <div class="upload-handle" @click.stop> - <div class="handle-icon" @click="handlePictureCardPreview(file)"> + <div class="handle-icon" @click="imagePreview(file.url!)"> <Icon icon="ep:zoom-in" /> <span>查看</span> </div> @@ -39,16 +39,12 @@ <div class="el-upload__tip"> <slot name="tip"></slot> </div> - <el-image-viewer - v-if="imgViewVisible" - :url-list="[viewImageUrl]" - @close="imgViewVisible = false" - /> </div> </template> <script lang="ts" setup> import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus' import { ElNotification } from 'element-plus' +import { createImageViewer } from '@/components/ImageViewer' import { propTypes } from '@/utils/propTypes' import { useUpload } from '@/components/UploadFile/src/useUpload' @@ -56,6 +52,13 @@ defineOptions({ name: 'UploadImgs' }) const message = useMessage() // 消息弹窗 +// 查看图片 +const imagePreview = (imgUrl: string) => { + createImageViewer({ + zIndex: 9999999, + urlList: [imgUrl] + }) +} type FileTypes = | 'image/apng' @@ -177,14 +180,6 @@ message: `当前最多只能上传 ${props.limit} 张图片,请移除后上传!`, type: 'warning' }) -} - -// 图片预览 -const viewImageUrl = ref('') -const imgViewVisible = ref(false) -const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => { - viewImageUrl.value = uploadFile.url! - imgViewVisible.value = true } </script> diff --git a/src/components/UploadFile/src/useUpload.ts b/src/components/UploadFile/src/useUpload.ts index c0465a2..c846acb 100644 --- a/src/components/UploadFile/src/useUpload.ts +++ b/src/components/UploadFile/src/useUpload.ts @@ -3,9 +3,16 @@ import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload' import axios from 'axios' +/** + * 获得上传 URL + */ +export const getUploadUrl = (): string => { + return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload' +} + export const useUpload = () => { // 后端上传地址 - const uploadUrl = import.meta.env.VITE_UPLOAD_URL + const uploadUrl = getUploadUrl() // 是否使用前端直连上传 const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE // 重写ElUpload上传方法 @@ -17,16 +24,18 @@ // 1.2 获取文件预签名地址 const presignedInfo = await FileApi.getFilePresignedUrl(fileName) // 1.3 上传文件(不能使用 ElUpload 的 ajaxUpload 方法的原因:其使用的是 FormData 上传,Minio 不支持) - return axios.put(presignedInfo.uploadUrl, options.file, { - headers: { - 'Content-Type': options.file.type, - } - }).then(() => { - // 1.4. 记录文件信息到后端(异步) - createFile(presignedInfo, fileName, options.file) - // 通知成功,数据格式保持与后端上传的返回结果一致 - return { data: presignedInfo.url } - }) + return axios + .put(presignedInfo.uploadUrl, options.file, { + headers: { + 'Content-Type': options.file.type + } + }) + .then(() => { + // 1.4. 记录文件信息到后端(异步) + createFile(presignedInfo, fileName, options.file) + // 通知成功,数据格式保持与后端上传的返回结果一致 + return { data: presignedInfo.url } + }) } else { // 模式二:后端上传 // 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子 @@ -92,6 +101,4 @@ enum UPLOAD_TYPE { // 客户端直接上传(只支持S3服务) CLIENT = 'client', - // 客户端发送到后端上传 - SERVER = 'server' } diff --git a/src/components/UserSelectForm/index.vue b/src/components/UserSelectForm/index.vue new file mode 100644 index 0000000..5ed99f8 --- /dev/null +++ b/src/components/UserSelectForm/index.vue @@ -0,0 +1,171 @@ +<template> + <Dialog v-model="dialogVisible" title="人员选择" width="800"> + <el-row class="gap2" v-loading="formLoading"> + <el-col :span="6"> + <ContentWrap class="h-1/1"> + <el-tree + ref="treeRef" + :data="deptTree" + :expand-on-click-node="false" + :props="defaultProps" + default-expand-all + highlight-current + node-key="id" + @node-click="handleNodeClick" + /> + </ContentWrap> + </el-col> + <el-col :span="17"> + <el-transfer + v-model="selectedUserIdList" + :titles="['未选', '已选']" + filterable + filter-placeholder="搜索成员" + :data="transferUserList" + :props="{ label: 'nickname', key: 'id' }" + /> + </el-col> + </el-row> + <template #footer> + <el-button + :disabled="formLoading || !selectedUserIdList?.length" + type="primary" + @click="submitForm" + > + 确 定 + </el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script lang="ts" setup> +import { defaultProps, handleTree } from '@/utils/tree' +import * as DeptApi from '@/api/system/dept' +import * as UserApi from '@/api/system/user' + +defineOptions({ name: 'UserSelectForm' }) +const emit = defineEmits<{ + confirm: [id: any, userList: any[]] +}>() +const { t } = useI18n() // 国际 +const message = useMessage() // 消息弹窗 +const deptTree = ref<Tree[]>([]) // 部门树形结构化 +const deptList = ref<any[]>([]) // 保存扁平化的部门列表数据 +const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表 +const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表 +const selectedUserIdList: any = ref([]) // 选中的用户列表 +const dialogVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中 +const activityId = ref() + +/** 计算属性:合并已选择的用户和当前部门过滤后的用户 */ +const transferUserList = computed(() => { + // 1.1 获取所有已选择的用户 + const selectedUsers = userList.value.filter((user: any) => + selectedUserIdList.value.includes(user.id) + ) + + // 1.2 获取当前部门过滤后的未选择用户 + const filteredUnselectedUsers = filteredUserList.value.filter( + (user: any) => !selectedUserIdList.value.includes(user.id) + ) + + // 2. 合并并去重 + return [...selectedUsers, ...filteredUnselectedUsers] +}) + +/** 打开弹窗 */ +const open = async (id: number, selectedList?: any[]) => { + activityId.value = id + resetForm() + + // 加载部门、用户列表 + const deptData = await DeptApi.getSimpleDeptList() + deptList.value = deptData // 保存扁平结构的部门数据 + deptTree.value = handleTree(deptData) // 转换成树形结构 + userList.value = await UserApi.getSimpleUserList() + + // 初始状态下,过滤列表等于所有用户列表 + filteredUserList.value = [...userList.value] + selectedUserIdList.value = selectedList?.map((item: any) => item.id) || [] + dialogVisible.value = true +} + +/** 获取指定部门及其所有子部门的ID列表 */ +const getChildDeptIds = (deptId: number, deptList: any[]): number[] => { + const ids = [deptId] + const children = deptList.filter((dept) => dept.parentId === deptId) + children.forEach((child) => { + ids.push(...getChildDeptIds(child.id, deptList)) + }) + return ids +} + +/** 获取部门过滤后的用户列表 */ +const filterUserList = async (deptId?: number) => { + formLoading.value = true + try { + if (!deptId) { + // 如果没有选择部门,显示所有用户 + filteredUserList.value = [...userList.value] + return + } + + // 直接使用已保存的部门列表数据进行过滤 + const deptIds = getChildDeptIds(deptId, deptList.value) + + // 过滤出这些部门下的用户 + filteredUserList.value = userList.value.filter((user) => deptIds.includes(user.deptId)) + } finally { + formLoading.value = false + } +} + +/** 提交选择 */ +const submitForm = async () => { + try { + message.success(t('common.updateSuccess')) + dialogVisible.value = false + // 从所有用户列表中筛选出已选择的用户 + const emitUserList = userList.value.filter((user: any) => + selectedUserIdList.value.includes(user.id) + ) + // 发送操作成功的事件 + emit('confirm', activityId.value, emitUserList) + } finally { + } +} + +/** 重置表单 */ +const resetForm = () => { + deptTree.value = [] + deptList.value = [] + userList.value = [] + filteredUserList.value = [] + selectedUserIdList.value = [] +} + +/** 处理部门被点击 */ +const handleNodeClick = (row: { [key: string]: any }) => { + filterUserList(row.id) +} + +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +</script> + +<style lang="scss" scoped> +:deep() { + .el-transfer { + display: flex; + } + .el-transfer__buttons { + display: flex !important; + flex-direction: column-reverse; + justify-content: center; + gap: 20px; + .el-transfer__button:nth-child(2) { + margin: 0; + } + } +} +</style> diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue index d8f921a..9d2fa5b 100644 --- a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue +++ b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue @@ -160,13 +160,6 @@ <XButton preIcon="ep:refresh" @click="processRestart()" /> </el-tooltip> </ElButtonGroup> - <XButton - preIcon="ep:plus" - title="保存模型" - @click="processSave" - :type="props.headerButtonType" - :disabled="simulationStatus" - /> </template> <!-- 用于打开本地文件--> <input @@ -314,6 +307,28 @@ ['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(value) !== -1 } }) + +// 监听value变化,重新加载流程图 +watch( + () => props.value, + (newValue) => { + if (newValue && bpmnModeler) { + createNewDiagram(newValue) + } + }, + { immediate: true } +) + +// 监听processId和processName变化 +watch( + [() => props.processId, () => props.processName], + ([newId, newName]) => { + if (newId && newName && !props.value) { + createNewDiagram(null) + } + }, + { immediate: true } +) provide('configGlobal', props) let bpmnModeler: any = null @@ -592,16 +607,6 @@ defaultZoom.value = newZoom bpmnModeler.get('canvas').zoom(defaultZoom.value) } -// const processZoomTo = (newZoom = 1) => { -// if (newZoom < 0.2) { -// throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2') -// } -// if (newZoom > 4) { -// throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4') -// } -// defaultZoom = newZoom -// bpmnModeler.get('canvas').zoom(newZoom) -// } const processReZoom = () => { defaultZoom.value = 1 bpmnModeler.get('canvas').zoom('fit-viewport', 'auto') @@ -640,63 +645,19 @@ } const previewProcessJson = () => { bpmnModeler.saveXML({ format: true }).then(({ xml }) => { - // console.log(xml, 'xml') - - // const rootNode = parseXmlString(xml) - // console.log(rootNode, 'rootNoderootNode') const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml)) - // console.log(rootNodes, 'rootNodesrootNodesrootNodes') - // console.log(rootNodes.parent.toJsObject(), 'rootNodes.toJSON()') - // console.log(JSON.stringify(rootNodes.parent.toJsObject()), 'rootNodes.toJSON()') - // console.log(JSON.stringify(rootNodes.parent.toJSON()), 'rootNodes.toJSON()') - - // const parser = new xml2js.XMLParser() - // let jObj = parser.parse(xml) - // console.log(jObj, 'jObjjObjjObjjObjjObj') - // const builder = new xml2js.XMLBuilder(xml) - // const xmlContent = builder - // console.log(xmlContent, 'xmlContent') - // console.log(xml2js, 'convertconvertconvert') previewResult.value = rootNodes.parent?.toJSON() as unknown as string - // previewResult.value = jObj - // previewResult.value = convert.xml2json(xml, {explicitArray : false},{ spaces: 2 }) previewType.value = 'json' previewModelVisible.value = true }) } -/* ------------------------------------------------ 工业互联网平台 methods ------------------------------------------------------ */ -const processSave = async () => { - // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler') - const { err, xml } = await bpmnModeler.saveXML() - // console.log(err, 'errerrerrerrerr') - // console.log(xml, 'xmlxmlxmlxmlxml') - // 读取异常时抛出异常 - if (err) { - // this.$modal.msgError('保存模型失败,请重试!') - alert('保存模型失败,请重试!') - return - } - // 触发 save 事件 - emit('save', xml) -} -/** 高亮显示 */ -// const highlightedCode = (previewType, previewResult) => { -// console.log(previewType, 'previewType, previewResult') -// console.log(previewResult, 'previewType, previewResult') -// console.log(hljs.highlight, 'hljs.highlight') -// const result = hljs.highlight(previewType, previewResult.value || '', true) -// return result.value || ' ' -// } -onBeforeMount(() => { - console.log(props, 'propspropspropsprops') -}) + +/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */ onMounted(() => { initBpmnModeler() createNewDiagram(props.value) }) onBeforeUnmount(() => { - // this.$once('hook:beforeDestroy', () => { - // }) if (bpmnModeler) bpmnModeler.destroy() emit('destroy', bpmnModeler) bpmnModeler = null diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue index 485b979..34a54c8 100644 --- a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue +++ b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue @@ -1,664 +1,379 @@ <template> - <div class="my-process-designer"> - <div class="my-process-designer__container"> - <div class="my-process-designer__canvas" style="height: 760px" ref="bpmnCanvas"></div> + <div class="process-viewer"> + <div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div> + <!-- 自定义箭头样式,用于已完成状态下流程连线箭头 --> + <defs ref="customDefs"> + <marker + id="sequenceflow-end-white-success" + viewBox="0 0 20 20" + refX="11" + refY="10" + markerWidth="10" + markerHeight="10" + orient="auto" + > + <path + class="success-arrow" + d="M 1 5 L 11 10 L 1 15 Z" + style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1" + /> + </marker> + <marker + id="conditional-flow-marker-white-success" + viewBox="0 0 20 20" + refX="-1" + refY="10" + markerWidth="10" + markerHeight="10" + orient="auto" + > + <path + class="success-conditional" + d="M 0 10 L 8 6 L 16 10 L 8 14 Z" + style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1" + /> + </marker> + </defs> + + <!-- 审批记录 --> + <el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px"> + <el-row> + <el-table + :data="selectTasks" + size="small" + border + header-cell-class-name="table-header-gray" + > + <el-table-column + label="序号" + header-align="center" + align="center" + type="index" + width="50" + /> + <el-table-column + label="审批人" + min-width="100" + align="center" + v-if="selectActivityType === 'bpmn:UserTask'" + > + <template #default="scope"> + {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} + </template> + </el-table-column> + <el-table-column + label="发起人" + prop="assigneeUser.nickname" + min-width="100" + align="center" + v-else + /> + <el-table-column label="部门" min-width="100" align="center"> + <template #default="scope"> + {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} + </template> + </el-table-column> + <el-table-column + :formatter="dateFormatter" + align="center" + label="开始时间" + prop="createTime" + min-width="140" + /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="结束时间" + prop="endTime" + min-width="140" + /> + <el-table-column align="center" label="审批状态" prop="status" min-width="90"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + align="center" + label="审批建议" + prop="reason" + min-width="120" + v-if="selectActivityType === 'bpmn:UserTask'" + /> + <el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> + <template #default="scope"> + {{ formatPast2(scope.row.durationInMillis) }} + </template> + </el-table-column> + </el-table> + </el-row> + </el-dialog> + + <!-- Zoom:放大、缩小 --> + <div style="position: absolute; top: 0; left: 0; width: 100%"> + <el-row type="flex" justify="end"> + <el-button-group key="scale-control" size="default"> + <el-button + size="default" + :plain="true" + :disabled="defaultZoom <= 0.3" + :icon="ZoomOut" + @click="processZoomOut()" + /> + <el-button size="default" style="width: 90px"> + {{ Math.floor(defaultZoom * 10 * 10) + '%' }} + </el-button> + <el-button + size="default" + :plain="true" + :disabled="defaultZoom >= 3.9" + :icon="ZoomIn" + @click="processZoomIn()" + /> + <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" /> + </el-button-group> + </el-row> </div> </div> </template> <script lang="ts" setup> +import '../theme/index.scss' import BpmnViewer from 'bpmn-js/lib/Viewer' -import DefaultEmptyXML from './plugins/defaultEmpty' -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { formatDate } from '@/utils/formatTime' -import { isEmpty } from '@/utils/is' - -defineOptions({ name: 'MyProcessViewer' }) +import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas' +import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' +import { DICT_TYPE } from '@/utils/dict' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' +import { BpmProcessInstanceStatus } from '@/utils/constants' const props = defineProps({ - value: { - // BPMN XML 字符串 + xml: { type: String, - default: '' + required: true }, - prefix: { - // 使用哪个引擎 - type: String, - default: 'camunda' - }, - activityData: { - // 活动的数据。传递时,可高亮流程 - type: Array, - default: () => [] - }, - processInstanceData: { - // 流程实例的数据。传递时,可展示流程发起人等信息 + view: { type: Object, - default: () => {} - }, - taskData: { - // 任务实例的数据。传递时,可展示 UserTask 审核相关的信息 - type: Array, - default: () => [] + require: true } }) -provide('configGlobal', props) +const processCanvas = ref() +const bpmnViewer = ref<BpmnViewer | null>(null) +const customDefs = ref() +const defaultZoom = ref(1) // 默认缩放比例 +const isLoading = ref(false) // 是否加载中 -const emit = defineEmits(['destroy']) +const processInstance = ref<any>({}) // 流程实例 +const tasks = ref([]) // 流程任务 -let bpmnModeler +const dialogVisible = ref(false) // 弹窗可见性 +const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题 +const selectActivityType = ref<string | undefined>(undefined) // 选中 Task 的活动编号 +const selectTasks = ref<any[]>([]) // 选中的任务数组 -const xml = ref('') -const activityLists = ref<any[]>([]) -const processInstance = ref<any>(undefined) -const taskList = ref<any[]>([]) -const bpmnCanvas = ref() -// const element = ref() -const elementOverlayIds = ref<any>(null) -const overlays = ref<any>(null) - -const initBpmnModeler = () => { - if (bpmnModeler) return - bpmnModeler = new BpmnViewer({ - container: bpmnCanvas.value, - bpmnRenderer: {} - }) +/** Zoom:恢复 */ +const processReZoom = () => { + defaultZoom.value = 1 + bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto') } -/* 创建新的流程图 */ -const createNewDiagram = async (xml) => { - // 将字符串转换成图显示出来 - let newId = `Process_${new Date().getTime()}` - let newName = `业务流程_${new Date().getTime()}` - let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix) - try { - let { warnings } = await bpmnModeler.importXML(xmlString) - if (warnings && warnings.length) { - warnings.forEach((warn) => console.warn(warn)) - } - // 高亮流程图 - await highlightDiagram() - const canvas = bpmnModeler.get('canvas') - canvas.zoom('fit-viewport', 'auto') - } catch (e) { - console.error(e) - // console.error(`[Process Designer Warn]: ${e?.message || e}`); +/** Zoom:放大 */ +const processZoomIn = (zoomStep = 0.1) => { + let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100 + if (newZoom > 4) { + throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4') } + defaultZoom.value = newZoom + bpmnViewer.value?.get('canvas').zoom(defaultZoom.value) } -/* 高亮流程图 */ -// TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html -const highlightDiagram = async () => { - const activityList = activityLists.value - if (activityList.length === 0) { +/** Zoom:缩小 */ +const processZoomOut = (zoomStep = 0.1) => { + let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100 + if (newZoom < 0.2) { + throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2') + } + defaultZoom.value = newZoom + bpmnViewer.value?.get('canvas').zoom(defaultZoom.value) +} + +/** 流程图预览清空 */ +const clearViewer = () => { + if (processCanvas.value) { + processCanvas.value.innerHTML = '' + } + if (bpmnViewer.value) { + bpmnViewer.value.destroy() + } + bpmnViewer.value = null +} + +/** 添加自定义箭头 */ +// TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!! +const addCustomDefs = () => { + if (!bpmnViewer.value) { return } - // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现 - // 再次基础上,增加不同审批结果的颜色等等 - let canvas = bpmnModeler.get('canvas') - let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务 - let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务 - let findProcessTask = false //是否已经高亮了进行中的任务 - //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据 - let removeTaskDefinitionKeyList = [] - // debugger - bpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => { - let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动 - if (!activity) { - return + const canvas = bpmnViewer.value?.get('canvas') + const svg = canvas?._svg + svg.appendChild(customDefs.value) +} + +/** 节点选中 */ +const onSelectElement = (element: any) => { + // 清空原选中 + selectActivityType.value = undefined + dialogTitle.value = undefined + if (!element || !processInstance.value?.id) { + return + } + + // UserTask 的情况 + const activityType = element.type + selectActivityType.value = activityType + if (activityType === 'bpmn:UserTask') { + dialogTitle.value = element.businessObject ? element.businessObject.name : undefined + selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id) + dialogVisible.value = true + } else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') { + dialogTitle.value = '审批信息' + selectTasks.value = [ + { + assigneeUser: processInstance.value.startUser, + createTime: processInstance.value.startTime, + endTime: processInstance.value.endTime, + status: processInstance.value.status, + durationInMillis: processInstance.value.durationInMillis + } + ] + dialogVisible.value = true + } +} + +/** 初始化 BPMN 视图 */ +const importXML = async (xml: string) => { + // 清空流程图 + clearViewer() + + // 初始化流程图 + if (xml != null && xml !== '') { + try { + bpmnViewer.value = new BpmnViewer({ + additionalModules: [MoveCanvasModule], + container: processCanvas.value + }) + // 增加点击事件 + bpmnViewer.value.on('element.click', ({ element }) => { + onSelectElement(element) + }) + + // 初始化 BPMN 视图 + isLoading.value = true + await bpmnViewer.value.importXML(xml) + // 自定义成功的箭头 + addCustomDefs() + } catch (e) { + clearViewer() + } finally { + isLoading.value = false + // 高亮流程 + setProcessStatus(props.view) } - if (n.$type === 'bpmn:UserTask') { - // 用户任务 - // 处理用户任务的高亮 - const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskId - if (!task) { - return - } - // 进行中的任务已经高亮过了,则不高亮后面的任务了 - if (findProcessTask) { - removeTaskDefinitionKeyList.push(n.id) - return - } - // 高亮任务 - canvas.addMarker(n.id, getResultCss(task.status)) - //标记是否高亮了进行中任务 - if (task.status === 1) { - findProcessTask = true - } - // 如果非通过,就不走后面的线条了 - if (task.status !== 2) { - return - } - // 处理 outgoing 出线 - const outgoing = getActivityOutgoing(activity) - outgoing?.forEach((nn: any) => { - // debugger - let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id) - // 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置 - if (targetActivity) { - canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo') - } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') { - // TODO 芋艿:这个流程,暂时没走到过 - canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo') - canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo') - } else if (nn.targetRef.$type === 'bpmn:EndEvent') { - // TODO 芋艿:这个流程,暂时没走到过 - if (!todoActivity && endActivity.key === n.id) { - canvas.addMarker(nn.id, 'highlight') - canvas.addMarker(nn.targetRef.id, 'highlight') - } - if (!activity.endTime) { - canvas.addMarker(nn.id, 'highlight-todo') - canvas.addMarker(nn.targetRef.id, 'highlight-todo') - } + } +} + +/** 高亮流程 */ +const setProcessStatus = (view: any) => { + // 设置相关变量 + if (!view || !view.processInstance) { + return + } + processInstance.value = view.processInstance + tasks.value = view.tasks + if (isLoading.value || !bpmnViewer.value) { + return + } + const { + unfinishedTaskActivityIds, + finishedTaskActivityIds, + finishedSequenceFlowActivityIds, + rejectedTaskActivityIds + } = view + const canvas = bpmnViewer.value.get('canvas') + const elementRegistry = bpmnViewer.value.get('elementRegistry') + + // 已完成节点 + if (Array.isArray(finishedSequenceFlowActivityIds)) { + finishedSequenceFlowActivityIds.forEach((item: any) => { + if (item != null) { + canvas.addMarker(item, 'success') + const element = elementRegistry.get(item) + const conditionExpression = element.businessObject.conditionExpression + if (conditionExpression) { + canvas.addMarker(item, 'condition-expression') } - }) - } else if (n.$type === 'bpmn:ExclusiveGateway') { - // 排它网关 - // 设置【bpmn:ExclusiveGateway】排它网关的高亮 - canvas.addMarker(n.id, getActivityHighlightCss(activity)) - // 查找需要高亮的连线 - let matchNN: any = undefined - let matchActivity: any = undefined - n.outgoing?.forEach((nn: any) => { - let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id) - if (!targetActivity) { - return - } - // 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径: - // 1. 一个是 UserTask => EndEvent - // 2. 一个是 EndEvent - // 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。 - // 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~ - if (!matchActivity || matchActivity.type === 'endEvent') { - matchNN = nn - matchActivity = targetActivity - } - }) - if (matchNN && matchActivity) { - canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity)) } - } else if (n.$type === 'bpmn:ParallelGateway') { - // 并行网关 - // 设置【bpmn:ParallelGateway】并行网关的高亮 - canvas.addMarker(n.id, getActivityHighlightCss(activity)) - n.outgoing?.forEach((nn: any) => { - // 获得连线是否有指向目标。如果有,则进行高亮 - const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id) - if (targetActivity) { - canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线 - // 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。 - canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity)) - } - }) - } else if (n.$type === 'bpmn:StartEvent') { - // 开始节点 - canvas.addMarker(n.id, 'highlight') - n.outgoing?.forEach((nn) => { - // outgoing 例如说【bpmn:SequenceFlow】连线 - // 获得连线是否有指向目标。如果有,则进行高亮 - let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id) - if (targetActivity) { - canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线 - canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己) - } - }) - } else if (n.$type === 'bpmn:EndEvent') { - // 结束节点 - if (!processInstance.value || processInstance.value.status === 1) { - return + }) + } + if (Array.isArray(finishedTaskActivityIds)) { + finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success')) + } + + // 未完成节点 + if (Array.isArray(unfinishedTaskActivityIds)) { + unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary')) + } + + // 被拒绝节点 + if (Array.isArray(rejectedTaskActivityIds)) { + rejectedTaskActivityIds.forEach((item: any) => { + if (item != null) { + canvas.addMarker(item, 'danger') } - canvas.addMarker(n.id, getResultCss(processInstance.value.status)) - } else if (n.$type === 'bpmn:ServiceTask') { - //服务任务 - if (activity.startTime > 0 && activity.endTime === 0) { - //进入执行,标识进行色 - canvas.addMarker(n.id, getResultCss(1)) - } - if (activity.endTime > 0) { - // 执行完成,节点标识完成色, 所有outgoing标识完成色。 - canvas.addMarker(n.id, getResultCss(2)) - const outgoing = getActivityOutgoing(activity) - outgoing?.forEach((out) => { - canvas.addMarker(out.id, getResultCss(2)) - }) - } - } 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.taskDefinitionKey) + }) + } + + // 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里 + if ( + [BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes( + processInstance.value.status ) - } -} - -const getActivityHighlightCss = (activity) => { - return activity.endTime ? 'highlight' : 'highlight-todo' -} - -const getResultCss = (status) => { - if (status === 1) { - // 审批中 - return 'highlight-todo' - } else if (status === 2) { - // 已通过 - return 'highlight' - } else if (status === 3) { - // 不通过 - return 'highlight-reject' - } else if (status === 4) { - // 已取消 - return 'highlight-cancel' - } else if (status === 5) { - // 退回 - return 'highlight-return' - } else if (status === 6) { - // 委派 - return 'highlight-todo' - } else if (status === 7) { - // 审批通过中 - return 'highlight-todo' - } else if (status === 0) { - // 待审批 - return 'highlight-todo' - } - return '' -} - -const getActivityOutgoing = (activity) => { - // 如果有 outgoing,则直接使用它 - if (activity.outgoing && activity.outgoing.length > 0) { - return activity.outgoing - } - // 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing - const flowElements = bpmnModeler.getDefinitions().rootElements[0].flowElements - const outgoing: any[] = [] - flowElements.forEach((item: any) => { - if (item.$type !== 'bpmn:SequenceFlow') { - return - } - if (item.sourceRef.id === activity.key) { - outgoing.push(item) - } - }) - return outgoing -} -const initModelListeners = () => { - const EventBus = bpmnModeler.get('eventBus') - // 注册需要的监听事件 - EventBus.on('element.hover', function (eventObj) { - let element = eventObj ? eventObj.element : null - elementHover(element) - }) - EventBus.on('element.out', function (eventObj) { - let element = eventObj ? eventObj.element : null - elementOut(element) - }) -} -// 流程图的元素被 hover -const elementHover = (element) => { - element.value = element - !elementOverlayIds.value && (elementOverlayIds.value = {}) - !overlays.value && (overlays.value = bpmnModeler.get('overlays')) - // 展示信息 - // console.log(activityLists.value, 'activityLists.value') - // console.log(element.value, 'element.value') - const activity = activityLists.value.find((m) => m.key === element.value.id) - // console.log(activity, 'activityactivityactivityactivity') - if (!activity) { - return - } - if (!elementOverlayIds.value[element.value.id] && element.value.type !== 'bpmn:Process') { - let html = `<div class="element-overlays"> - <p>Elemet id: ${element.value.id}</p> - <p>Elemet type: ${element.value.type}</p> - </div>` // 默认值 - if (element.value.type === 'bpmn:StartEvent' && processInstance.value) { - html = `<p>发起人:${processInstance.value.startUser.nickname}</p> - <p>部门:${processInstance.value.startUser.deptName}</p> - <p>创建时间:${formatDate(processInstance.value.createTime)}` - } else if (element.value.type === 'bpmn:UserTask') { - let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId - if (!task) { - return + ) { + const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent') + endNodes.forEach((item: any) => { + canvas.removeMarker(item.id, 'success') + if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) { + canvas.addMarker(item.id, 'cancel') + } else { + canvas.addMarker(item.id, 'danger') } - let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS) - let dataResult = '' - optionData.forEach((element) => { - if (element.value == task.status) { - dataResult = element.label - } - }) - html = `<p>审批人:${task.assigneeUser.nickname}</p> - <p>部门:${task.assigneeUser.deptName}</p> - <p>结果:${dataResult}</p> - <p>创建时间:${formatDate(task.createTime)}</p>` - // html = `<p>审批人:${task.assigneeUser.nickname}</p> - // <p>部门:${task.assigneeUser.deptName}</p> - // <p>结果:${getIntDictOptions( - // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, - // task.status - // )}</p> - // <p>创建时间:${formatDate(task.createTime)}</p>` - if (task.endTime) { - html += `<p>结束时间:${formatDate(task.endTime)}</p>` - } - if (task.reason) { - html += `<p>审批建议:${task.reason}</p>` - } - } else if (element.value.type === 'bpmn:ServiceTask' && processInstance.value) { - if (activity.startTime > 0) { - html = `<p>创建时间:${formatDate(activity.startTime)}</p>` - } - if (activity.endTime > 0) { - html += `<p>结束时间:${formatDate(activity.endTime)}</p>` - } - console.log(html) - } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) { - let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS) - let dataResult = '' - optionData.forEach((element) => { - if (element.value == processInstance.value.status) { - dataResult = element.label - } - }) - html = `<p>结果:${dataResult}</p>` - // html = `<p>结果:${getIntDictOptions( - // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT, - // processInstance.value.status - // )}</p>` - if (processInstance.value.endTime) { - html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>` - } - } - // 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 }) - elementOverlayIds.value[element.id] = null -} +watch( + () => props.xml, + (newXml) => { + importXML(newXml) + }, + { immediate: true } +) +watch( + () => props.view, + (newView) => { + setProcessStatus(newView) + }, + { immediate: true } +) + +/** mounted:初始化 */ onMounted(() => { - xml.value = props.value - activityLists.value = props.activityData - // 初始化 - initBpmnModeler() - createNewDiagram(xml.value) - // 初始模型的监听器 - initModelListeners() + importXML(props.xml) + setProcessStatus(props.view) }) +/** unmount:销毁 */ onBeforeUnmount(() => { - // this.$once('hook:beforeDestroy', () => { - // }) - if (bpmnModeler) bpmnModeler.destroy() - emit('destroy', bpmnModeler) - bpmnModeler = null + clearViewer() }) - -watch( - () => props.value, - (newValue) => { - xml.value = newValue - createNewDiagram(xml.value) - } -) -watch( - () => props.activityData, - (newActivityData) => { - activityLists.value = newActivityData - createNewDiagram(xml.value) - } -) -watch( - () => props.processInstanceData, - (newProcessInstanceData) => { - processInstance.value = newProcessInstanceData - createNewDiagram(xml.value) - } -) -watch( - () => props.taskData, - (newTaskListData) => { - taskList.value = newTaskListData - createNewDiagram(xml.value) - } -) </script> - -<style lang="scss"> -/** 处理中 */ -.highlight-todo.djs-connection > .djs-visual > path { - stroke: #1890ff !important; - stroke-dasharray: 4px !important; - fill-opacity: 0.2 !important; -} - -.highlight-todo.djs-shape .djs-visual > :nth-child(1) { - fill: #1890ff !important; - stroke: #1890ff !important; - stroke-dasharray: 4px !important; - fill-opacity: 0.2 !important; -} - -:deep(.highlight-todo.djs-connection > .djs-visual > path) { - stroke: #1890ff !important; - stroke-dasharray: 4px !important; - fill-opacity: 0.2 !important; - marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr'); -} - -:deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) { - fill: #1890ff !important; - stroke: #1890ff !important; - stroke-dasharray: 4px !important; - fill-opacity: 0.2 !important; -} - -/** 通过 */ -.highlight.djs-shape .djs-visual > :nth-child(1) { - fill: green !important; - stroke: green !important; - fill-opacity: 0.2 !important; -} - -.highlight.djs-shape .djs-visual > :nth-child(2) { - fill: green !important; -} - -.highlight.djs-shape .djs-visual > path { - fill: green !important; - fill-opacity: 0.2 !important; - stroke: green !important; -} - -.highlight.djs-connection > .djs-visual > path { - stroke: green !important; -} - -.highlight:not(.djs-connection) .djs-visual > :nth-child(1) { - fill: green !important; /* color elements as green */ -} - -:deep(.highlight.djs-shape .djs-visual > :nth-child(1)) { - fill: green !important; - stroke: green !important; - fill-opacity: 0.2 !important; -} - -:deep(.highlight.djs-shape .djs-visual > :nth-child(2)) { - fill: green !important; -} - -:deep(.highlight.djs-shape .djs-visual > path) { - fill: green !important; - fill-opacity: 0.2 !important; - stroke: green !important; -} - -:deep(.highlight.djs-connection > .djs-visual > path) { - stroke: green !important; -} - -.djs-element.highlight > .djs-visual > path { - stroke: green !important; -} - -/** 不通过 */ -.highlight-reject.djs-shape .djs-visual > :nth-child(1) { - fill: red !important; - stroke: red !important; - fill-opacity: 0.2 !important; -} - -.highlight-reject.djs-shape .djs-visual > :nth-child(2) { - fill: red !important; -} - -.highlight-reject.djs-shape .djs-visual > path { - fill: red !important; - fill-opacity: 0.2 !important; - stroke: red !important; -} - -.highlight-reject.djs-connection > .djs-visual > path { - stroke: red !important; - marker-end: url(#sequenceflow-end-white-success) !important; -} - -.highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) { - fill: red !important; /* color elements as green */ -} - -:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) { - fill: red !important; - stroke: red !important; - fill-opacity: 0.2 !important; -} - -:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) { - fill: red !important; -} - -:deep(.highlight-reject.djs-shape .djs-visual > path) { - fill: red !important; - fill-opacity: 0.2 !important; - stroke: red !important; -} - -:deep(.highlight-reject.djs-connection > .djs-visual > path) { - stroke: red !important; -} - -/** 已取消 */ -.highlight-cancel.djs-shape .djs-visual > :nth-child(1) { - fill: grey !important; - stroke: grey !important; - fill-opacity: 0.2 !important; -} - -.highlight-cancel.djs-shape .djs-visual > :nth-child(2) { - fill: grey !important; -} - -.highlight-cancel.djs-shape .djs-visual > path { - fill: grey !important; - fill-opacity: 0.2 !important; - stroke: grey !important; -} - -.highlight-cancel.djs-connection > .djs-visual > path { - stroke: grey !important; -} - -.highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) { - fill: grey !important; /* color elements as green */ -} - -:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) { - fill: grey !important; - stroke: grey !important; - fill-opacity: 0.2 !important; -} - -:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) { - fill: grey !important; -} - -:deep(.highlight-cancel.djs-shape .djs-visual > path) { - fill: grey !important; - fill-opacity: 0.2 !important; - stroke: grey !important; -} - -:deep(.highlight-cancel.djs-connection > .djs-visual > path) { - stroke: grey !important; -} - -/** 回退 */ -.highlight-return.djs-shape .djs-visual > :nth-child(1) { - fill: #e6a23c !important; - stroke: #e6a23c !important; - fill-opacity: 0.2 !important; -} - -.highlight-return.djs-shape .djs-visual > :nth-child(2) { - fill: #e6a23c !important; -} - -.highlight-return.djs-shape .djs-visual > path { - fill: #e6a23c !important; - fill-opacity: 0.2 !important; - stroke: #e6a23c !important; -} - -.highlight-return.djs-connection > .djs-visual > path { - stroke: #e6a23c !important; -} - -.highlight-return:not(.djs-connection) .djs-visual > :nth-child(1) { - fill: #e6a23c !important; /* color elements as green */ -} - -:deep(.highlight-return.djs-shape .djs-visual > :nth-child(1)) { - fill: #e6a23c !important; - stroke: #e6a23c !important; - fill-opacity: 0.2 !important; -} - -:deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) { - fill: #e6a23c !important; -} - -:deep(.highlight-return.djs-shape .djs-visual > path) { - fill: #e6a23c !important; - fill-opacity: 0.2 !important; - stroke: #e6a23c !important; -} - -:deep(.highlight-return.djs-connection > .djs-visual > path) { - stroke: #e6a23c !important; -} - -.element-overlays { - width: 200px; - padding: 8px; - color: #fafafa; - background: rgb(0 0 0 / 60%); - border-radius: 4px; - box-sizing: border-box; -} -</style> diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json index 4ea632a..7fe1fa7 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json @@ -406,6 +406,31 @@ "name": "variableMappingDelegateExpression", "isAttr": true, "type": "String" + }, + { + "name": "calledElementType", + "isAttr": true, + "type": "String" + }, + { + "name": "processInstanceName", + "isAttr": true, + "type": "String" + }, + { + "name": "inheritBusinessKey", + "isAttr": true, + "type": "Boolean" + }, + { + "name": "businessKey", + "isAttr": true, + "type": "String" + }, + { + "name": "inheritVariables", + "isAttr": true, + "type": "Boolean" } ] }, @@ -1211,6 +1236,208 @@ "isAttr": true } ] + }, + { + "name": "AssignStartUserHandlerType", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "Integer", + "isBody": true + } + ] + }, + { + "name": "RejectHandlerType", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "Integer", + "isBody": true + } + ] + }, + { + "name": "RejectReturnTaskId", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "String", + "isBody": true + } + ] + }, + { + "name": "AssignEmptyHandlerType", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "Integer", + "isBody": true + } + ] + }, + { + "name": "AssignEmptyUserIds", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "String", + "isBody": true + } + ] + }, + { + "name": "ButtonsSetting", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "flowable:id", + "type": "Integer", + "isAttr": true + }, + { + "name": "flowable:enable", + "type": "Boolean", + "isAttr": true + }, + { + "name": "flowable:displayName", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "FieldsPermission", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "flowable:field", + "type": "String", + "isAttr": true + }, + { + "name": "flowable:title", + "type": "String", + "isAttr": true + }, + { + "name": "flowable:permission", + "type": "String", + "isAttr": true + } + ] + }, + { + "name": "BoundaryEventType", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:BoundaryEvent"] + }, + "properties": [ + { + "name": "value", + "type": "Integer", + "isBody": true + } + ] + }, + { + "name": "TimeoutHandlerType", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:BoundaryEvent"] + }, + "properties": [ + { + "name": "value", + "type": "Integer", + "isBody": true + } + ] + }, + { + "name": "ApproveType", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "Integer", + "isBody": true + } + ] + }, + { + "name": "ApproveMethod", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "Integer", + "isBody": true + } + ] + }, + { + "name": "CandidateStrategy", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "Integer", + "isBody": true + } + ] + }, + { + "name": "CandidateParam", + "superClass": ["Element"], + "meta": { + "allowedIn": ["bpmn:UserTask"] + }, + "properties": [ + { + "name": "value", + "type": "String", + "isBody": true + } + ] } ], "emumerations": [] diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js index 5e2803b..788e4d1 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js @@ -165,6 +165,18 @@ 'bpmn-icon-user-task', translate('Create User Task') ), + 'create.call-activity': createAction( + 'bpmn:CallActivity', + 'activity', + 'bpmn-icon-call-activity', + translate('Create Call Activity') + ), + 'create.service-task': createAction( + 'bpmn:ServiceTask', + 'activity', + 'bpmn-icon-service', + translate('Create Service Task') + ), 'create.data-object': createAction( 'bpmn:DataObjectReference', 'data-object', diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js index 7098981..304875c 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js @@ -171,6 +171,12 @@ 'bpmn-icon-user-task', translate('Create User Task') ), + 'create.service-task': createAction( + 'bpmn:ServiceTask', + 'activity', + 'bpmn-icon-service', + translate('Create Service Task') + ), 'create.data-object': createAction( 'bpmn:DataObjectReference', 'data-object', diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js b/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js index 777db3e..cb92041 100644 --- a/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js +++ b/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js @@ -56,6 +56,8 @@ 'Create EndEvent': '创建结束事件', 'Create Task': '创建任务', 'Create User Task': '创建用户任务', + 'Create Call Activity': '创建调用活动', + 'Create Service Task': '创建服务任务', 'Create Gateway': '创建网关', 'Create DataObjectReference': '创建数据对象', 'Create DataStoreReference': '创建数据存储', diff --git a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue index 86a1cf7..e426eb6 100644 --- a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue +++ b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue @@ -1,6 +1,6 @@ <template> <div class="process-panel__container" :style="{ width: `${width}px` }"> - <el-collapse v-model="activeTab"> + <el-collapse v-model="activeTab" v-if="isReady"> <el-collapse-item name="base"> <!-- class="panel-tab__title" --> <template #title> @@ -26,8 +26,10 @@ <template #title><Icon icon="ep:list" />表单</template> <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> + <el-collapse-item name="task" v-if="isTaskCollapseItemShow(elementType)" key="task"> + <template #title + ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template + > <element-task :id="elementId" :type="elementType" /> </el-collapse-item> <el-collapse-item @@ -35,8 +37,12 @@ v-if="elementType.indexOf('Task') !== -1" key="multiInstance" > - <template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template> - <element-multi-instance :business-object="elementBusinessObject" :type="elementType" /> + <template #title><Icon icon="ep:help-filled" />多人审批方式</template> + <element-multi-instance + :id="elementId" + :business-object="elementBusinessObject" + :type="elementType" + /> </el-collapse-item> <el-collapse-item name="listeners" key="listeners"> <template #title><Icon icon="ep:bell-filled" />执行监听器</template> @@ -54,6 +60,14 @@ <template #title><Icon icon="ep:promotion" />其他</template> <element-other-config :id="elementId" /> </el-collapse-item> + <el-collapse-item name="customConfig" key="customConfig"> + <template #title><Icon icon="ep:tools" />自定义配置</template> + <element-custom-config + :id="elementId" + :type="elementType" + :business-object="elementBusinessObject" + /> + </el-collapse-item> </el-collapse> </div> </template> @@ -68,6 +82,7 @@ import ElementProperties from './properties/ElementProperties.vue' // import ElementForm from './form/ElementForm.vue' import UserTaskListeners from './listeners/UserTaskListeners.vue' +import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data' defineOptions({ name: 'MyPropertiesPanel' }) @@ -104,24 +119,16 @@ const conditionFormVisible = ref(false) // 流转条件设置 const formVisible = ref(false) // 表单配置 const bpmnElement = ref() +const isReady = ref(false) provide('prefix', props.prefix) provide('width', props.width) -const bpmnInstances = () => (window as any)?.bpmnInstances -// 监听 props.bpmnModeler 然后 initModels -const unwatchBpmn = watch( - () => props.bpmnModeler, - () => { - // 避免加载时 流程图 并未加载完成 - if (!props.bpmnModeler) { - console.log('缺少props.bpmnModeler') - return - } - - console.log('props.bpmnModeler 有值了!!!') - const w = window as any - w.bpmnInstances = { +// 初始化 bpmnInstances +const initBpmnInstances = () => { + if (!props.bpmnModeler) return false + try { + const instances = { modeler: props.bpmnModeler, modeling: props.bpmnModeler.get('modeling'), moddle: props.bpmnModeler.get('moddle'), @@ -133,9 +140,45 @@ selection: props.bpmnModeler.get('selection') } - console.log(bpmnInstances(), 'window.bpmnInstances') - getActiveElement() - unwatchBpmn() + // 检查所有实例是否都存在 + const allInstancesExist = Object.values(instances).every(instance => instance) + if (allInstancesExist) { + const w = window as any + w.bpmnInstances = instances + return true + } + return false + } catch (error) { + console.error('初始化 bpmnInstances 失败:', error) + return false + } +} + +const bpmnInstances = () => (window as any)?.bpmnInstances + +// 监听 props.bpmnModeler 然后 initModels +const unwatchBpmn = watch( + () => props.bpmnModeler, + async () => { + // 避免加载时 流程图 并未加载完成 + if (!props.bpmnModeler) { + console.log('缺少props.bpmnModeler') + return + } + + try { + // 等待 modeler 初始化完成 + await nextTick() + if (initBpmnInstances()) { + isReady.value = true + await nextTick() + getActiveElement() + } else { + console.error('modeler 实例未完全初始化') + } + } catch (error) { + console.error('初始化失败:', error) + } }, { immediate: true @@ -143,6 +186,8 @@ ) const getActiveElement = () => { + if (!isReady.value || !props.bpmnModeler) return + // 初始第一个选中元素 bpmn:Process initFormOnChanged(null) props.bpmnModeler.on('import.done', (e) => { @@ -160,8 +205,11 @@ } }) } + // 初始化数据 const initFormOnChanged = (element) => { + if (!isReady.value || !bpmnInstances()) return + let activatedElement = element if (!activatedElement) { activatedElement = @@ -169,32 +217,36 @@ bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration') } if (!activatedElement) return - console.log(` - ---------- - select element changed: - id: ${activatedElement.id} - type: ${activatedElement.businessObject.$type} - ---------- - `) - console.log('businessObject: ', activatedElement.businessObject) - bpmnInstances().bpmnElement = activatedElement - bpmnElement.value = activatedElement - elementId.value = activatedElement.id - elementType.value = activatedElement.type.split(':')[1] || '' - elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) - conditionFormVisible.value = !!( - elementType.value === 'SequenceFlow' && - activatedElement.source && - activatedElement.source.type.indexOf('StartEvent') === -1 - ) - formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' + + try { + console.log(` + ---------- + select element changed: + id: ${activatedElement.id} + type: ${activatedElement.businessObject.$type} + ---------- + `) + console.log('businessObject: ', activatedElement.businessObject) + bpmnInstances().bpmnElement = activatedElement + bpmnElement.value = activatedElement + elementId.value = activatedElement.id + elementType.value = activatedElement.type.split(':')[1] || '' + elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) + conditionFormVisible.value = !!( + elementType.value === 'SequenceFlow' && + activatedElement.source && + activatedElement.source.type.indexOf('StartEvent') === -1 + ) + formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' + } catch (error) { + console.error('初始化表单数据失败:', error) + } } onBeforeUnmount(() => { const w = window as any w.bpmnInstances = null - console.log(props, 'props1') - console.log(props.bpmnModeler, 'props.bpmnModeler1') + isReady.value = false }) watch( diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue new file mode 100644 index 0000000..f9cb9ac --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue @@ -0,0 +1,39 @@ +<template> + <div class="panel-tab__content"> + <component :is="customConfigComponent" v-bind="$props" /> + </div> +</template> + +<script lang="ts" setup> +import { CustomConfigMap } from './data' + +defineOptions({ name: 'ElementCustomConfig' }) + +const props = defineProps({ + id: String, + type: String, + businessObject: { + type: Object, + default: () => {} + } +}) + +const bpmnInstances = () => (window as any)?.bpmnInstances +const customConfigComponent = ref<any>(null) + +watch( + () => props.businessObject, + () => { + if (props.type && props.businessObject) { + let val = props.type + if (props.businessObject.eventDefinitions) { + val += props.businessObject.eventDefinitions[0]?.$type.split(':')[1] || '' + } + customConfigComponent.value = CustomConfigMap[val]?.componet + } + }, + { immediate: true } +) +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue new file mode 100644 index 0000000..ca46b27 --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue @@ -0,0 +1,252 @@ +<template> + <div> + <el-divider content-position="left">审批人超时未处理时</el-divider> + <el-form-item label="启用开关" prop="timeoutHandlerEnable"> + <el-switch + v-model="timeoutHandlerEnable" + active-text="开启" + inactive-text="关闭" + @change="timeoutHandlerChange" + /> + </el-form-item> + <el-form-item label="执行动作" prop="timeoutHandlerType" v-if="timeoutHandlerEnable"> + <el-radio-group v-model="timeoutHandlerType.value" @change="onTimeoutHandlerTypeChanged"> + <el-radio-button + v-for="item in TIMEOUT_HANDLER_TYPES" + :key="item.value" + :value="item.value" + :label="item.label" + /> + </el-radio-group> + </el-form-item> + <el-form-item label="超时时间设置" v-if="timeoutHandlerEnable"> + <span class="mr-2">当超过</span> + <el-form-item prop="timeDuration"> + <el-input-number + class="mr-2" + :style="{ width: '100px' }" + v-model="timeDuration" + :min="1" + controls-position="right" + @change="() => updateTimeModdle()" + /> + </el-form-item> + <el-select + v-model="timeUnit" + class="mr-2" + :style="{ width: '100px' }" + @change="onTimeUnitChange" + > + <el-option + v-for="item in TIME_UNIT_TYPES" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + 未处理 + </el-form-item> + <el-form-item + label="最大提醒次数" + prop="maxRemindCount" + v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1" + > + <el-input-number + v-model="maxRemindCount" + :min="1" + :max="10" + @change="() => updateTimeModdle()" + /> + </el-form-item> + </div> +</template> + +<script lang="ts" setup> +import { + TimeUnitType, + TIME_UNIT_TYPES, + TIMEOUT_HANDLER_TYPES, +} from '@/components/SimpleProcessDesignerV2/src/consts' +import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils' + +defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' }) +const props = defineProps({ + id: String, + type: String +}) +const prefix = inject('prefix') + +const bpmnElement = ref() +const bpmnInstances = () => (window as any)?.bpmnInstances + +const timeoutHandlerEnable = ref(false) +const boundaryEventType = ref() +const timeoutHandlerType = ref({ + value: undefined +}) +const timeModdle = ref() +const timeDuration = ref(6) +const timeUnit = ref(TimeUnitType.HOUR) +const maxRemindCount = ref(1) + +const elExtensionElements = ref() +const otherExtensions = ref() +const configExtensions = ref([]) +const eventDefinition = ref() + +const resetElement = () => { + bpmnElement.value = bpmnInstances().bpmnElement + eventDefinition.value = bpmnElement.value.businessObject.eventDefinitions[0] + + // 获取元素扩展属性 或者 创建扩展属性 + elExtensionElements.value = + bpmnElement.value.businessObject?.extensionElements ?? + bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) + + // 是否开启自定义用户任务超时处理 + boundaryEventType.value = elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:BoundaryEventType` + )?.[0] + if (boundaryEventType.value && boundaryEventType.value.value === 1) { + timeoutHandlerEnable.value = true + configExtensions.value.push(boundaryEventType.value) + } + + // 执行动作 + timeoutHandlerType.value = elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:TimeoutHandlerType` + )?.[0] + if (timeoutHandlerType.value) { + configExtensions.value.push(timeoutHandlerType.value) + if (eventDefinition.value.timeCycle) { + const timeStr = eventDefinition.value.timeCycle.body + const maxRemindCountStr = timeStr.split('/')[0] + const timeDurationStr = timeStr.split('/')[1] + console.log(maxRemindCountStr) + maxRemindCount.value = parseInt(maxRemindCountStr.slice(1)) + timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1)) + timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1)) + timeModdle.value = eventDefinition.value.timeCycle + } + if (eventDefinition.value.timeDuration) { + const timeDurationStr = eventDefinition.value.timeDuration.body + timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1)) + timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1)) + timeModdle.value = eventDefinition.value.timeDuration + } + } + + // 保留剩余扩展元素,便于后面更新该元素对应属性 + otherExtensions.value = + elExtensionElements.value.values?.filter( + (ex) => + ex.$type !== `${prefix}:BoundaryEventType` && ex.$type !== `${prefix}:TimeoutHandlerType` + ) ?? [] +} + +const timeoutHandlerChange = (val) => { + timeoutHandlerEnable.value = val + if (val) { + // 启用自定义用户任务超时处理 + // 边界事件类型 --- 超时 + boundaryEventType.value = bpmnInstances().moddle.create(`${prefix}:BoundaryEventType`, { + value: 1 + }) + configExtensions.value.push(boundaryEventType.value) + // 超时处理类型 + timeoutHandlerType.value = bpmnInstances().moddle.create(`${prefix}:TimeoutHandlerType`, { + value: 1 + }) + configExtensions.value.push(timeoutHandlerType.value) + // 超时时间表达式 + timeDuration.value = 6 + timeUnit.value = 2 + maxRemindCount.value = 1 + timeModdle.value = bpmnInstances().moddle.create(`bpmn:Expression`, { + body: 'PT6H' + }) + eventDefinition.value.timeDuration = timeModdle.value + } else { + // 关闭自定义用户任务超时处理 + configExtensions.value = [] + delete eventDefinition.value.timeDuration + delete eventDefinition.value.timeCycle + } + updateElementExtensions() +} + +const onTimeoutHandlerTypeChanged = () => { + maxRemindCount.value = 1 + updateElementExtensions() + updateTimeModdle() +} + +const onTimeUnitChange = () => { + // 分钟,默认是 60 分钟 + if (timeUnit.value === TimeUnitType.MINUTE) { + timeDuration.value = 60 + } + // 小时,默认是 6 个小时 + if (timeUnit.value === TimeUnitType.HOUR) { + timeDuration.value = 6 + } + // 天, 默认 1天 + if (timeUnit.value === TimeUnitType.DAY) { + timeDuration.value = 1 + } + updateTimeModdle() +} + +const updateTimeModdle = () => { + if (maxRemindCount.value > 1) { + timeModdle.value.body = 'R' + maxRemindCount.value + '/' + isoTimeDuration() + if (!eventDefinition.value.timeCycle) { + delete eventDefinition.value.timeDuration + eventDefinition.value.timeCycle = timeModdle.value + } + } else { + timeModdle.value.body = isoTimeDuration() + if (!eventDefinition.value.timeDuration) { + delete eventDefinition.value.timeCycle + eventDefinition.value.timeDuration = timeModdle.value + } + } +} + +const isoTimeDuration = () => { + let strTimeDuration = 'PT' + if (timeUnit.value === TimeUnitType.MINUTE) { + strTimeDuration += timeDuration.value + 'M' + } + if (timeUnit.value === TimeUnitType.HOUR) { + strTimeDuration += timeDuration.value + 'H' + } + if (timeUnit.value === TimeUnitType.DAY) { + strTimeDuration += timeDuration.value + 'D' + } + return strTimeDuration +} + +const updateElementExtensions = () => { + const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { + values: [...otherExtensions.value, ...configExtensions.value] + }) + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + extensionElements: extensions + }) +} + +watch( + () => props.id, + (val) => { + val && + val.length && + nextTick(() => { + resetElement() + }) + }, + { immediate: true } +) +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue new file mode 100644 index 0000000..ba38514 --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue @@ -0,0 +1,623 @@ +<!-- UserTask 自定义配置: + 1. 审批人与提交人为同一人时 + 2. 审批人拒绝时 + 3. 审批人为空时 + 4. 操作按钮 + 5. 字段权限 + 6. 审批类型 +--> +<template> + <div> + <el-divider content-position="left">审批类型</el-divider> + <el-form-item prop="approveType"> + <el-radio-group v-model="approveType.value"> + <el-radio + v-for="(item, index) in APPROVE_TYPE" + :key="index" + :value="item.value" + :label="item.value" + > + {{ item.label }} + </el-radio> + </el-radio-group> + </el-form-item> + + <el-divider content-position="left">审批人拒绝时</el-divider> + <el-form-item prop="rejectHandlerType"> + <el-radio-group + v-model="rejectHandlerType" + :disabled="returnTaskList.length === 0" + @change="updateRejectHandlerType" + > + <div class="flex-col"> + <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index"> + <el-radio :key="item.value" :value="item.value" :label="item.label" /> + </div> + </div> + </el-radio-group> + </el-form-item> + <el-form-item + v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK" + label="驳回节点" + prop="returnNodeId" + > + <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId"> + <el-option + v-for="item in returnTaskList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </el-form-item> + + <el-divider content-position="left">审批人为空时</el-divider> + <el-form-item prop="assignEmptyHandlerType"> + <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType"> + <div class="flex-col"> + <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index"> + <el-radio :key="item.value" :value="item.value" :label="item.label" /> + </div> + </div> + </el-radio-group> + </el-form-item> + <el-form-item + v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER" + label="指定用户" + prop="assignEmptyHandlerUserIds" + span="24" + > + <el-select + v-model="assignEmptyUserIds" + clearable + multiple + style="width: 100%" + @change="updateAssignEmptyUserIds" + > + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + + <el-divider content-position="left">审批人与提交人为同一人时</el-divider> + <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType"> + <div class="flex-col"> + <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index"> + <el-radio :key="item.value" :value="item.value" :label="item.label" /> + </div> + </div> + </el-radio-group> + + <el-divider content-position="left">操作按钮</el-divider> + <div class="button-setting-pane"> + <div class="button-setting-title"> + <div class="button-title-label">操作按钮</div> + <div class="pl-4 button-title-label">显示名称</div> + <div class="button-title-label">启用</div> + </div> + <div class="button-setting-item" v-for="(item, index) in buttonsSettingEl" :key="index"> + <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div> + <div class="button-setting-item-label"> + <input + type="text" + class="editable-title-input" + @blur="btnDisplayNameBlurEvent(index)" + v-mountedFocus + v-model="item.displayName" + :placeholder="item.displayName" + v-if="btnDisplayNameEdit[index]" + /> + <el-button v-else text @click="changeBtnDisplayName(index)" + >{{ item.displayName }} <Icon icon="ep:edit" + /></el-button> + </div> + <div class="button-setting-item-label"> + <el-switch v-model="item.enable" /> + </div> + </div> + </div> + + <el-divider content-position="left">字段权限</el-divider> + <div class="field-setting-pane" v-if="formType === 10"> + <div class="field-permit-title"> + <div class="setting-title-label first-title"> 字段名称 </div> + <div class="other-titles"> + <span class="setting-title-label">只读</span> + <span class="setting-title-label">可编辑</span> + <span class="setting-title-label">隐藏</span> + </div> + </div> + <div class="field-setting-item" v-for="(item, index) in fieldsPermissionEl" :key="index"> + <div class="field-setting-item-label"> {{ item.title }} </div> + <el-radio-group class="field-setting-item-group" v-model="item.permission"> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.READ" + size="large" + :label="FieldPermissionType.READ" + ><span></span + ></el-radio> + </div> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.WRITE" + size="large" + :label="FieldPermissionType.WRITE" + ><span></span + ></el-radio> + </div> + <div class="item-radio-wrap"> + <el-radio + :value="FieldPermissionType.NONE" + size="large" + :label="FieldPermissionType.NONE" + ><span></span + ></el-radio> + </div> + </el-radio-group> + </div> + </div> + </div> +</template> + +<script lang="ts" setup> +import { + ASSIGN_START_USER_HANDLER_TYPES, + RejectHandlerType, + REJECT_HANDLER_TYPES, + ASSIGN_EMPTY_HANDLER_TYPES, + AssignEmptyHandlerType, + OPERATION_BUTTON_NAME, + DEFAULT_BUTTON_SETTING, + FieldPermissionType, + APPROVE_TYPE, + ApproveType, + ButtonSetting +} from '@/components/SimpleProcessDesignerV2/src/consts' +import * as UserApi from '@/api/system/user' +import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node' + +defineOptions({ name: 'ElementCustomConfig4UserTask' }) +const props = defineProps({ + id: String, + type: String +}) +const prefix = inject('prefix') + +// 审批人与提交人为同一人时 +const assignStartUserHandlerTypeEl = ref() +const assignStartUserHandlerType = ref() + +// 审批人拒绝时 +const rejectHandlerTypeEl = ref() +const rejectHandlerType = ref() +const returnNodeIdEl = ref() +const returnNodeId = ref() +const returnTaskList = ref([]) + +// 审批人为空时 +const assignEmptyHandlerTypeEl = ref() +const assignEmptyHandlerType = ref() +const assignEmptyUserIdsEl = ref() +const assignEmptyUserIds = ref() + +// 操作按钮 +const buttonsSettingEl = ref() +const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } = useButtonsSetting() + +// 字段权限 +const fieldsPermissionEl = ref([]) +const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( + FieldPermissionType.READ +) + +// 审批类型 +const approveType = ref({ value: ApproveType.USER }) + +const elExtensionElements = ref() +const otherExtensions = ref() +const bpmnElement = ref() +const bpmnInstances = () => (window as any)?.bpmnInstances + +const resetCustomConfigList = () => { + bpmnElement.value = bpmnInstances().bpmnElement + + // 获取可回退的列表 + returnTaskList.value = findAllPredecessorsExcludingStart( + bpmnElement.value.id, + bpmnInstances().modeler + ) + + // 获取元素扩展属性 或者 创建扩展属性 + elExtensionElements.value = + bpmnElement.value.businessObject?.extensionElements ?? + bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) + + // 审批类型 + approveType.value = + elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ApproveType`)?.[0] || + bpmnInstances().moddle.create(`${prefix}:ApproveType`, { value: ApproveType.USER }) + + // 审批人与提交人为同一人时 + assignStartUserHandlerTypeEl.value = + elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType` + )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 }) + assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value + + // 审批人拒绝时 + rejectHandlerTypeEl.value = + elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:RejectHandlerType` + )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 }) + rejectHandlerType.value = rejectHandlerTypeEl.value.value + returnNodeIdEl.value = + elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:RejectReturnTaskId` + )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' }) + returnNodeId.value = returnNodeIdEl.value.value + + // 审批人为空时 + assignEmptyHandlerTypeEl.value = + elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType` + )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 }) + assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value + assignEmptyUserIdsEl.value = + elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:AssignEmptyUserIds` + )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' }) + assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value?.split(',').map((item) => { + // 如果数字超出了最大安全整数范围,则将其作为字符串处理 + let num = Number(item) + return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num + }) + + // 操作按钮 + buttonsSettingEl.value = elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:ButtonsSetting` + ) + if (buttonsSettingEl.value.length === 0) { + DEFAULT_BUTTON_SETTING.forEach((item) => { + buttonsSettingEl.value.push( + bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, { + 'flowable:id': item.id, + 'flowable:displayName': item.displayName, + 'flowable:enable': item.enable + }) + ) + }) + } + + // 字段权限 + if (formType.value === 10) { + const fieldsPermissionList = elExtensionElements.value.values?.filter( + (ex) => ex.$type === `${prefix}:FieldsPermission` + ) + fieldsPermissionEl.value = [] + getNodeConfigFormFields() + // 由于默认添加了发起人元素,这里需要删掉 + fieldsPermissionConfig.value = fieldsPermissionConfig.value.slice(1) + fieldsPermissionConfig.value.forEach((element) => { + element.permission = + fieldsPermissionList?.find((obj) => obj.field === element.field)?.permission ?? '1' + fieldsPermissionEl.value.push( + bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, element) + ) + }) + } + + // 保留剩余扩展元素,便于后面更新该元素对应属性 + otherExtensions.value = + elExtensionElements.value.values?.filter( + (ex) => + ex.$type !== `${prefix}:AssignStartUserHandlerType` && + ex.$type !== `${prefix}:RejectHandlerType` && + ex.$type !== `${prefix}:RejectReturnTaskId` && + ex.$type !== `${prefix}:AssignEmptyHandlerType` && + ex.$type !== `${prefix}:AssignEmptyUserIds` && + ex.$type !== `${prefix}:ButtonsSetting` && + ex.$type !== `${prefix}:FieldsPermission` && + ex.$type !== `${prefix}:ApproveType` + ) ?? [] + + // 更新元素扩展属性,避免后续报错 + updateElementExtensions() +} + +const updateAssignStartUserHandlerType = () => { + assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value + + updateElementExtensions() +} + +const updateRejectHandlerType = () => { + rejectHandlerTypeEl.value.value = rejectHandlerType.value + + returnNodeId.value = returnTaskList.value[0].id + returnNodeIdEl.value.value = returnNodeId.value + + updateElementExtensions() +} + +const updateReturnNodeId = () => { + returnNodeIdEl.value.value = returnNodeId.value + + updateElementExtensions() +} + +const updateAssignEmptyHandlerType = () => { + assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value + + updateElementExtensions() +} + +const updateAssignEmptyUserIds = () => { + assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString() + + updateElementExtensions() +} + +const updateElementExtensions = () => { + const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { + values: [ + ...otherExtensions.value, + assignStartUserHandlerTypeEl.value, + rejectHandlerTypeEl.value, + returnNodeIdEl.value, + assignEmptyHandlerTypeEl.value, + assignEmptyUserIdsEl.value, + approveType.value, + ...buttonsSettingEl.value, + ...fieldsPermissionEl.value + ] + }) + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + extensionElements: extensions + }) +} + +watch( + () => props.id, + (val) => { + val && + val.length && + nextTick(() => { + resetCustomConfigList() + }) + }, + { immediate: true } +) + +function findAllPredecessorsExcludingStart(elementId, modeler) { + const elementRegistry = modeler.get('elementRegistry') + const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow') + const predecessors = new Set() // 使用 Set 来避免重复节点 + const visited = new Set() // 用于记录已访问的节点 + + // 检查是否是开始事件节点 + function isStartEvent(element) { + return element.type === 'bpmn:StartEvent' + } + + function findPredecessorsRecursively(element) { + // 如果该节点已经访问过,直接返回,避免循环 + if (visited.has(element)) { + return + } + + // 标记当前节点为已访问 + visited.add(element) + + // 获取与当前节点相连的所有连接 + const incomingConnections = allConnections.filter((connection) => connection.target === element) + + incomingConnections.forEach((connection) => { + const source = connection.source // 获取前置节点 + + // 只添加不是开始事件的前置节点 + if (!isStartEvent(source)) { + predecessors.add(source.businessObject) + // 递归查找前置节点 + findPredecessorsRecursively(source) + } + }) + } + + const targetElement = elementRegistry.get(elementId) + if (targetElement) { + findPredecessorsRecursively(targetElement) + } + + return Array.from(predecessors) // 返回前置节点数组 +} + +function useButtonsSetting() { + const buttonsSetting = ref<ButtonSetting[]>() + // 操作按钮显示名称可编辑 + const btnDisplayNameEdit = ref<boolean[]>([]) + const changeBtnDisplayName = (index: number) => { + btnDisplayNameEdit.value[index] = true + } + const btnDisplayNameBlurEvent = (index: number) => { + btnDisplayNameEdit.value[index] = false + const buttonItem = buttonsSetting.value![index] + buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)! + } + return { + buttonsSetting, + btnDisplayNameEdit, + changeBtnDisplayName, + btnDisplayNameBlurEvent + } +} + +const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 +onMounted(async () => { + // 获得用户列表 + userOptions.value = await UserApi.getSimpleUserList() +}) +</script> + +<style lang="scss" scoped> +.button-setting-pane { + display: flex; + flex-direction: column; + font-size: 14px; + margin-top: 8px; + + .button-setting-desc { + padding-right: 8px; + margin-bottom: 16px; + font-size: 16px; + font-weight: 700; + } + + .button-setting-title { + display: flex; + justify-content: space-between; + align-items: center; + height: 45px; + padding-left: 12px; + background-color: #f8fafc0a; + border: 1px solid #1f38581a; + + & > :first-child { + width: 100px !important; + text-align: left !important; + } + + & > :last-child { + text-align: center !important; + } + + .button-title-label { + width: 150px; + font-size: 13px; + font-weight: 700; + color: #000; + text-align: left; + } + } + + .button-setting-item { + align-items: center; + display: flex; + justify-content: space-between; + height: 38px; + padding-left: 12px; + border: 1px solid #1f38581a; + border-top: 0; + + & > :first-child { + width: 100px !important; + } + + & > :last-child { + text-align: center !important; + } + + .button-setting-item-label { + width: 150px; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + } + + .editable-title-input { + height: 24px; + max-width: 130px; + margin-left: 4px; + line-height: 24px; + border: 1px solid #d9d9d9; + border-radius: 4px; + transition: all 0.3s; + + &:focus { + border-color: #40a9ff; + outline: 0; + box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); + } + } + } +} + +.field-setting-pane { + display: flex; + flex-direction: column; + font-size: 14px; + + .field-setting-desc { + padding-right: 8px; + margin-bottom: 16px; + font-size: 16px; + font-weight: 700; + } + + .field-permit-title { + display: flex; + justify-content: space-between; + align-items: center; + height: 45px; + padding-left: 12px; + line-height: 45px; + background-color: #f8fafc0a; + border: 1px solid #1f38581a; + + .first-title { + text-align: left !important; + } + + .other-titles { + display: flex; + justify-content: space-between; + } + + .setting-title-label { + display: inline-block; + width: 100px; + padding: 5px 0; + font-size: 13px; + font-weight: 700; + color: #000; + text-align: center; + } + } + + .field-setting-item { + align-items: center; + display: flex; + justify-content: space-between; + height: 38px; + padding-left: 12px; + border: 1px solid #1f38581a; + border-top: 0; + + .field-setting-item-label { + display: inline-block; + width: 100px; + min-height: 16px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: text; + } + + .field-setting-item-group { + display: flex; + justify-content: space-between; + + .item-radio-wrap { + display: inline-block; + width: 100px; + text-align: center; + } + } + } +} +</style> diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts b/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts new file mode 100644 index 0000000..a45355e --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts @@ -0,0 +1,13 @@ +import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue' +import BoundaryEventTimer from './components/BoundaryEventTimer.vue' + +export const CustomConfigMap = { + UserTask: { + name: '用户任务', + componet: UserTaskCustomConfig + }, + BoundaryEventTimerEventDefinition: { + name: '定时边界事件(非中断)', + componet: BoundaryEventTimer + } +} diff --git a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue index 33f0bc0..3bb7d66 100644 --- a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue +++ b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue @@ -268,9 +268,9 @@ const resetFormList = () => { bpmnELement.value = bpmnInstances().bpmnElement formKey.value = bpmnELement.value.businessObject.formKey - if (formKey.value?.length > 0) { - formKey.value = parseInt(formKey.value) - } + // if (formKey.value?.length > 0) { + // formKey.value = parseInt(formKey.value) + // } // 获取元素扩展属性 或者 创建扩展属性 elExtensionElements.value = bpmnELement.value.businessObject.get('extensionElements') || diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue index c557b59..7c5cd1f 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue @@ -302,7 +302,7 @@ } // 打开 监听器详情 侧边栏 const openListenerForm = (listener, index?) => { - // debugger + console.log(listener) if (listener) { listenerForm.value = initListenerForm(listener) editingListenerIndex.value = index @@ -370,7 +370,7 @@ } // 移除监听器 const removeListener = (index) => { - // debugger + debugger ElMessageBox.confirm('确认移除该监听器吗?', '提示', { confirmButtonText: '确 认', cancelButtonText: '取 消' diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue index 76e0c80..4486698 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue @@ -337,16 +337,13 @@ const bpmnInstances = () => (window as any)?.bpmnInstances const resetListenersList = () => { - console.log( - bpmnInstances().bpmnElement, - 'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement' - ) bpmnElement.value = bpmnInstances().bpmnElement otherExtensionList.value = [] bpmnElementListeners.value = bpmnElement.value.businessObject?.extensionElements?.values.filter( (ex) => ex.$type === `${prefix}:TaskListener` ) ?? [] + console.log(bpmnElementListeners.value.map) elementListenersList.value = bpmnElementListeners.value.map((listener) => initListenerType(listener) ) diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts index b4eb1d2..6ca7eb6 100644 --- a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts +++ b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts @@ -1,5 +1,6 @@ // 初始化表单数据 export function initListenerForm(listener) { + console.log(listener) let self = { ...listener } @@ -28,6 +29,7 @@ } export function initListenerType(listener) { + listener.id = listener.$attrs.id let listenerType if (listener.class) listenerType = 'classListener' if (listener.expression) listenerType = 'expressionListener' diff --git a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue index c0ec1ca..de2fb0d 100644 --- a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue +++ b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue @@ -1,6 +1,30 @@ <template> <div class="panel-tab__content"> - <el-form label-width="90px"> + <el-radio-group v-model="approveMethod" @change="onApproveMethodChange"> + <div class="flex-col"> + <div v-for="(item, index) in APPROVE_METHODS" :key="index"> + <el-radio :value="item.value" :label="item.value"> + {{ item.label }} + </el-radio> + <el-form-item prop="approveRatio"> + <el-input-number + v-model="approveRatio" + :min="10" + :max="100" + :step="10" + size="small" + v-if=" + item.value === ApproveMethodType.APPROVE_BY_RATIO && + approveMethod === ApproveMethodType.APPROVE_BY_RATIO + " + @change="onApproveRatioChange" + /> + </el-form-item> + </div> + </div> + </el-radio-group> + <!-- 与Simple设计器配置合并,保留以前的代码 --> + <el-form label-width="90px" style="display: none"> <el-form-item label="快捷配置"> <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button> <el-button size="small" @click="changeConfig('会签')">会签</el-button> @@ -45,17 +69,20 @@ <el-checkbox v-model="loopInstanceForm.asyncBefore" label="异步前" + value="异步前" @change="updateLoopAsync('asyncBefore')" /> <el-checkbox v-model="loopInstanceForm.asyncAfter" label="异步后" + value="异步后" @change="updateLoopAsync('asyncAfter')" /> <el-checkbox v-model="loopInstanceForm.exclusive" v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore" label="排除" + value="排除" @change="updateLoopAsync('exclusive')" /> </el-form-item> @@ -73,11 +100,14 @@ </template> <script lang="ts" setup> +import { ApproveMethodType, APPROVE_METHODS } from '@/components/SimpleProcessDesignerV2/src/consts' + defineOptions({ name: 'ElementMultiInstance' }) const props = defineProps({ businessObject: Object, - type: String + type: String, + id: String }) const prefix = inject('prefix') const loopCharacteristics = ref('') @@ -264,16 +294,118 @@ } } +/** + * -----新版本多实例----- + */ +const approveMethod = ref() +const approveRatio = ref(100) +const otherExtensions = ref() +const getElementLoopNew = () => { + const extensionElements = + bpmnElement.value.businessObject?.extensionElements ?? + bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) + approveMethod.value = extensionElements.values.filter( + (ex) => ex.$type === `${prefix}:ApproveMethod` + )?.[0]?.value + + otherExtensions.value = + extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? [] + + if (!approveMethod.value) { + approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE + updateLoopCharacteristics() + } +} +const onApproveMethodChange = () => { + approveRatio.value = 100 + updateLoopCharacteristics() +} +const onApproveRatioChange = () => { + updateLoopCharacteristics() +} +const updateLoopCharacteristics = () => { + // 根据ApproveMethod生成multiInstanceLoopCharacteristics节点 + if (approveMethod.value === ApproveMethodType.RANDOM_SELECT_ONE_APPROVE) { + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + loopCharacteristics: null + }) + } else { + if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) { + multiLoopInstance.value = bpmnInstances().moddle.create( + 'bpmn:MultiInstanceLoopCharacteristics', + { isSequential: false, collection: '${coll_userList}' } + ) + multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( + 'bpmn:FormalExpression', + { + body: '${ nrOfCompletedInstances/nrOfInstances >= ' + approveRatio.value / 100 + '}' + } + ) + } + if (approveMethod.value === ApproveMethodType.ANY_APPROVE) { + multiLoopInstance.value = bpmnInstances().moddle.create( + 'bpmn:MultiInstanceLoopCharacteristics', + { isSequential: false, collection: '${coll_userList}' } + ) + multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( + 'bpmn:FormalExpression', + { + body: '${ nrOfCompletedInstances > 0 }' + } + ) + } + if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) { + multiLoopInstance.value = bpmnInstances().moddle.create( + 'bpmn:MultiInstanceLoopCharacteristics', + { isSequential: true, collection: '${coll_userList}' } + ) + multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create( + 'bpmn:FormalExpression', + { + body: '1' + } + ) + multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create( + 'bpmn:FormalExpression', + { + body: '${ nrOfCompletedInstances >= nrOfInstances }' + } + ) + } + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + loopCharacteristics: toRaw(multiLoopInstance.value) + }) + } + + // 添加ApproveMethod到ExtensionElements + const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { + values: [ + ...otherExtensions.value, + bpmnInstances().moddle.create(`${prefix}:ApproveMethod`, { + value: approveMethod.value + }) + ] + }) + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + extensionElements: extensions + }) +} + onBeforeUnmount(() => { multiLoopInstance.value = null bpmnElement.value = null }) watch( - () => props.businessObject, + () => props.id, (val) => { - bpmnElement.value = bpmnInstances().bpmnElement - getElementLoop(val) + if (val) { + nextTick(() => { + bpmnElement.value = bpmnInstances().bpmnElement + // getElementLoop(val) + getElementLoopNew() + }) + } }, { immediate: true } ) diff --git a/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue b/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue index 494b3d9..7bf4f0e 100644 --- a/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue +++ b/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue @@ -75,17 +75,16 @@ const bpmnInstances = () => (window as any)?.bpmnInstances const resetAttributesList = () => { - console.log(window, 'windowwindowwindowwindowwindowwindowwindow') bpmnElement.value = bpmnInstances().bpmnElement otherExtensionList.value = [] // 其他扩展配置 bpmnElementProperties.value = // bpmnElement.value.businessObject?.extensionElements?.filter((ex) => { - bpmnElement.value.businessObject?.extensionElements?.values.filter((ex) => { + bpmnElement.value.businessObject?.extensionElements?.values?.filter((ex) => { if (ex.$type !== `${prefix}:Properties`) { otherExtensionList.value.push(ex) } return ex.$type === `${prefix}:Properties` - }) ?? [] + }) ?? []; // 保存所有的 扩展属性字段 bpmnElementPropertyList.value = bpmnElementProperties.value.reduce( diff --git a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue index e808af3..3a71b4c 100644 --- a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue +++ b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue @@ -6,13 +6,20 @@ <el-checkbox v-model="taskConfigForm.asyncBefore" label="异步前" + value="异步前" @change="changeTaskAsync" /> - <el-checkbox v-model="taskConfigForm.asyncAfter" label="异步后" @change="changeTaskAsync" /> + <el-checkbox + v-model="taskConfigForm.asyncAfter" + label="异步后" + value="异步后" + @change="changeTaskAsync" + /> <el-checkbox v-model="taskConfigForm.exclusive" v-if="taskConfigForm.asyncAfter || taskConfigForm.asyncBefore" label="排除" + value="排除" @change="changeTaskAsync" /> </el-form-item> @@ -22,9 +29,7 @@ </template> <script lang="ts" setup> -import UserTask from './task-components/UserTask.vue' -import ScriptTask from './task-components/ScriptTask.vue' -import ReceiveTask from './task-components/ReceiveTask.vue' +import { installedComponent } from './data' defineOptions({ name: 'ElementTaskConfig' }) @@ -38,14 +43,7 @@ exclusive: false }) const witchTaskComponent = ref() -const installedComponent = ref({ - // 手工任务与普通任务一致,不需要其他配置 - // 接收消息任务,需要在全局下插入新的消息实例,并在该节点下的 messageRef 属性绑定该实例 - // 发送任务、服务任务、业务规则任务共用一个相同配置 - UserTask: 'UserTask', // 用户任务配置 - ScriptTask: 'ScriptTask', // 脚本任务配置 - ReceiveTask: 'ReceiveTask' // 消息接收任务 -}) + const bpmnElement = ref() const bpmnInstances = () => (window as any).bpmnInstances @@ -71,15 +69,8 @@ watch( () => props.type, () => { - // witchTaskComponent.value = installedComponent.value[props.type] - if (props.type == installedComponent.value.UserTask) { - witchTaskComponent.value = UserTask - } - if (props.type == installedComponent.value.ScriptTask) { - witchTaskComponent.value = ScriptTask - } - if (props.type == installedComponent.value.ReceiveTask) { - witchTaskComponent.value = ReceiveTask + if (props.type) { + witchTaskComponent.value = installedComponent[props.type].component } }, { immediate: true } diff --git a/src/components/bpmnProcessDesigner/package/penal/task/data.ts b/src/components/bpmnProcessDesigner/package/penal/task/data.ts new file mode 100644 index 0000000..805c9ac --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/task/data.ts @@ -0,0 +1,36 @@ +import UserTask from './task-components/UserTask.vue' +import ServiceTask from './task-components/ServiceTask.vue' +import ScriptTask from './task-components/ScriptTask.vue' +import ReceiveTask from './task-components/ReceiveTask.vue' +import CallActivity from './task-components/CallActivity.vue' + +export const installedComponent = { + UserTask: { + name: '用户任务', + component: UserTask + }, + ServiceTask: { + name: '服务任务', + component: ServiceTask + }, + ScriptTask: { + name: '脚本任务', + component: ScriptTask + }, + ReceiveTask: { + name: '接收任务', + component: ReceiveTask + }, + CallActivity: { + name: '调用活动', + component: CallActivity + } +} + +export const getTaskCollapseItemName = (elementType) => { + return installedComponent[elementType].name +} + +export const isTaskCollapseItemShow = (elementType) => { + return installedComponent[elementType] +} diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue new file mode 100644 index 0000000..6d8268b --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue @@ -0,0 +1,280 @@ +<template> + <div> + <el-form label-width="100px"> + <el-form-item label="实例名称" prop="processInstanceName"> + <el-input + v-model="formData.processInstanceName" + clearable + placeholder="请输入实例名称" + @change="updateCallActivityAttr('processInstanceName')" + /> + </el-form-item> + + <!-- TODO 需要可选择已存在的流程 --> + <el-form-item label="被调用流程" prop="calledElement"> + <el-input + v-model="formData.calledElement" + clearable + placeholder="请输入被调用流程" + @change="updateCallActivityAttr('calledElement')" + /> + </el-form-item> + + <el-form-item label="继承变量" prop="inheritVariables"> + <el-switch + v-model="formData.inheritVariables" + @change="updateCallActivityAttr('inheritVariables')" + /> + </el-form-item> + + <el-form-item label="继承业务键" prop="inheritBusinessKey"> + <el-switch + v-model="formData.inheritBusinessKey" + @change="updateCallActivityAttr('inheritBusinessKey')" + /> + </el-form-item> + + <el-form-item v-if="!formData.inheritBusinessKey" label="业务键表达式" prop="businessKey"> + <el-input + v-model="formData.businessKey" + clearable + placeholder="请输入业务键表达式" + @change="updateCallActivityAttr('businessKey')" + /> + </el-form-item> + + <el-divider /> + <div> + <div class="flex mb-10px"> + <el-text>输入参数</el-text> + <XButton + class="ml-auto" + type="primary" + preIcon="ep:plus" + title="添加参数" + size="small" + @click="openVariableForm('in', null, -1)" + /> + </div> + <el-table :data="inVariableList" max-height="240" fit border> + <el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip /> + <el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip /> + <el-table-column label="操作" width="110px"> + <template #default="scope"> + <el-button link @click="openVariableForm('in', scope.row, scope.$index)" size="small"> + 编辑 + </el-button> + <el-divider direction="vertical" /> + <el-button + link + size="small" + style="color: #ff4d4f" + @click="removeVariable('in', scope.$index)" + > + 移除 + </el-button> + </template> + </el-table-column> + </el-table> + </div> + + <el-divider /> + <div> + <div class="flex mb-10px"> + <el-text>输出参数</el-text> + <XButton + class="ml-auto" + type="primary" + preIcon="ep:plus" + title="添加参数" + size="small" + @click="openVariableForm('out', null, -1)" + /> + </div> + <el-table :data="outVariableList" max-height="240" fit border> + <el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip /> + <el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip /> + <el-table-column label="操作" width="110px"> + <template #default="scope"> + <el-button + link + @click="openVariableForm('out', scope.row, scope.$index)" + size="small" + > + 编辑 + </el-button> + <el-divider direction="vertical" /> + <el-button + link + size="small" + style="color: #ff4d4f" + @click="removeVariable('out', scope.$index)" + > + 移除 + </el-button> + </template> + </el-table-column> + </el-table> + </div> + </el-form> + + <!-- 添加或修改参数 --> + <el-dialog + v-model="variableDialogVisible" + title="参数配置" + width="600px" + append-to-body + destroy-on-close + > + <el-form :model="varialbeFormData" label-width="80px" ref="varialbeFormRef"> + <el-form-item label="源:" prop="source"> + <el-input v-model="varialbeFormData.source" clearable /> + </el-form-item> + <el-form-item label="目标:" prop="target"> + <el-input v-model="varialbeFormData.target" clearable /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="variableDialogVisible = false">取 消</el-button> + <el-button type="primary" @click="saveVariable">确 定</el-button> + </template> + </el-dialog> + </div> +</template> + +<script lang="ts" setup> +defineOptions({ name: 'CallActivity' }) +const props = defineProps({ + id: String, + type: String +}) +const prefix = inject('prefix') +const message = useMessage() + +const formData = ref({ + processInstanceName: '', + calledElement: '', + inheritVariables: false, + businessKey: '', + inheritBusinessKey: false, + calledElementType: 'key' +}) +const inVariableList = ref() +const outVariableList = ref() +const variableType = ref() // 参数类型 +const editingVariableIndex = ref(-1) // 编辑参数下标 +const variableDialogVisible = ref(false) +const varialbeFormRef = ref() +const varialbeFormData = ref({ + source: '', + target: '' +}) + +const bpmnInstances = () => (window as any)?.bpmnInstances +const bpmnElement = ref() +const otherExtensionList = ref() + +const initCallActivity = () => { + bpmnElement.value = bpmnInstances().bpmnElement + console.log(bpmnElement.value.businessObject, 'callActivity') + + // 初始化所有配置项 + Object.keys(formData.value).forEach((key) => { + formData.value[key] = bpmnElement.value.businessObject[key] ?? formData.value[key] + }) + + otherExtensionList.value = [] // 其他扩展配置 + inVariableList.value = [] + outVariableList.value = [] + // 初始化输入参数 + bpmnElement.value.businessObject?.extensionElements?.values?.forEach((ex) => { + if (ex.$type === `${prefix}:In`) { + inVariableList.value.push(ex) + } else if (ex.$type === `${prefix}:Out`) { + outVariableList.value.push(ex) + } else { + otherExtensionList.value.push(ex) + } + }) + + // 默认添加 + // bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + // calledElementType: 'key' + // }) +} + +const updateCallActivityAttr = (attr) => { + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + [attr]: formData.value[attr] + }) +} + +const openVariableForm = (type, data, index) => { + editingVariableIndex.value = index + variableType.value = type + varialbeFormData.value = index === -1 ? {} : { ...data } + variableDialogVisible.value = true +} + +const removeVariable = async (type, index) => { + try { + await message.delConfirm() + if (type === 'in') { + inVariableList.value.splice(index, 1) + } + if (type === 'out') { + outVariableList.value.splice(index, 1) + } + updateElementExtensions() + } catch {} +} + +const saveVariable = () => { + if (editingVariableIndex.value === -1) { + if (variableType.value === 'in') { + inVariableList.value.push( + bpmnInstances().moddle.create(`${prefix}:In`, { ...varialbeFormData.value }) + ) + } + if (variableType.value === 'out') { + outVariableList.value.push( + bpmnInstances().moddle.create(`${prefix}:Out`, { ...varialbeFormData.value }) + ) + } + updateElementExtensions() + } else { + if (variableType.value === 'in') { + inVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source + inVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target + } + if (variableType.value === 'out') { + outVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source + outVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target + } + } + variableDialogVisible.value = false +} + +const updateElementExtensions = () => { + const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { + values: [...inVariableList.value, ...outVariableList.value, ...otherExtensionList.value] + }) + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + extensionElements: extensions + }) +} + +watch( + () => props.id, + (val) => { + val && + val.length && + nextTick(() => { + initCallActivity() + }) + }, + { immediate: true } +) +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue new file mode 100644 index 0000000..2f9c535 --- /dev/null +++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue @@ -0,0 +1,91 @@ +<template> + <div> + <el-form-item label="执行类型" key="executeType"> + <el-select v-model="serviceTaskForm.executeType"> + <el-option label="Java类" value="class" /> + <el-option label="表达式" value="expression" /> + <el-option label="代理表达式" value="delegateExpression" /> + </el-select> + </el-form-item> + <el-form-item + v-if="serviceTaskForm.executeType === 'class'" + label="Java类" + prop="class" + key="execute-class" + > + <el-input v-model="serviceTaskForm.class" clearable @change="updateElementTask" /> + </el-form-item> + <el-form-item + v-if="serviceTaskForm.executeType === 'expression'" + label="表达式" + prop="expression" + key="execute-expression" + > + <el-input v-model="serviceTaskForm.expression" clearable @change="updateElementTask" /> + </el-form-item> + <el-form-item + v-if="serviceTaskForm.executeType === 'delegateExpression'" + label="代理表达式" + prop="delegateExpression" + key="execute-delegate" + > + <el-input v-model="serviceTaskForm.delegateExpression" clearable @change="updateElementTask" /> + </el-form-item> + </div> +</template> + +<script lang="ts" setup> +defineOptions({ name: 'ServiceTask' }) +const props = defineProps({ + id: String, + type: String +}) + +const defaultTaskForm = ref({ + executeType: '', + class: '', + expression: '', + delegateExpression: '' +}) + +const serviceTaskForm = ref<any>({}) +const bpmnElement = ref() + +const bpmnInstances = () => (window as any)?.bpmnInstances + +const resetTaskForm = () => { + for (let key in defaultTaskForm.value) { + let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key] + serviceTaskForm.value[key] = value + if (value) { + serviceTaskForm.value.executeType = key + } + } +} + +const updateElementTask = () => { + let taskAttr = Object.create(null); + const type = serviceTaskForm.value.executeType; + for (let key in serviceTaskForm.value) { + if (key !== 'executeType' && key !== type) taskAttr[key] = null; + } + taskAttr[type] = serviceTaskForm.value[type] || ""; + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr) +} + +onBeforeUnmount(() => { + bpmnElement.value = null +}) + +watch( + () => props.id, + () => { + bpmnElement.value = bpmnInstances().bpmnElement + nextTick(() => { + resetTaskForm() + }) + }, + { immediate: true } +) + +</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 f404ef7..e563bfd 100644 --- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue +++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue @@ -1,5 +1,5 @@ <template> - <el-form label-width="100px"> + <el-form label-width="120px"> <el-form-item label="规则类型" prop="candidateStrategy"> <el-select v-model="userTaskForm.candidateStrategy" @@ -8,15 +8,15 @@ @change="changeCandidateStrategy" > <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)" - :key="dict.value" + v-for="(dict, index) in CANDIDATE_STRATEGY" + :key="index" :label="dict.label" :value="dict.value" /> </el-select> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy == 10" + v-if="userTaskForm.candidateStrategy == CandidateStrategy.ROLE" label="指定角色" prop="candidateParam" > @@ -31,7 +31,11 @@ </el-select> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy == 20 || userTaskForm.candidateStrategy == 21" + v-if=" + userTaskForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER || + userTaskForm.candidateStrategy == CandidateStrategy.DEPT_LEADER || + userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER + " label="指定部门" prop="candidateParam" span="24" @@ -49,7 +53,7 @@ /> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy == 22" + v-if="userTaskForm.candidateStrategy == CandidateStrategy.POST" label="指定岗位" prop="candidateParam" span="24" @@ -65,7 +69,7 @@ </el-select> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy == 30" + v-if="userTaskForm.candidateStrategy == CandidateStrategy.USER" label="指定用户" prop="candidateParam" span="24" @@ -86,7 +90,7 @@ </el-select> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy === 40" + v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP" label="指定用户组" prop="candidateParam" > @@ -106,7 +110,67 @@ </el-select> </el-form-item> <el-form-item - v-if="userTaskForm.candidateStrategy === 60" + v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER" + label="表单内用户字段" + prop="formUser" + > + <el-select + v-model="userTaskForm.candidateParam" + clearable + style="width: 100%" + @change="handleFormUserChange" + > + <el-option + v-for="(item, idx) in userFieldOnFormOptions" + :key="idx" + :label="item.title" + :value="item.field" + :disabled="!item.required" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER" + label="表单内部门字段" + prop="formDept" + > + <el-select + v-model="userTaskForm.candidateParam" + clearable + style="width: 100%" + @change="updateElementTask" + > + <el-option + v-for="(item, idx) in deptFieldOnFormOptions" + :key="idx" + :label="item.title" + :value="item.field" + :disabled="!item.required" + /> + </el-select> + </el-form-item> + <el-form-item + v-if=" + userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || + userTaskForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || + userTaskForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER || + userTaskForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER + " + :label="deptLevelLabel!" + prop="deptLevel" + span="24" + > + <el-select v-model="deptLevel" clearable @change="updateElementTask"> + <el-option + v-for="(item, index) in MULTI_LEVEL_DEPT" + :key="index" + :label="item.label" + :value="item.value" + /> + </el-select> + </el-form-item> + <el-form-item + v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION" label="流程表达式" prop="candidateParam" > @@ -114,12 +178,17 @@ type="textarea" v-model="userTaskForm.candidateParam[0]" clearable - style="width: 72%" + style="width: 100%" @change="updateElementTask" /> - <el-button class="ml-5px" size="small" type="success" @click="openProcessExpressionDialog" - >选择表达式</el-button - > + <XButton + class="!w-1/1 mt-5px" + type="success" + preIcon="ep:select" + title="选择表达式" + size="small" + @click="openProcessExpressionDialog" + /> <!-- 选择弹窗 --> <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" /> </el-form-item> @@ -127,7 +196,12 @@ </template> <script lang="ts" setup> -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { + CANDIDATE_STRATEGY, + CandidateStrategy, + FieldPermissionType, + MULTI_LEVEL_DEPT +} from '@/components/SimpleProcessDesignerV2/src/consts' import { defaultProps, handleTree } from '@/utils/tree' import * as RoleApi from '@/api/system/role' import * as DeptApi from '@/api/system/dept' @@ -136,12 +210,14 @@ import * as UserGroupApi from '@/api/bpm/userGroup' import ProcessExpressionDialog from './ProcessExpressionDialog.vue' import { ProcessExpressionVO } from '@/api/bpm/processExpression' +import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node' defineOptions({ name: 'UserTask' }) const props = defineProps({ id: String, type: String }) +const prefix = inject('prefix') const userTaskForm = ref({ candidateStrategy: undefined, // 分配规则 candidateParam: [] // 分配选项 @@ -155,11 +231,88 @@ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 +const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ) +// 表单内用户字段选项, 必须是必填和用户选择器 +const userFieldOnFormOptions = computed(() => { + return formFieldOptions.filter((item) => item.type === 'UserSelect') +}) +// 表单内部门字段选项, 必须是必填和部门选择器 +const deptFieldOnFormOptions = computed(() => { + return formFieldOptions.filter((item) => item.type === 'DeptSelect') +}) + +const deptLevel = ref(1) +const deptLevelLabel = computed(() => { + let label = '部门负责人来源' + if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { + label = label + '(指定部门向上)' + } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) { + label = label + '(表单内部门向上)' + } else { + label = label + '(发起人部门向上)' + } + return label +}) + +const otherExtensions = ref() + const resetTaskForm = () => { const businessObject = bpmnElement.value.businessObject if (!businessObject) { return } + + const extensionElements = + businessObject?.extensionElements ?? + bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] }) + userTaskForm.value.candidateStrategy = extensionElements.values?.filter( + (ex) => ex.$type === `${prefix}:CandidateStrategy` + )?.[0]?.value + const candidateParamStr = extensionElements.values?.filter( + (ex) => ex.$type === `${prefix}:CandidateParam` + )?.[0]?.value + if (candidateParamStr && candidateParamStr.length > 0) { + if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) { + // 特殊:流程表达式,只有一个 input 输入框 + userTaskForm.value.candidateParam = [candidateParamStr] + } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) { + // 特殊:多级不部门负责人,需要通过'|'分割 + userTaskForm.value.candidateParam = candidateParamStr + .split('|')[0] + .split(',') + .map((item) => { + // 如果数字超出了最大安全整数范围,则将其作为字符串处理 + let num = Number(item) + return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num + }) + deptLevel.value = +candidateParamStr.split('|')[1] + } else if ( + userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || + userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER + ) { + userTaskForm.value.candidateParam = +candidateParamStr + deptLevel.value = +candidateParamStr + } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) { + userTaskForm.value.candidateParam = candidateParamStr.split('|')[0] + deptLevel.value = +candidateParamStr.split('|')[1] + } else { + userTaskForm.value.candidateParam = candidateParamStr.split(',').map((item) => { + // 如果数字超出了最大安全整数范围,则将其作为字符串处理 + let num = Number(item) + return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num + }) + } + } else { + userTaskForm.value.candidateParam = [] + } + + otherExtensions.value = + extensionElements.values?.filter( + (ex) => ex.$type !== `${prefix}:CandidateStrategy` && ex.$type !== `${prefix}:CandidateParam` + ) ?? [] + + // 改用通过extensionElements来存储数据 + return if (businessObject.candidateStrategy != undefined) { userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any } else { @@ -172,7 +325,7 @@ } else { userTaskForm.value.candidateParam = businessObject.candidateParam .split(',') - .map((item) => +item) + .map((item) => item) } } else { userTaskForm.value.candidateParam = [] @@ -182,11 +335,55 @@ /** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */ const changeCandidateStrategy = () => { userTaskForm.value.candidateParam = [] + deptLevel.value = 1 + if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) { + // 特殊处理表单内用户字段,当只有发起人选项时应选中发起人 + if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) { + userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER + } + } updateElementTask() } /** 选中某个 options 时候,更新 bpmn 图 */ const updateElementTask = () => { + let candidateParam = + userTaskForm.value.candidateParam instanceof Array + ? userTaskForm.value.candidateParam.join(',') + : userTaskForm.value.candidateParam + + // 特殊处理多级部门情况 + if ( + userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER || + userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER + ) { + candidateParam += '|' + deptLevel.value + } + // 特殊处理发起人部门负责人、发起人连续部门负责人 + if ( + userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER || + userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER + ) { + candidateParam = deptLevel.value + '' + } + + const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', { + values: [ + ...otherExtensions.value, + bpmnInstances().moddle.create(`${prefix}:CandidateStrategy`, { + value: userTaskForm.value.candidateStrategy + }), + bpmnInstances().moddle.create(`${prefix}:CandidateParam`, { + value: candidateParam + }) + ] + }) + bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { + extensionElements: extensions + }) + + // 改用通过extensionElements来存储数据 + return bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), { candidateStrategy: userTaskForm.value.candidateStrategy, candidateParam: userTaskForm.value.candidateParam.join(',') @@ -203,6 +400,14 @@ updateElementTask() } +const handleFormUserChange = (e) => { + if (e === 'PROCESS_START_USER_ID') { + userTaskForm.value.candidateParam = [] + userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER + } + updateElementTask() +} + watch( () => props.id, () => { diff --git a/src/components/bpmnProcessDesigner/package/theme/element-variables.scss b/src/components/bpmnProcessDesigner/package/theme/element-variables.scss index 49bd326..0646f8e 100644 --- a/src/components/bpmnProcessDesigner/package/theme/element-variables.scss +++ b/src/components/bpmnProcessDesigner/package/theme/element-variables.scss @@ -5,7 +5,7 @@ /* 改变 icon 字体路径变量,必需 */ $--font-path: '~element-ui/lib/theme-chalk/fonts'; -@import '~element-ui/packages/theme-chalk/src/index'; +@use '~element-ui/packages/theme-chalk/src/index'; .el-table td, .el-table th { diff --git a/src/components/bpmnProcessDesigner/package/theme/index.scss b/src/components/bpmnProcessDesigner/package/theme/index.scss index 2e60fad..2404760 100644 --- a/src/components/bpmnProcessDesigner/package/theme/index.scss +++ b/src/components/bpmnProcessDesigner/package/theme/index.scss @@ -1,2 +1,117 @@ -@import './process-designer.scss'; -@import './process-panel.scss'; +@use './process-designer.scss'; +@use './process-panel.scss'; + +$success-color: #4eb819; +$primary-color: #409EFF; +$danger-color: #F56C6C; +$cancel-color: #909399; + +.process-viewer { + position: relative; + border: 1px solid #EFEFEF; + background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important; + + .success-arrow { + fill: $success-color; + stroke: $success-color; + } + + .success-conditional { + fill: white; + stroke: $success-color; + } + + .success.djs-connection { + .djs-visual path { + stroke: $success-color!important; + //marker-end: url(#sequenceflow-end-white-success)!important; + } + } + + .success.djs-connection.condition-expression { + .djs-visual path { + //marker-start: url(#conditional-flow-marker-white-success)!important; + } + } + + .success.djs-shape { + .djs-visual rect { + stroke: $success-color!important; + fill: $success-color!important; + fill-opacity: 0.15!important; + } + + .djs-visual polygon { + stroke: $success-color!important; + } + + .djs-visual path:nth-child(2) { + stroke: $success-color!important; + fill: $success-color!important; + } + + .djs-visual circle { + stroke: $success-color!important; + fill: $success-color!important; + fill-opacity: 0.15!important; + } + } + + .primary.djs-shape { + .djs-visual rect { + stroke: $primary-color!important; + fill: $primary-color!important; + fill-opacity: 0.15!important; + } + + .djs-visual polygon { + stroke: $primary-color!important; + } + + .djs-visual circle { + stroke: $primary-color!important; + fill: $primary-color!important; + fill-opacity: 0.15!important; + } + } + + .danger.djs-shape { + .djs-visual rect { + stroke: $danger-color!important; + fill: $danger-color!important; + fill-opacity: 0.15!important; + } + + .djs-visual polygon { + stroke: $danger-color!important; + } + + .djs-visual circle { + stroke: $danger-color!important; + fill: $danger-color!important; + fill-opacity: 0.15!important; + } + } + + .cancel.djs-shape { + .djs-visual rect { + stroke: $cancel-color!important; + fill: $cancel-color!important; + fill-opacity: 0.15!important; + } + + .djs-visual polygon { + stroke: $cancel-color!important; + } + + .djs-visual circle { + stroke: $cancel-color!important; + fill: $cancel-color!important; + fill-opacity: 0.15!important; + } + } +} + +.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette { + display: none; +} diff --git a/src/components/bpmnProcessDesigner/package/theme/process-designer.scss b/src/components/bpmnProcessDesigner/package/theme/process-designer.scss index 6af945d..ac2976b 100644 --- a/src/components/bpmnProcessDesigner/package/theme/process-designer.scss +++ b/src/components/bpmnProcessDesigner/package/theme/process-designer.scss @@ -1,6 +1,4 @@ -@import 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; -@import 'bpmn-js-token-simulation/assets/css/font-awesome.min.css'; -@import 'bpmn-js-token-simulation/assets/css/normalize.css'; +@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; // 边框被 token-simulation 样式覆盖了 .djs-palette { @@ -83,7 +81,7 @@ height: 100%; position: relative; background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') - repeat !important; + repeat !important; div.toggle-mode { display: none; } @@ -97,12 +95,12 @@ box-sizing: border-box; } } - svg { - width: 100%; - height: 100%; - min-height: 100%; - overflow: hidden; - } + // svg { + // width: 100%; + // height: 100%; + // min-height: 100%; + // overflow: hidden; + // } } } diff --git a/src/components/bpmnProcessDesigner/package/utils.ts b/src/components/bpmnProcessDesigner/package/utils.ts index a7de5f0..8996788 100644 --- a/src/components/bpmnProcessDesigner/package/utils.ts +++ b/src/components/bpmnProcessDesigner/package/utils.ts @@ -2,7 +2,7 @@ const bpmnInstances = () => (window as any)?.bpmnInstances // 创建监听器实例 export function createListenerObject(options, isTask, prefix) { - // debugger + 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 cf5ca1c..2053b1c 100644 --- a/src/config/axios/service.ts +++ b/src/config/axios/service.ts @@ -1,10 +1,4 @@ -import axios, { - AxiosError, - AxiosInstance, - AxiosRequestHeaders, - AxiosResponse, - InternalAxiosRequestConfig -} from 'axios' +import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' import qs from 'qs' @@ -37,7 +31,11 @@ const service: AxiosInstance = axios.create({ baseURL: base_url, // api 的 base_url timeout: request_timeout, // 请求超时时间 - withCredentials: false // 禁用 Cookie 等信息 + withCredentials: false, // 禁用 Cookie 等信息 + // 自定义参数序列化函数 + paramsSerializer: (params) => { + return qs.stringify(params, { allowDots: true }) + } }) // request拦截器 @@ -46,34 +44,31 @@ // 是否需要设置 token let isToken = (config!.headers || {}).isToken === false whiteList.some((v) => { - if (config.url) { - config.url.indexOf(v) > -1 + if (config.url && config.url.indexOf(v) > -1) { return (isToken = false) } }) if (getAccessToken() && !isToken) { - ;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token + config.headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token } // 设置租户 if (tenantEnable && tenantEnable === 'true') { const tenantId = getTenantId() - if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId + if (tenantId) config.headers['tenant-id'] = tenantId } - const params = config.params || {} - const data = config.data || false - if ( - config.method?.toUpperCase() === 'POST' && - (config.headers as AxiosRequestHeaders)['Content-Type'] === - 'application/x-www-form-urlencoded' - ) { - config.data = qs.stringify(data) + const method = config.method?.toUpperCase() + // 防止 GET 请求缓存 + if (method === 'GET') { + config.headers['Cache-Control'] = 'no-cache' + config.headers['Pragma'] = 'no-cache' } - // get参数编码 - if (config.method?.toUpperCase() === 'GET' && params) { - config.params = {} - const paramsStr = qs.stringify(params, { allowDots: true }) - if (paramsStr) { - config.url = config.url + '?' + paramsStr + // 自定义参数序列化函数 + else if (method === 'POST') { + const contentType = config.headers['Content-Type'] || config.headers['content-type'] + if (contentType === 'application/x-www-form-urlencoded') { + if (config.data && typeof config.data !== 'string') { + config.data = qs.stringify(config.data) + } } } return config @@ -165,7 +160,7 @@ t('sys.api.errMsg901') + '</div>' + '<div> </div>' + - '<div>参考 https://xxxx/ 教程</div>' + + '<div>参考 https://doc.iailab.cn/ 教程</div>' + '<div> </div>' + '<div>5 分钟搭建本地环境</div>' }) @@ -206,15 +201,12 @@ const handleAuthorized = () => { const { t } = useI18n() if (!isRelogin.show) { - // 如果已经到重新登录页面则不进行弹窗提示 - if (window.location.href.includes('login?redirect=')) { - return - } isRelogin.show = true ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), { showCancelButton: false, closeOnClickModal: false, showClose: false, + closeOnPressEscape: false, confirmButtonText: t('login.relogin'), type: 'warning' }).then(() => { diff --git a/src/directives/index.ts b/src/directives/index.ts index 89cc8ba..1b99988 100644 --- a/src/directives/index.ts +++ b/src/directives/index.ts @@ -11,3 +11,14 @@ hasRole(app) hasPermi(app) } + +/** + * 导出指令:v-mountedFocus + */ +export const setupMountedFocus = (app: App<Element>) => { + app.directive('mountedFocus', { + mounted(el) { + el.focus() + } + }) +} diff --git a/src/directives/permission/hasPermi.ts b/src/directives/permission/hasPermi.ts index d86d2f5..0ef3c50 100644 --- a/src/directives/permission/hasPermi.ts +++ b/src/directives/permission/hasPermi.ts @@ -5,17 +5,10 @@ export function hasPermi(app: App<Element>) { app.directive('hasPermi', (el, binding) => { - const { wsCache } = useCache() const { value } = binding - const all_permission = '*:*:*' - const permissions = wsCache.get(CACHE_KEY.USER).permissions if (value && value instanceof Array && value.length > 0) { - const permissionFlag = value - - const hasPermissions = permissions.some((permission: string) => { - return all_permission === permission || permissionFlag.includes(permission) - }) + const hasPermissions = hasPermission(value) if (!hasPermissions) { el.parentNode && el.parentNode.removeChild(el) @@ -25,3 +18,14 @@ } }) } + +export const hasPermission = (permission: string[]) => { + const { wsCache } = useCache() + const all_permission = '*:*:*' + const userInfo = wsCache.get(CACHE_KEY.USER) + const permissions = userInfo?.permissions || [] + + return permissions.some((p: string) => { + return all_permission === p || permission.includes(p) + }) +} diff --git a/src/directives/permission/hasRole.ts b/src/directives/permission/hasRole.ts index 31a352a..a512811 100644 --- a/src/directives/permission/hasRole.ts +++ b/src/directives/permission/hasRole.ts @@ -7,8 +7,9 @@ app.directive('hasRole', (el, binding) => { const { wsCache } = useCache() const { value } = binding - const super_admin = 'admin' - const roles = wsCache.get(CACHE_KEY.USER).roles + const super_admin = 'super_admin' + const userInfo = wsCache.get(CACHE_KEY.USER) + const roles = userInfo?.roles || [] if (value && value instanceof Array && value.length > 0) { const roleFlag = value diff --git a/src/hooks/web/useCache.ts b/src/hooks/web/useCache.ts index 4f39f30..f6b2bd1 100644 --- a/src/hooks/web/useCache.ts +++ b/src/hooks/web/useCache.ts @@ -37,3 +37,19 @@ wsCache.delete(CACHE_KEY.ROLE_ROUTERS) // 注意,不要清理 LoginForm 登录表单 } + +export const useSessionCache = (type: CacheType = 'sessionStorage') => { + const wsSessionCache: WebStorageCache = new WebStorageCache({ + storage: type + }) + + return { + wsSessionCache + } +} + +export const deleteUserSessionCache = () => { + const { wsSessionCache } = useSessionCache() + wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) + // 注意,不要清理 用户和 LoginForm 登录表单 +} diff --git a/src/hooks/web/useMessage.ts b/src/hooks/web/useMessage.ts index 2bbf5cb..ac2b552 100644 --- a/src/hooks/web/useMessage.ts +++ b/src/hooks/web/useMessage.ts @@ -90,30 +90,6 @@ cancelButtonText: t('common.cancel'), type: 'warning' }) - }, - // 启用窗体 - enableConfirm(ids, content?: string, tip?: string) { - return ElMessageBox.confirm( - content ? content : t('确定启用选中的'+ ids.length +'项数据?'), - tip ? tip : t('common.confirmTitle'), - { - confirmButtonText: t('common.ok'), - cancelButtonText: t('common.cancel'), - type: 'warning' - } - ) - }, - // 禁用窗体 - disableConfirm(ids, content?: string, tip?: string) { - return ElMessageBox.confirm( - content ? content : t('确定禁用选中的'+ ids.length +'项数据?'), - tip ? tip : t('common.confirmTitle'), - { - confirmButtonText: t('common.ok'), - cancelButtonText: t('common.cancel'), - type: 'warning' - } - ) } } } diff --git a/src/layout/components/AppView.vue b/src/layout/components/AppView.vue index 4434187..df720a1 100644 --- a/src/layout/components/AppView.vue +++ b/src/layout/components/AppView.vue @@ -36,27 +36,10 @@ <template> <section :class="[ - 'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]', + 'p-[var(--app-content-padding)] w-full bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]', { - '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]': - (fixedHeader && - (layout === 'classic' || layout === 'topLeft' || layout === 'top') && - footer) || - (!tagsView && layout === 'top' && footer), - '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]': - tagsView && layout === 'top' && footer, - - '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]': - !fixedHeader && layout === 'classic' && footer, - - '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]': - !fixedHeader && layout === 'topLeft' && footer, - - '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]': - fixedHeader && layout === 'cutMenu' && footer, - - '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]': - !fixedHeader && layout === 'cutMenu' && footer + '!min-h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))] pb-0': + footer } ]" > diff --git a/src/layout/components/Breadcrumb/src/Breadcrumb.vue b/src/layout/components/Breadcrumb/src/Breadcrumb.vue index 4079a06..80770a8 100644 --- a/src/layout/components/Breadcrumb/src/Breadcrumb.vue +++ b/src/layout/components/Breadcrumb/src/Breadcrumb.vue @@ -92,7 +92,7 @@ $prefix-cls: #{$elNamespace}-breadcrumb; .#{$prefix-cls} { - :deep(&__item) { + :deep(.#{$prefix-cls}__item) { display: flex; .#{$prefix-cls}__inner { display: flex; @@ -105,7 +105,7 @@ } } - :deep(&__item):not(:last-child) { + :deep(.#{$prefix-cls}__item):not(:last-child) { .#{$prefix-cls}__inner { color: var(--top-header-text-color); @@ -115,7 +115,7 @@ } } - :deep(&__item):last-child { + :deep(.#{$prefix-cls}__item):last-child { .#{$prefix-cls}__inner { display: flex; align-items: center; diff --git a/src/layout/components/Footer/src/Footer.vue b/src/layout/components/Footer/src/Footer.vue index 5510159..c5a1d1a 100644 --- a/src/layout/components/Footer/src/Footer.vue +++ b/src/layout/components/Footer/src/Footer.vue @@ -12,13 +12,17 @@ const appStore = useAppStore() const title = computed(() => appStore.getTitle) + + +// 添加当前年份计算属性 +const currentYear = computed(() => new Date().getFullYear()) </script> <template> <div :class="prefixCls" - class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]" + class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden" > - <span class="text-14px">Copyright ©2024-{{ title }}</span> + <span class="text-14px">Copyright ©{{ currentYear }} {{ title }}</span> </div> </template> diff --git a/src/layout/components/Logo/src/Logo.vue b/src/layout/components/Logo/src/Logo.vue index 2d1cfb6..ef80370 100644 --- a/src/layout/components/Logo/src/Logo.vue +++ b/src/layout/components/Logo/src/Logo.vue @@ -1,8 +1,17 @@ <script lang="ts" setup> import { computed, onMounted, ref, unref, watch } from 'vue' import { useAppStore } from '@/store/modules/app' +import { useUserStoreWithOut } from '@/store/modules/user' +import { usePermissionStoreWithOut } from '@/store/modules/permission' import { useDesign } from '@/hooks/web/useDesign' -import * as authUtil from "@/utils/auth"; +import {isRelogin} from "@/config/axios/service"; +import router from "@/router"; +import type {RouteRecordRaw} from "vue-router"; +import {CACHE_KEY, useCache, useSessionCache} from "@/hooks/web/useCache"; +import {getAccessToken} from "@/utils/auth"; +import {getInfo} from "@/api/login"; +const { wsCache } = useCache() +const { wsSessionCache } = useSessionCache() defineOptions({ name: 'Logo' }) @@ -59,6 +68,22 @@ } } ) + +/** 刷新所有菜单权限 */ +const gotoHome = async () => { + const permissionStore = usePermissionStoreWithOut() + isRelogin.show = true + let userInfo = await getInfo() + wsCache.set(CACHE_KEY.USER, userInfo) + wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus) + isRelogin.show = false + // 后端过滤菜单 + await permissionStore.generateRoutes() + permissionStore.getAddRouters.forEach((route) => { + router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表 + }) +} + </script> <template> @@ -69,6 +94,7 @@ layout !== 'classic' ? `${prefixCls}__Top` : '', 'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative decoration-none overflow-hidden' ]" + @click="gotoHome" :to="homePath" > <img diff --git a/src/layout/components/Menu/src/Menu.vue b/src/layout/components/Menu/src/Menu.vue index 466cca5..94a1da4 100644 --- a/src/layout/components/Menu/src/Menu.vue +++ b/src/layout/components/Menu/src/Menu.vue @@ -90,6 +90,11 @@ backgroundColor="var(--left-menu-bg-color)" textColor="var(--left-menu-text-color)" activeTextColor="var(--left-menu-text-active-color)" + popperClass={ + unref(menuMode) === 'vertical' + ? `${prefixCls}-popper--vertical` + : `${prefixCls}-popper--horizontal` + } onSelect={menuSelect} > {{ @@ -190,6 +195,16 @@ } } + // 垂直菜单 + &__vertical { + :deep(.#{$elNamespace}-menu--vertical) { + &:not(.#{$elNamespace}-menu--collapse) .#{$elNamespace}-sub-menu__title, + .#{$elNamespace}-menu-item { + padding-right: 0; + } + } + } + // 水平菜单 &__horizontal { height: calc(var(--top-tool-height)) !important; diff --git a/src/layout/components/Message/src/Message.vue b/src/layout/components/Message/src/Message.vue index 6bd7724..d769d88 100644 --- a/src/layout/components/Message/src/Message.vue +++ b/src/layout/components/Message/src/Message.vue @@ -1,10 +1,12 @@ <script lang="ts" setup> import { formatDate } from '@/utils/formatTime' import * as NotifyMessageApi from '@/api/system/notify/message' +import { useUserStoreWithOut } from '@/store/modules/user' defineOptions({ name: 'Message' }) const { push } = useRouter() +const userStore = useUserStoreWithOut() const activeName = ref('notice') const unreadCount = ref(0) // 未读消息数量 const list = ref<any[]>([]) // 消息列表 @@ -37,7 +39,11 @@ // 轮询刷新小红点 setInterval( () => { - getUnreadCount() + if (userStore.getIsSetUser) { + getUnreadCount() + } else { + unreadCount.value = 0 + } }, 1000 * 60 * 2 ) diff --git a/src/layout/components/Setting/src/Setting.vue b/src/layout/components/Setting/src/Setting.vue index e1908b6..2973674 100644 --- a/src/layout/components/Setting/src/Setting.vue +++ b/src/layout/components/Setting/src/Setting.vue @@ -126,8 +126,10 @@ message: ${appStore.getMessage}, // 标签页 tagsView: ${appStore.getTagsView}, + // 标签页 + tagsViewImmerse: ${appStore.getTagsViewImmerse}, // 标签页图标 - getTagsViewIcon: ${appStore.getTagsViewIcon}, + tagsViewIcon: ${appStore.getTagsViewIcon}, // logo logo: ${appStore.getLogo}, // 菜单手风琴 @@ -295,5 +297,6 @@ .#{$prefix-cls} { border-radius: 6px 0 0 6px; + z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/ } </style> diff --git a/src/layout/components/TabMenu/src/TabMenu.vue b/src/layout/components/TabMenu/src/TabMenu.vue index b70464c..efad6a6 100644 --- a/src/layout/components/TabMenu/src/TabMenu.vue +++ b/src/layout/components/TabMenu/src/TabMenu.vue @@ -139,7 +139,7 @@ id={`${variables.namespace}-menu`} class={[ prefixCls, - 'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right', + 'relative bg-[var(--left-menu-bg-color)] layout-border__right', { 'w-[var(--tab-menu-max-width)]': !unref(collapse), 'w-[var(--tab-menu-min-width)]': unref(collapse) @@ -147,7 +147,7 @@ ]} onMouseleave={mouseleave} > - <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]"> + <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height))]"> <div> {() => { return unref(tabRouters).map((v) => { @@ -199,7 +199,7 @@ { '!left-[var(--tab-menu-min-width)]': unref(collapse), '!left-[var(--tab-menu-max-width)]': !unref(collapse), - '!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu), + '!w-[var(--left-menu-max-width)]': unref(showMenu) || unref(fixedMenu), '!w-0': !unref(showMenu) && !unref(fixedMenu) } ]} diff --git a/src/layout/components/TagsView/src/TagsView.vue b/src/layout/components/TagsView/src/TagsView.vue index 7db0cf6..dcbb90f 100644 --- a/src/layout/components/TagsView/src/TagsView.vue +++ b/src/layout/components/TagsView/src/TagsView.vue @@ -1,7 +1,7 @@ <script lang="ts" setup> -import { onMounted, watch, computed, unref, ref, nextTick } from 'vue' -import { useRouter } from 'vue-router' +import { computed, nextTick, onMounted, ref, unref, watch } from 'vue' import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router' +import { useRouter } from 'vue-router' import { usePermissionStore } from '@/store/modules/permission' import { useTagsViewStore } from '@/store/modules/tagsView' import { useAppStore } from '@/store/modules/app' @@ -32,6 +32,8 @@ const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([]) const appStore = useAppStore() + +const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse) const tagsViewIcon = computed(() => appStore.getTagsViewIcon) @@ -125,12 +127,8 @@ const moveToCurrentTag = async () => { await nextTick() for (const v of unref(visitedViews)) { - if (v.fullPath === unref(currentRoute).path) { + if (v.fullPath === unref(currentRoute).fullPath) { moveToTarget(v) - if (v.fullPath !== unref(currentRoute).fullPath) { - tagsViewStore.updateVisitedView(unref(currentRoute)) - } - break } } @@ -205,7 +203,7 @@ // 是否是当前tag const isActive = (route: RouteLocationNormalizedLoaded): boolean => { - return route.path === unref(currentRoute).path + return route.fullPath === unref(currentRoute).fullPath } // 所有右键菜单组件的元素 @@ -266,21 +264,33 @@ class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]" > <span - :class="`${prefixCls}__tool ${prefixCls}__tool--first`" + :class="tagsViewImmerse ? '' : `${prefixCls}__tool ${prefixCls}__tool--first`" class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center" @click="move(-200)" > <Icon - icon="ep:d-arrow-left" - color="var(--el-text-color-placeholder)" :hover-color="isDark ? '#fff' : 'var(--el-color-black)'" + color="var(--el-text-color-placeholder)" + icon="ep:d-arrow-left" /> </span> <div class="flex-1 overflow-hidden"> <ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll"> - <div class="h-full flex"> + <div class="h-[var(--tags-view-height)] flex"> <ContextMenu + v-for="item in visitedViews" + :key="item.fullPath" :ref="itemRefs.set" + :class="[ + `${prefixCls}__item`, + tagsViewImmerse ? `${prefixCls}__item--immerse` : '', + tagsViewIcon ? `${prefixCls}__item--icon` : '', + tagsViewImmerse && tagsViewIcon ? `${prefixCls}__item--immerse--icon` : '', + item?.meta?.affix ? `${prefixCls}__item--affix` : '', + { + 'is-active': isActive(item) + } + ]" :schema="[ { icon: 'ep:refresh', @@ -338,41 +348,36 @@ } } ]" - v-for="item in visitedViews" - :key="item.fullPath" :tag-item="item" - :class="[ - `${prefixCls}__item`, - item?.meta?.affix ? `${prefixCls}__item--affix` : '', - { - 'is-active': isActive(item) - } - ]" @visible-change="visibleChange" > <div> - <router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }"> + <router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="{ ...item }" custom> <div + :class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`" @click="navigate" - class="h-full flex items-center justify-center whitespace-nowrap pl-15px" > <Icon v-if=" - item?.matched && - item?.matched[1] && - item?.matched[1]?.meta?.icon && - tagsViewIcon + tagsViewIcon && + (item?.meta?.icon || + (item?.matched && + item.matched[0] && + item.matched[item.matched.length - 1].meta?.icon)) " - :icon="item?.matched[1]?.meta?.icon" + :icon="item?.meta?.icon || item.matched[item.matched.length - 1].meta.icon" :size="12" class="mr-5px" /> - {{ t(item?.meta?.title as string) }} + {{ + t(item?.meta?.title as string) + + (item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '') + }} <Icon :class="`${prefixCls}__item--close`" + :size="12" color="#333" icon="ep:close" - :size="12" @click.prevent.stop="closeSelectedTag(item)" /> </div> @@ -383,29 +388,28 @@ </ElScrollbar> </div> <span - :class="`${prefixCls}__tool`" + :class="tagsViewImmerse ? '' : `${prefixCls}__tool`" class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center" @click="move(200)" > <Icon - icon="ep:d-arrow-right" - color="var(--el-text-color-placeholder)" :hover-color="isDark ? '#fff' : 'var(--el-color-black)'" + color="var(--el-text-color-placeholder)" + icon="ep:d-arrow-right" /> </span> <span - :class="`${prefixCls}__tool`" + :class="tagsViewImmerse ? '' : `${prefixCls}__tool`" class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center" @click="refreshSelectedTag(selectedTag)" > <Icon - icon="ep:refresh-right" - color="var(--el-text-color-placeholder)" :hover-color="isDark ? '#fff' : 'var(--el-color-black)'" + color="var(--el-text-color-placeholder)" + icon="ep:refresh-right" /> </span> <ContextMenu - trigger="click" :schema="[ { icon: 'ep:refresh', @@ -457,15 +461,16 @@ } } ]" + trigger="click" > <span - :class="`${prefixCls}__tool`" + :class="tagsViewImmerse ? '' : `${prefixCls}__tool`" class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center" > <Icon - icon="ep:menu" - color="var(--el-text-color-placeholder)" :hover-color="isDark ? '#fff' : 'var(--el-color-black)'" + color="var(--el-text-color-placeholder)" + icon="ep:menu" /> </span> </ContextMenu> @@ -485,10 +490,10 @@ &::before { position: absolute; - top: 1px; + top: 0; left: 0; width: 100%; - height: calc(100% - 1px); + height: 100%; border-left: 1px solid var(--el-border-color); content: ''; } @@ -496,10 +501,10 @@ &--first { &::before { position: absolute; - top: 1px; + top: 0; left: 0; width: 100%; - height: calc(100% - 1px); + height: 100%; border-right: 1px solid var(--el-border-color); border-left: none; content: ''; @@ -509,14 +514,15 @@ &__item { position: relative; - top: 2px; + top: 3px; height: calc(100% - 6px); - padding-right: 25px; + padding-right: 15px; margin-left: 4px; font-size: 12px; cursor: pointer; border: 1px solid #d9d9d9; border-radius: 2px; + box-sizing: border-box; &--close { position: absolute; @@ -525,11 +531,16 @@ display: none; transform: translate(0, -50%); } + &:not(.#{$prefix-cls}__item--affix):hover { .#{$prefix-cls}__item--close { display: block; } } + } + + &__item--icon { + padding-right: 20px; } &__item:not(.is-active) { @@ -542,9 +553,45 @@ color: var(--el-color-white); background-color: var(--el-color-primary); border: 1px solid var(--el-color-primary); + .#{$prefix-cls}__item--close { :deep(span) { color: var(--el-color-white) !important; + } + } + } + + &__item--immerse { + top: 2px; + height: calc(100% - 3px); + padding-right: 35px; + margin: 0 -10px; + border: none !important; + -webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E") + 12 27 15; + + .#{$prefix-cls}__item--label { + padding-left: 35px; + } + + .#{$prefix-cls}__item--close { + right: 20px; + } + } + + &__item--immerse--icon { + padding-right: 35px; + } + + &__item--immerse:not(.is-active) { + &:hover { + color: var(--el-color-white); + background-color: var(--el-color-primary); + + .#{$prefix-cls}__item--close { + :deep(span) { + color: var(--el-color-white) !important; + } } } } @@ -574,12 +621,19 @@ color: var(--el-color-white); background-color: var(--el-color-primary); border: 1px solid var(--el-color-primary); + .#{$prefix-cls}__item--close { :deep(span) { color: var(--el-color-white) !important; } } } + + &__item--immerse:not(.is-active) { + &:hover { + color: var(--el-color-white); + } + } } } </style> diff --git a/src/layout/components/UserInfo/src/UserInfo.vue b/src/layout/components/UserInfo/src/UserInfo.vue index 797fb87..714a088 100644 --- a/src/layout/components/UserInfo/src/UserInfo.vue +++ b/src/layout/components/UserInfo/src/UserInfo.vue @@ -23,7 +23,7 @@ const prefixCls = getPrefixCls('user-info') -const avatar = computed(() => userStore.user.avatar ?? avatarImg) +const avatar = computed(() => userStore.user.avatar || avatarImg) const userName = computed(() => userStore.user.nickname ?? 'Admin') // 锁定屏幕 @@ -43,14 +43,14 @@ }) await userStore.loginOut() tagsViewStore.delAllViews() - replace('/login?redirect=/index') + await replace('/login?redirect=/index') } catch {} } const toProfile = async () => { push('/user/profile') } const toDocument = () => { - window.open('https://xxxx/') + window.open('https://doc.iailab.cn/') } </script> diff --git a/src/layout/components/UserInfo/src/components/LockDialog.vue b/src/layout/components/UserInfo/src/components/LockDialog.vue index f4ab7d4..7257be1 100644 --- a/src/layout/components/UserInfo/src/components/LockDialog.vue +++ b/src/layout/components/UserInfo/src/components/LockDialog.vue @@ -21,7 +21,7 @@ }) const userStore = useUserStore() -const avatar = computed(() => userStore.user.avatar ?? avatarImg) +const avatar = computed(() => userStore.user.avatar || avatarImg) const userName = computed(() => userStore.user.nickname ?? 'Admin') const emit = defineEmits(['update:modelValue']) diff --git a/src/layout/components/UserInfo/src/components/LockPage.vue b/src/layout/components/UserInfo/src/components/LockPage.vue index e53443f..27d0a43 100644 --- a/src/layout/components/UserInfo/src/components/LockPage.vue +++ b/src/layout/components/UserInfo/src/components/LockPage.vue @@ -22,7 +22,7 @@ const { getPrefixCls } = useDesign() const prefixCls = getPrefixCls('lock-page') -const avatar = computed(() => userStore.user.avatar ?? avatarImg) +const avatar = computed(() => userStore.user.avatar || avatarImg) const userName = computed(() => userStore.user.nickname ?? 'Admin') const lockStore = useLockStore() diff --git a/src/layout/components/useRenderLayout.tsx b/src/layout/components/useRenderLayout.tsx index 1110cd8..5cae84d 100644 --- a/src/layout/components/useRenderLayout.tsx +++ b/src/layout/components/useRenderLayout.tsx @@ -126,7 +126,7 @@ <ToolHeader class="flex-1"></ToolHeader> </div> - <div class="absolute left-0 top-[var(--logo-height)+1px] h-[calc(100%-1px-var(--logo-height))] w-full flex"> + <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex"> <Menu class="relative layout-border__right !h-full"></Menu> <div class={[ @@ -157,9 +157,9 @@ 'layout-border__bottom absolute', { '!fixed top-0 left-0 z-10': fixedHeader.value, - 'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[calc(var(--logo-height)+1px)]': + 'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[var(--logo-height)]': collapse.value && fixedHeader.value, - 'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[calc(var(--logo-height)+1px)]': + 'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[var(--logo-height)]': !collapse.value && fixedHeader.value } ]} @@ -190,24 +190,14 @@ <Menu class="h-[var(--top-tool-height)] flex-1 px-10px"></Menu> <ToolHeader></ToolHeader> </div> - <div - class={[ - `${prefixCls}-content`, - 'w-full', - { - 'h-[calc(100%-var(--app-footer-height))]': !fixedHeader.value, - 'h-[calc(100%-var(--tags-view-height)-var(--app-footer-height))]': fixedHeader.value - } - ]} - > + <div class={[`${prefixCls}-content`, 'w-full h-[calc(100%-var(--top-tool-height))]']}> <ElScrollbar v-loading={pageLoading.value} class={[ `${prefixCls}-content-scrollbar`, { - 'mt-[var(--tags-view-height)] !pb-[calc(var(--tags-view-height)+var(--app-footer-height))]': - fixedHeader.value, - 'pb-[var(--app-footer-height)]': !fixedHeader.value + '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]': + fixedHeader.value } ]} > @@ -216,7 +206,7 @@ class={[ 'layout-border__bottom layout-border__top relative', { - '!fixed w-full top-[calc(var(--top-tool-height)+1px)] left-0': fixedHeader.value + '!fixed w-full top-[var(--top-tool-height)] left-0': fixedHeader.value } ]} style="transition: width var(--transition-time-02), left var(--transition-time-02);" @@ -238,7 +228,7 @@ <ToolHeader class="flex-1"></ToolHeader> </div> - <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-[calc(100%-2px)] flex"> + <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex"> <TabMenu></TabMenu> <div class={[ @@ -270,18 +260,16 @@ {tagsView.value ? ( <TagsView class={[ - 'relative layout-border__bottom layout-border__top', + 'relative layout-border__bottom', { '!fixed top-0 left-0 z-10': fixedHeader.value, 'w-[calc(100%-var(--tab-menu-min-width))] !left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]': - collapse.value && fixedHeader.value, + collapse.value && fixedHeader.value && !fixedMenu.value, 'w-[calc(100%-var(--tab-menu-max-width))] !left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]': - !collapse.value && fixedHeader.value, - '!fixed top-0 !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] z-10': - fixedHeader.value && fixedMenu.value, - 'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]': + !collapse.value && fixedHeader.value && !fixedMenu.value, + 'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-min-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]': collapse.value && fixedHeader.value && fixedMenu.value, - 'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-max-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]': + 'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-max-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]': !collapse.value && fixedHeader.value && fixedMenu.value } ]} diff --git a/src/main.ts b/src/main.ts index cc30f17..ed098e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,19 @@ // 导入全局的svg图标 import '@/plugins/svgIcon' +import Iconify from '@iconify/iconify' +import epJson from '@iconify/json/json/ep.json' +import faJson from '@iconify/json/json/fa.json' +import faSolidJson from '@iconify/json/json/fa-solid.json' + +Iconify.addCollection(epJson) +Iconify.addCollection(faJson) +Iconify.addCollection(faSolidJson) + +export * from '@iconify/iconify' + +export default Iconify + // 初始化多语言 import { setupI18n } from '@/plugins/vueI18n' @@ -29,7 +42,7 @@ import router, { setupRouter } from '@/router' // 权限 -import { setupAuth } from '@/directives' +import { setupAuth, setupMountedFocus } from '@/directives' import { createApp } from 'vue' @@ -74,6 +87,8 @@ setupAuth(app) + setupMountedFocus(app) + await router.isReady() app.use(VueDOMPurifyHTML) diff --git a/src/plugins/formCreate/index.ts b/src/plugins/formCreate/index.ts index 44556de..07d2c51 100644 --- a/src/plugins/formCreate/index.ts +++ b/src/plugins/formCreate/index.ts @@ -1,7 +1,37 @@ import type { App } from 'vue' // 👇使用 form-create 需额外全局引入 element plus 组件 import { + // ElAutocomplete, + // ElButton, + // ElCascader, + // ElCheckbox, + // ElCheckboxButton, + // ElCheckboxGroup, + // ElCol, + // ElColorPicker, + // ElDatePicker, + // ElDialog, + // ElForm, + // ElInput, + // ElInputNumber, + // ElPopover, + // ElRadio, + // ElRadioButton, + // ElRadioGroup, + // ElRate, + // ElRow, + // ElSelect, + // ElSlider, + // ElSwitch, + // ElTimePicker, + // ElTooltip, + // ElTree, + // ElUpload, + // ElIcon, + // ElProgress, + // 以上会由 @form-create/element-ui/auto-import 自动引入 ElAlert, + ElTransfer, ElAside, ElContainer, ElDivider, @@ -12,7 +42,21 @@ ElTableColumn, ElTabPane, ElTabs, - ElTransfer + ElDropdown, + ElDropdownMenu, + ElDropdownItem, + ElBadge, + ElTag, + ElText, + ElMenu, + ElMenuItem, + ElFooter, + ElMessage, + ElCollapse, + ElCollapseItem, + ElCard, + // ElFormItem, + // ElOption } from 'element-plus' import FcDesigner from '@form-create/designer' import formCreate from '@form-create/element-ui' @@ -41,18 +85,30 @@ }) const components = [ + ElAlert, + ElTransfer, ElAside, - ElPopconfirm, - ElHeader, - ElMain, ElContainer, ElDivider, - ElTransfer, - ElAlert, - ElTabs, + ElHeader, + ElMain, + ElPopconfirm, ElTable, ElTableColumn, ElTabPane, + ElTabs, + ElDropdown, + ElDropdownMenu, + ElDropdownItem, + ElBadge, + ElTag, + ElText, + ElMenu, + ElMenuItem, + ElFooter, + ElMessage, + // ElFormItem, + // ElOption, UploadImg, UploadImgs, UploadFile, @@ -60,7 +116,10 @@ UserSelect, DeptSelect, ApiSelect, - Editor + Editor, + ElCollapse, + ElCollapseItem, + ElCard, ] // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档 diff --git a/src/router/index.ts b/src/router/index.ts index 6af4c26..b818421 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -5,7 +5,7 @@ // 创建路由实例 const router = createRouter({ - history: createWebHistory('/plat'), // createWebHashHistory URL带#,createWebHistory URL不带# + history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带# strict: true, routes: remainingRouter as RouteRecordRaw[], scrollBehavior: () => ({ left: 0, top: 0 }) diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 5de3d74..f603ca5 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -72,6 +72,7 @@ path: '/', component: Layout, name: 'Home', + redirect: '/index', meta: { hidden: true, noTagsView: true @@ -287,6 +288,18 @@ } }, { + path: 'manager/simple/model', + component: () => import('@/views/bpm/simple/SimpleModelDesign.vue'), + name: 'SimpleModelDesign', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '仿钉钉设计流程', + activeMenu: '/bpm/manager/model' + } + }, + { path: 'manager/definition', component: () => import('@/views/bpm/definition/index.vue'), name: 'BpmProcessDefinition', @@ -308,7 +321,12 @@ canTo: true, title: '流程详情', activeMenu: '/bpm/task/my' - } + }, + props: (route) => ({ + id: route.query.id, + taskId: route.query.taskId, + activityId: route.query.activityId + }) }, { path: 'oa/leave/create', @@ -333,6 +351,30 @@ title: '查看 OA 请假', activeMenu: '/bpm/oa/leave' } + }, + { + path: 'manager/model/create', + component: () => import('@/views/bpm/model/form/index.vue'), + name: 'BpmModelCreate', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '创建流程', + activeMenu: '/bpm/manager/model' + } + }, + { + path: 'manager/model/update/:id', + component: () => import('@/views/bpm/model/form/index.vue'), + name: 'BpmModelUpdate', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '修改流程', + activeMenu: '/bpm/manager/model' + } } ] }, diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts index 8733618..e3d6a56 100644 --- a/src/store/modules/app.ts +++ b/src/store/modules/app.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia' import { store } from '../index' -import { setCssVar, humpToUnderline } from '@/utils' +import { humpToUnderline, setCssVar } from '@/utils' import { ElMessage } from 'element-plus' import { CACHE_KEY, useCache } from '@/hooks/web/useCache' import { ElementPlusSize } from '@/types/elementPlus' @@ -21,6 +21,7 @@ locale: boolean message: boolean tagsView: boolean + tagsViewImmerse: boolean tagsViewIcon: boolean logo: boolean fixedHeader: boolean @@ -58,6 +59,7 @@ locale: true, // 多语言图标 message: true, // 消息图标 tagsView: true, // 标签页 + tagsViewImmerse: false, // 标签页沉浸 tagsViewIcon: true, // 是否显示标签图标 logo: true, // logo fixedHeader: true, // 固定toolheader @@ -130,6 +132,9 @@ }, getTagsView(): boolean { return this.tagsView + }, + getTagsViewImmerse(): boolean { + return this.tagsViewImmerse }, getTagsViewIcon(): boolean { return this.tagsViewIcon @@ -208,6 +213,9 @@ setTagsView(tagsView: boolean) { this.tagsView = tagsView }, + setTagsViewImmerse(tagsViewImmerse: boolean) { + this.tagsViewImmerse = tagsViewImmerse + }, setTagsViewIcon(tagsViewIcon: boolean) { this.tagsViewIcon = tagsViewIcon }, diff --git a/src/store/modules/bpm/simpleWorkflow.ts b/src/store/modules/bpm/simpleWorkflow.ts new file mode 100644 index 0000000..2942951 --- /dev/null +++ b/src/store/modules/bpm/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, + approverDrawer: false, + approverConfig1: {}, + copyerDrawer: false, + copyerConfig: {}, + conditionDrawer: false, + conditionsConfig1: { + conditionNodes: [] + }, + userTaskConfig: {} + }), + actions: { + setTableId(payload) { + this.tableId = payload + }, + setIsTried(payload) { + this.isTried = payload + }, + setPromoter(payload) { + this.promoterDrawer = payload + }, + setApproverDrawer(payload) { + this.approverDrawer = payload + }, + setApproverConfig(payload) { + this.approverConfig1 = payload + }, + setCopyerDrawer(payload) { + this.copyerDrawer = payload + }, + setCopyerConfig(payload) { + this.copyerConfig = payload + }, + setCondition(payload) { + this.conditionDrawer = payload + }, + setConditionsConfig(payload) { + this.conditionsConfig1 = payload + }, + setUserTaskConfig(payload) { + this.userTaskConfig = payload + } + } +}) + +export const useWorkFlowStoreWithOut = () => { + return useWorkFlowStore(store) +} diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index 5e3287a..f32facc 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -3,9 +3,9 @@ import { cloneDeep } from 'lodash-es' import remainingRouter from '@/router/modules/remaining' import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper' -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import {CACHE_KEY, useSessionCache} from '@/hooks/web/useCache' -const { wsCache } = useCache() +const { wsSessionCache } = useSessionCache() export interface PermissionState { routers: AppRouteRecordRaw[] @@ -35,15 +35,17 @@ return new Promise<void>(async (resolve) => { // 获得菜单列表,它在登录的时候,setUserInfoAction 方法中已经进行获取 let res: AppCustomRouteRecordRaw[] = [] - if (wsCache.get(CACHE_KEY.ROLE_ROUTERS)) { - res = wsCache.get(CACHE_KEY.ROLE_ROUTERS) as AppCustomRouteRecordRaw[] + const roleRouters = wsSessionCache.get(CACHE_KEY.ROLE_ROUTERS) + if (roleRouters) { + res = roleRouters as AppCustomRouteRecordRaw[] } const routerMap: AppRouteRecordRaw[] = generateRoute(res) // 动态路由,404一定要放到最后面 + // preschooler:vue-router@4以后已支持静态404路由,此处可不再追加 this.addRouters = routerMap.concat([ { path: '/:path(.*)*', - redirect: '/404', + component: () => import('@/views/Error/404.vue'), name: '404Page', meta: { hidden: true, diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts index 25a3a1f..4368efe 100644 --- a/src/store/modules/tagsView.ts +++ b/src/store/modules/tagsView.ts @@ -31,13 +31,27 @@ }, // 新增tag addVisitedView(view: RouteLocationNormalizedLoaded) { - if (this.visitedViews.some((v) => v.path === view.path)) return + if (this.visitedViews.some((v) => v.fullPath === view.fullPath)) return if (view.meta?.noTagsView) return - this.visitedViews.push( - Object.assign({}, view, { - title: view.meta?.title || 'no-name' + const visitedView = Object.assign({}, view, { title: view.meta?.title || 'no-name' }) + + if (visitedView.meta) { + const titleSuffixList: string[] = [] + this.visitedViews.forEach((v) => { + if (v.path === visitedView.path && v.meta?.title === visitedView.meta?.title) { + titleSuffixList.push(v.meta?.titleSuffix || '1') + } }) - ) + if (titleSuffixList.length) { + let titleSuffix = 1 + while (titleSuffixList.includes(`${titleSuffix}`)) { + titleSuffix += 1 + } + visitedView.meta.titleSuffix = titleSuffix === 1 ? undefined : `${titleSuffix}` + } + } + + this.visitedViews.push(visitedView) }, // 新增缓存 addCachedView() { @@ -63,7 +77,7 @@ // 删除tag delVisitedView(view: RouteLocationNormalizedLoaded) { for (const [i, v] of this.visitedViews.entries()) { - if (v.path === view.path) { + if (v.fullPath === view.fullPath) { this.visitedViews.splice(i, 1) break } @@ -95,18 +109,18 @@ // 删除其他tag delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { this.visitedViews = this.visitedViews.filter((v) => { - return v?.meta?.affix || v.path === view.path + return v?.meta?.affix || v.fullPath === view.fullPath }) }, // 删除左侧 delLeftViews(view: RouteLocationNormalizedLoaded) { const index = findIndex<RouteLocationNormalizedLoaded>( this.visitedViews, - (v) => v.path === view.path + (v) => v.fullPath === view.fullPath ) if (index > -1) { this.visitedViews = this.visitedViews.filter((v, i) => { - return v?.meta?.affix || v.path === view.path || i > index + return v?.meta?.affix || v.fullPath === view.fullPath || i > index }) this.addCachedView() } @@ -115,18 +129,18 @@ delRightViews(view: RouteLocationNormalizedLoaded) { const index = findIndex<RouteLocationNormalizedLoaded>( this.visitedViews, - (v) => v.path === view.path + (v) => v.fullPath === view.fullPath ) if (index > -1) { this.visitedViews = this.visitedViews.filter((v, i) => { - return v?.meta?.affix || v.path === view.path || i < index + return v?.meta?.affix || v.fullPath === view.fullPath || i < index }) this.addCachedView() } }, updateVisitedView(view: RouteLocationNormalizedLoaded) { for (let v of this.visitedViews) { - if (v.path === view.path) { + if (v.fullPath === view.fullPath) { v = Object.assign(v, view) break } diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index b386180..e4c45fd 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -1,10 +1,17 @@ import { store } from '@/store' import { defineStore } from 'pinia' import { getAccessToken, removeToken } from '@/utils/auth' -import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache' +import { + CACHE_KEY, + useCache, + deleteUserCache, + useSessionCache, + deleteUserSessionCache +} from '@/hooks/web/useCache' import { getInfo, loginOut } from '@/api/login' const { wsCache } = useCache() +const { wsSessionCache } = useSessionCache() interface UserVO { id: number @@ -62,7 +69,9 @@ this.user = userInfo.user this.isSetUser = true wsCache.set(CACHE_KEY.USER, userInfo) - wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus) + if(!wsSessionCache.get(CACHE_KEY.ROLE_ROUTERS)) { + wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus) + } }, async setUserAvatarAction(avatar: string) { const userInfo = wsCache.get(CACHE_KEY.USER) @@ -82,6 +91,7 @@ await loginOut() removeToken() deleteUserCache() // 删除用户缓存 + deleteUserSessionCache() //删除路由缓存 this.resetState() }, resetState() { diff --git a/src/styles/global.module.scss b/src/styles/global.module.scss index 8448a92..af793f0 100644 --- a/src/styles/global.module.scss +++ b/src/styles/global.module.scss @@ -1,4 +1,4 @@ -@import './variables.scss'; +@use './variables.scss' as *; // 导出变量 :export { namespace: $namespace; diff --git a/src/styles/index.scss b/src/styles/index.scss index fbe76f2..7607941 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,6 +1,7 @@ -@import './var.css'; -@import './FormCreate/index.scss'; -@import 'element-plus/theme-chalk/dark/css-vars.css'; +@use './var.css'; +@use './FormCreate/index.scss'; +@use './theme.scss'; +@use 'element-plus/theme-chalk/dark/css-vars.css'; .reset-margin [class*='el-icon'] + span { margin-left: 2px !important; diff --git a/src/styles/var.css b/src/styles/var.css index 63459ba..44f9405 100644 --- a/src/styles/var.css +++ b/src/styles/var.css @@ -64,3 +64,11 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +*, +:after, +:before { + margin: 0; + padding: 0; + box-sizing: border-box; +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 870bbfb..a291a0d 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -461,3 +461,23 @@ TRUE: true, // 启用 FALSE: false // 禁用 } + +// ========== BPM 模块 ========== + +export const BpmModelType = { + BPMN: 10, // BPMN 设计器 + SIMPLE: 20 // 简易设计器 +} + +export const BpmModelFormType = { + NORMAL: 10, // 流程表单 + CUSTOM: 20 // 业务表单 +} + +export const BpmProcessInstanceStatus = { + NOT_START: -1, // 未开始 + RUNNING: 1, // 审批中 + APPROVE: 2, // 审批通过 + REJECT: 3, // 审批不通过 + CANCEL: 4 // 已取消 +} diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 9347dfc..0795004 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -145,6 +145,7 @@ INFRA_OPERATE_TYPE = 'infra_operate_type', // ========== BPM 模块 ========== + BPM_MODEL_TYPE = 'bpm_model_type', BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy', BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', @@ -164,7 +165,7 @@ MODEL_METHOD_SETTING_VALUE_TYPE = 'model_method_setting_value_type', PRED_GRANULARITY = 'pred_granularity', ITEM_RUN_STATUS = 'item_run_status', - + RESULT_TYPE = 'result_type', // ========== DATA - 数据平台模块 ========== DATA_FIELD_TYPE = 'data_field_type', TAG_DATA_TYPE = 'tag_data_type', @@ -186,4 +187,5 @@ CAMERA_BRAND = 'camera_brand', CAPTURE_TYPE = 'capture_type', MODEL_RESULT_TYPE = 'model_result_type', + DATA_QUALITY = 'data_quality' } diff --git a/src/utils/formCreate.ts b/src/utils/formCreate.ts index 850df8c..a93d9cd 100644 --- a/src/utils/formCreate.ts +++ b/src/utils/formCreate.ts @@ -44,6 +44,7 @@ value?: object ) => { if (isRef(detailPreview)) { + // @ts-ignore detailPreview = detailPreview.value } // @ts-ignore diff --git a/src/utils/permission.ts b/src/utils/permission.ts index a63ee62..43d7f95 100644 --- a/src/utils/permission.ts +++ b/src/utils/permission.ts @@ -12,7 +12,8 @@ const { wsCache } = useCache() const permissionDatas = value const all_permission = '*:*:*' - const permissions = wsCache.get(CACHE_KEY.USER).permissions + const userInfo = wsCache.get(CACHE_KEY.USER) + const permissions = userInfo?.permissions || [] const hasPermission = permissions.some((permission) => { return all_permission === permission || permissionDatas.includes(permission) }) @@ -33,7 +34,8 @@ const { wsCache } = useCache() const permissionRoles = value const super_admin = 'admin' - const roles = wsCache.get(CACHE_KEY.USER).roles + const userInfo = wsCache.get(CACHE_KEY.USER) + const roles = userInfo?.roles || [] const hasRole = roles.some((role) => { return super_admin === role || permissionRoles.includes(role) }) diff --git a/src/utils/routerHelper.ts b/src/utils/routerHelper.ts index e9c8c64..a4b2295 100644 --- a/src/utils/routerHelper.ts +++ b/src/utils/routerHelper.ts @@ -21,7 +21,6 @@ /* Layout */ export const Layout = () => import('@/layout/Layout.vue') - export const getParentLayout = () => { return () => new Promise((resolve) => { @@ -74,7 +73,7 @@ noCache: !route.keepAlive, alwaysShow: route.children && - route.children.length === 1 && + route.children.length > 0 && (route.alwaysShow !== undefined ? route.alwaysShow : true) } as any // 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数 @@ -89,7 +88,8 @@ // 2. 生成 data(AppRouteRecordRaw) // 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive let data: AppRouteRecordRaw = { - path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path, + path: + route.path.indexOf('?') > -1 && !isUrl(route.path) ? route.path.split('?')[0] : route.path, // 注意,需要排除 http 这种 url,避免它带 ? 参数被截取掉 name: route.componentName && route.componentName.length > 0 ? route.componentName @@ -120,7 +120,7 @@ data.children = [childrenData] } else { // 目录 - if (route.children) { + if (route.children?.length) { data.component = Layout data.redirect = getRedirect(route.path, route.children) // 外链 diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 91059ef..e5db503 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -376,6 +376,9 @@ let str = '' function performAThoroughValidation(arr) { + if (typeof arr === 'undefined' || !Array.isArray(arr) || arr.length === 0) { + return false + } for (const item of arr) { if (item.id === nodeId) { str += ` / ${item.name}` diff --git a/src/views/Home/Index.vue b/src/views/Home/Index.vue index 822f800..1558788 100644 --- a/src/views/Home/Index.vue +++ b/src/views/Home/Index.vue @@ -1,13 +1,20 @@ <template> - <div> - <h1>IAILAB 平台主页</h1> + <div id="title"> + <span>工业互联网平台</span> </div> <el-skeleton :loading="loading" animated> - <div id="app" v-for="(item, index) in appList" :key="`dynamics-${index}`"> - <div class="card" @click="gotoApp(item)"> - <img :src="item.icon" style="width: 100px; height: 100px"/> + <div id="app"> + <div class="card" v-for="(item, index) in appList" :key="`dynamics-${index}`"> <div> - {{ item.appName }} + <img class="card-left" :src="item.icon"/> + <div class="card-right"> + <div class="app-title"> + {{ item.appName }} + </div> + <div class="goto-app" @click="gotoApp(item)"> + <div>进入应用</div> + </div> + </div> </div> </div> </div> @@ -18,12 +25,13 @@ import * as AppApi from '@/api/system/app' import {Apps} from "@/views/Home/types"; -import {CACHE_KEY, useCache} from "@/hooks/web/useCache"; +import {CACHE_KEY, useCache, useSessionCache} from "@/hooks/web/useCache"; defineOptions({name: 'Home'}) const {wsCache} = useCache() +const {wsSessionCache} = useSessionCache() const loading = ref(true) @@ -37,10 +45,10 @@ const getAppMenuList = async (id, appCode) => { const data = await AppApi.getAppMenuList(id) let userInfo = wsCache.get(CACHE_KEY.USER) - userInfo.menus = data + // userInfo.menus = data wsCache.set(CACHE_KEY.USER, userInfo) - wsCache.set(CACHE_KEY.ROLE_ROUTERS, data) - window.location.href = '/plat/index' + wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, data) + window.location.href = import.meta.env.VITE_BASE_PATH + 'index' } const getAllApi = async () => { @@ -56,18 +64,18 @@ const gotoApp = async (item) => { let path = window.location.pathname let appName = path.split("/")[0] - console.log(appName) let id = item.id let type = item.type let appCode = item.appCode if (type === 0) { await getAppMenuList(id, appCode) } else { - const data = await AppApi.getAppMenuList(id) - let userInfo = wsCache.get(CACHE_KEY.USER) - userInfo.menus = data - wsCache.set(CACHE_KEY.USER, userInfo) - wsCache.set(CACHE_KEY.ROLE_ROUTERS, data) + // const data = await AppApi.getAppMenuList(id) + // let userInfo = wsCache.get(CACHE_KEY.USER) + // userInfo.menus = data + // wsCache.set(CACHE_KEY.USER, userInfo) + // wsSessionCache.set(CACHE_KEY.ROLE_ROUTERS, data) + localStorage.setItem(appCode, id) window.open(item.appDomain + '/index', '_blank') // window.open('/plat/shasteel', '_blank') // window.location.href = '/plat/shasteel' @@ -78,24 +86,67 @@ </script> <style lang="scss" scoped> +#title { + width: 280px; + height: 51px; + margin: 30px auto; + font-family: Microsoft YaHei UI, Microsoft YaHei UI; + font-weight: bold; + font-size: 40px; + color: #282F3D; +} + #app { - width: 300px; - height: 200px; - display: inline-block; - background: transparent; + margin: 0 96px; + width: 100%; } .card { - border: thin dashed gainsboro; - width: 150px; - height: 120px; - padding: 30px; - text-align: center; - justify-content: center; - font-size: 15px; - font-weight: bolder; - color: blue; - background: aliceblue; - border-radius: 10px; + width: 354px; + height: 200px; + margin: 0 24px 24px 0; + background: linear-gradient(180deg, #E9F0FA 0%, #FFFFFF 100%); + border-radius: 12px 12px 12px 12px; + border: 2px solid; + border-image: linear-gradient(180deg, rgba(255, 255, 255, 1), rgba(255, 255, 255, 1)) 2 2; + display: inline-block; +} + +.card-left { + height: 100px; + width: 100px; + float: left; + margin: 50px 30px; +} + +.card-right { + float: right; + margin: 61px 10px; +} + +.app-title { + width: 162px; + font-family: Microsoft YaHei, Microsoft YaHei; + font-weight: bold; + font-size: 24px; + color: #282F3D; +} + +.goto-app { + width: 96px; + height: 35px; + margin-top: 5px; + background: #3A99FD; + border-radius: 80px 80px 80px 80px; + cursor: pointer; +} + +.goto-app > div { + padding: 6px; + margin-left: 5px; + font-family: Microsoft YaHei UI, Microsoft YaHei UI; + font-weight: 400; + font-size: 18px; + color: #FFFFFF; } </style> diff --git a/src/views/bpm/category/CategoryForm.vue b/src/views/bpm/category/CategoryForm.vue index 5b77153..9c24b3e 100644 --- a/src/views/bpm/category/CategoryForm.vue +++ b/src/views/bpm/category/CategoryForm.vue @@ -18,7 +18,7 @@ <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" - :label="dict.value" + :value="dict.value" > {{ dict.label }} </el-radio> @@ -42,6 +42,7 @@ <script setup lang="ts"> import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { CategoryApi, CategoryVO } from '@/api/bpm/category' +import { CommonStatusEnum } from '@/utils/constants' /** BPM 流程分类 表单 */ defineOptions({ name: 'CategoryForm' }) @@ -57,7 +58,7 @@ id: undefined, name: undefined, code: undefined, - status: undefined, + status: CommonStatusEnum.ENABLE, sort: undefined }) const formRules = reactive({ @@ -116,7 +117,7 @@ id: undefined, name: undefined, code: undefined, - status: undefined, + status: CommonStatusEnum.ENABLE, sort: undefined } formRef.value?.resetFields() diff --git a/src/views/bpm/definition/index.vue b/src/views/bpm/definition/index.vue index 03aa475..8d5309d 100644 --- a/src/views/bpm/definition/index.vue +++ b/src/views/bpm/definition/index.vue @@ -69,13 +69,7 @@ <!-- 弹窗:流程模型图的预览 --> <Dialog title="流程图" v-model="bpmnDetailVisible" width="800"> - <MyProcessViewer - key="designer" - v-model="bpmnXml" - :value="bpmnXml as any" - v-bind="bpmnControlForm" - :prefix="bpmnControlForm.prefix" - /> + <MyProcessViewer style="height: 700px" key="designer" :xml="bpmnXml" /> </Dialog> </template> @@ -117,7 +111,7 @@ rule: [], option: {} }) -const handleFormDetail = async (row) => { +const handleFormDetail = async (row: any) => { if (row.formType == 10) { // 设置表单 setConfAndFields2(formDetailPreview, row.formConf, row.formFields) @@ -132,13 +126,13 @@ /** 流程图的详情按钮操作 */ const bpmnDetailVisible = ref(false) -const bpmnXml = ref(null) -const bpmnControlForm = ref({ - prefix: 'flowable' -}) -const handleBpmnDetail = async (row) => { - bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml +const bpmnXml = ref('') +const handleBpmnDetail = async (row: any) => { + // 设置可见 + bpmnXml.value = '' bpmnDetailVisible.value = true + // 加载 BPMN XML + bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml } /** 初始化 **/ diff --git a/src/views/bpm/form/editor/index.vue b/src/views/bpm/form/editor/index.vue index 0d1230c..12945e0 100644 --- a/src/views/bpm/form/editor/index.vue +++ b/src/views/bpm/form/editor/index.vue @@ -1,14 +1,18 @@ <template> - <ContentWrap> + <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0"> <!-- 表单设计器 --> - <FcDesigner ref="designer" height="780px"> - <template #handle> - <el-button round size="small" type="primary" @click="handleSave"> - <Icon class="mr-5px" icon="ep:plus" /> - 保存 - </el-button> - </template> - </FcDesigner> + <div + class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]" + > + <fc-designer class="my-designer" ref="designer" :config="designerConfig"> + <template #handle> + <el-button size="small" type="success" plain @click="handleSave"> + <Icon class="mr-5px" icon="ep:plus" /> + 保存 + </el-button> + </template> + </fc-designer> + </div> </ContentWrap> <!-- 表单保存的弹窗 --> @@ -22,7 +26,7 @@ <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" - :label="dict.value" + :value="dict.value" > {{ dict.label }} </el-radio> @@ -55,6 +59,35 @@ const { query } = useRoute() // 路由信息 const { delView } = useTagsViewStore() // 视图操作 +// 表单设计器配置 +const designerConfig = ref({ + switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段 + autoActive: true, // 是否自动选中拖入的组件 + useTemplate: false, // 是否生成vue2语法的模板组件 + formOptions: { + form: { + labelWidth: '100px' // 设置默认的 label 宽度为 100px + } + }, // 定义表单配置默认值 + fieldReadonly: false, // 配置field是否可以编辑 + hiddenDragMenu: false, // 隐藏拖拽操作按钮 + hiddenDragBtn: false, // 隐藏拖拽按钮 + hiddenMenu: [], // 隐藏部分菜单 + hiddenItem: [], // 隐藏部分组件 + hiddenItemConfig: {}, // 隐藏组件的部分配置项 + disabledItemConfig: {}, // 禁用组件的部分配置项 + showSaveBtn: false, // 是否显示保存按钮 + showConfig: true, // 是否显示右侧的配置界面 + showBaseForm: true, // 是否显示组件的基础配置表单 + showControl: true, // 是否显示组件联动 + showPropsForm: true, // 是否显示组件的属性配置表单 + showEventForm: true, // 是否显示组件的事件配置表单 + showValidateForm: true, // 是否显示组件的验证配置表单 + showFormConfig: true, // 是否显示表单配置 + showInputData: true, // 是否显示录入按钮 + showDevice: true, // 是否显示多端适配选项 + appendConfigData: [] // 定义渲染规则所需的formData +}) const designer = ref() // 表单设计器 useFormCreateDesigner(designer) // 表单设计器增强 const dialogVisible = ref(false) // 弹窗是否展示 @@ -119,3 +152,13 @@ setConfAndFields(designer, data.conf, data.fields) }) </script> + +<style> +.my-designer { + ._fc-l, + ._fc-m, + ._fc-r { + border-top: none; + } +} +</style> diff --git a/src/views/bpm/form/index.vue b/src/views/bpm/form/index.vue index 11c492d..65699c4 100644 --- a/src/views/bpm/form/index.vue +++ b/src/views/bpm/form/index.vue @@ -1,5 +1,4 @@ <template> - <ContentWrap> <!-- 搜索工作栏 --> <el-form @@ -142,8 +141,9 @@ const toRouter: { name: string; query?: { id: number } } = { name: 'BpmFormEditor' } + console.log(typeof id) // 表单新建的时候id传的是event需要排除 - if (typeof id === 'number') { + if (typeof id === 'number' || typeof id === 'string') { toRouter.query = { id } diff --git a/src/views/bpm/group/UserGroupForm.vue b/src/views/bpm/group/UserGroupForm.vue index ac0cfcb..3c825eb 100644 --- a/src/views/bpm/group/UserGroupForm.vue +++ b/src/views/bpm/group/UserGroupForm.vue @@ -28,7 +28,7 @@ <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" - :label="dict.value" + :value="dict.value" > {{ dict.label }} </el-radio> diff --git a/src/views/bpm/model/CategoryDraggableModel.vue b/src/views/bpm/model/CategoryDraggableModel.vue new file mode 100644 index 0000000..f3b5a42 --- /dev/null +++ b/src/views/bpm/model/CategoryDraggableModel.vue @@ -0,0 +1,511 @@ +<template> + <div class="flex items-center h-50px"> + <!-- 头部:分类名 --> + <div class="flex items-center"> + <el-tooltip content="拖动排序" v-if="isCategorySorting"> + <Icon + :size="22" + icon="ic:round-drag-indicator" + class="ml-10px category-drag-icon cursor-move text-#8a909c" + /> + </el-tooltip> + <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3> + <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div> + </div> + <!-- 头部:操作 --> + <div class="flex-1 flex" v-if="!isCategorySorting"> + <div + v-if="categoryInfo.modelList.length > 0" + class="ml-20px flex items-center" + :class="[ + 'transition-transform duration-300 cursor-pointer', + isExpand ? 'rotate-180' : 'rotate-0' + ]" + @click="isExpand = !isExpand" + > + <Icon icon="ep:arrow-down-bold" color="#999" /> + </div> + <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'"> + <template v-if="!isModelSorting"> + <el-button + v-if="categoryInfo.modelList.length > 0" + link + type="info" + class="mr-20px" + @click.stop="handleModelSort" + > + <Icon icon="fa:sort-amount-desc" class="mr-5px" /> + 排序 + </el-button> + <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')"> + <Icon icon="fa:plus" class="mr-5px" /> + 新建 + </el-button> + <el-dropdown + @command="(command) => handleCategoryCommand(command, categoryInfo)" + placement="bottom" + > + <el-button link type="info"> + <Icon icon="ep:setting" class="mr-5px" /> + 分类 + </el-button> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item> + <el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </template> + <template v-else> + <el-button @click.stop="handleModelSortCancel"> 取 消 </el-button> + <el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button> + </template> + </div> + </div> + </div> + + <!-- 模型列表 --> + <el-collapse-transition> + <div v-show="isExpand"> + <el-table + :class="categoryInfo.name" + ref="tableRef" + :header-cell-style="{ backgroundColor: isDark ? '' : '#edeff0', paddingLeft: '10px' }" + :cell-style="{ paddingLeft: '10px' }" + :row-style="{ height: '68px' }" + :data="modelList" + row-key="id" + > + <el-table-column label="流程名" prop="name" min-width="150"> + <template #default="scope"> + <div class="flex items-center"> + <el-tooltip content="拖动排序" v-if="isModelSorting"> + <Icon + icon="ic:round-drag-indicator" + class="drag-icon cursor-move text-#8a909c mr-10px" + /> + </el-tooltip> + <el-image :src="scope.row.icon" class="h-38px w-38px mr-10px rounded" /> + {{ scope.row.name }} + </div> + </template> + </el-table-column> + <el-table-column label="可见范围" prop="startUserIds" min-width="150"> + <template #default="scope"> + <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0"> + 全部可见 + </el-text> + <el-text v-else-if="scope.row.startUsers.length == 1"> + {{ scope.row.startUsers[0].nickname }} + </el-text> + <el-text v-else> + <el-tooltip + class="box-item" + effect="dark" + placement="top" + :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')" + > + {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见 + </el-tooltip> + </el-text> + </template> + </el-table-column> + <el-table-column label="表单信息" prop="formType" min-width="150"> + <template #default="scope"> + <el-button + v-if="scope.row.formType === BpmModelFormType.NORMAL" + type="primary" + link + @click="handleFormDetail(scope.row)" + > + <span>{{ scope.row.formName }}</span> + </el-button> + <el-button + v-else-if="scope.row.formType === BpmModelFormType.CUSTOM" + type="primary" + link + @click="handleFormDetail(scope.row)" + > + <span>{{ scope.row.formCustomCreatePath }}</span> + </el-button> + <label v-else>暂无表单</label> + </template> + </el-table-column> + <el-table-column label="最后发布" prop="deploymentTime" min-width="250"> + <template #default="scope"> + <div class="flex items-center"> + <span v-if="scope.row.processDefinition" class="w-150px"> + {{ formatDate(scope.row.processDefinition.deploymentTime) }} + </span> + <el-tag v-if="scope.row.processDefinition"> + v{{ scope.row.processDefinition.version }} + </el-tag> + <el-tag v-else type="warning">未部署</el-tag> + <el-tag + v-if="scope.row.processDefinition?.suspensionState === 2" + type="warning" + class="ml-10px" + > + 已停用 + </el-tag> + </div> + </template> + </el-table-column> + <el-table-column label="操作" width="200" fixed="right"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openModelForm('update', scope.row.id)" + v-hasPermi="['bpm:model:update']" + :disabled="!isManagerUser(scope.row)" + > + 修改 + </el-button> + <el-button + link + class="!ml-5px" + type="primary" + @click="handleDeploy(scope.row)" + v-hasPermi="['bpm:model:deploy']" + :disabled="!isManagerUser(scope.row)" + > + 发布 + </el-button> + <el-dropdown + class="!align-middle ml-5px" + @command="(command) => handleModelCommand(command, scope.row)" + v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']" + > + <el-button type="primary" link>更多</el-button> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item + command="handleDefinitionList" + v-if="checkPermi(['bpm:process-definition:query'])" + > + 历史 + </el-dropdown-item> + <el-dropdown-item + command="handleChangeState" + v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition" + :disabled="!isManagerUser(scope.row)" + > + {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }} + </el-dropdown-item> + <el-dropdown-item + type="danger" + command="handleDelete" + v-if="checkPermi(['bpm:model:delete'])" + :disabled="!isManagerUser(scope.row)" + > + 删除 + </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </template> + </el-table-column> + </el-table> + </div> + </el-collapse-transition> + + <!-- 弹窗:重命名分类 --> + <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400"> + <template #title> + <div class="pl-10px font-bold text-18px"> 重命名分类 </div> + </template> + <div class="px-30px"> + <el-input v-model="renameCategoryForm.name" /> + </div> + <template #footer> + <div class="pr-25px pb-25px"> + <el-button @click="renameCategoryVisible = false">取 消</el-button> + <el-button type="primary" @click="handleRenameConfirm">确 定</el-button> + </div> + </template> + </Dialog> + + <!-- 表单弹窗:添加流程模型 --> + <ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" /> +</template> + +<script lang="ts" setup> +import ModelForm from './ModelForm.vue' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' +import Sortable from 'sortablejs' +import { propTypes } from '@/utils/propTypes' +import { formatDate } from '@/utils/formatTime' +import * as ModelApi from '@/api/bpm/model' +import * as FormApi from '@/api/bpm/form' +import { setConfAndFields2 } from '@/utils/formCreate' +import { BpmModelFormType } from '@/utils/constants' +import { checkPermi } from '@/utils/permission' +import { useUserStoreWithOut } from '@/store/modules/user' +import { useAppStore } from '@/store/modules/app' +import { cloneDeep } from 'lodash-es' + +defineOptions({ name: 'BpmModel' }) + +const props = defineProps({ + categoryInfo: propTypes.object.def([]), // 分类后的数据 + isCategorySorting: propTypes.bool.def(false) // 是否分类在排序 +}) +const emit = defineEmits(['success']) +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 +const { push } = useRouter() // 路由 +const userStore = useUserStoreWithOut() // 用户信息缓存 +const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式 + +const isModelSorting = ref(false) // 是否正处于排序状态 +const originalData: any = ref([]) // 原始数据 +const modelList: any = ref([]) // 模型列表 +const isExpand = ref(false) // 是否处于展开状态 + +/** '更多'操作按钮 */ +const handleModelCommand = (command: string, row: any) => { + switch (command) { + case 'handleDefinitionList': + handleDefinitionList(row) + break + case 'handleDelete': + handleDelete(row) + break + case 'handleChangeState': + handleChangeState(row) + break + default: + break + } +} + +/** '分类'操作按钮 */ +const handleCategoryCommand = async (command: string, row: any) => { + switch (command) { + case 'handleRename': + renameCategoryForm.value = await CategoryApi.getCategory(row.id) + renameCategoryVisible.value = true + break + case 'handleDeleteCategory': + await handleDeleteCategory() + break + default: + break + } +} + +/** 删除按钮操作 */ +const handleDelete = async (row: any) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await ModelApi.deleteModel(row.id) + message.success(t('common.delSuccess')) + // 刷新列表 + emit('success') + } catch {} +} + +/** 更新状态操作 */ +const handleChangeState = async (row: any) => { + const state = row.processDefinition.suspensionState + const newState = state === 1 ? 2 : 1 + try { + // 修改状态的二次确认 + const id = row.id + debugger + const statusState = state === 1 ? '停用' : '启用' + const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?' + await message.confirm(content) + // 发起修改状态 + await ModelApi.updateModelState(id, newState) + message.success(statusState + '成功') + // 刷新列表 + emit('success') + } catch {} +} + +/** 发布流程 */ +const handleDeploy = async (row: any) => { + try { + // 删除的二次确认 + await message.confirm('是否部署该流程!!') + // 发起部署 + await ModelApi.deployModel(row.id) + message.success(t('部署成功')) + // 刷新列表 + emit('success') + } catch {} +} + +/** 跳转到指定流程定义列表 */ +const handleDefinitionList = (row: any) => { + push({ + name: 'BpmProcessDefinition', + query: { + key: row.key + } + }) +} + +/** 流程表单的详情按钮操作 */ +const formDetailVisible = ref(false) +const formDetailPreview = ref({ + rule: [], + option: {} +}) +const handleFormDetail = async (row: any) => { + if (row.formType == 10) { + // 设置表单 + const data = await FormApi.getForm(row.formId) + setConfAndFields2(formDetailPreview, data.conf, data.fields) + // 弹窗打开 + formDetailVisible.value = true + } else { + await push({ + path: row.formCustomCreatePath + }) + } +} + +/** 判断是否可以操作 */ +const isManagerUser = (row: any) => { + const userId = userStore.getUser.id + return row.managerUserIds && row.managerUserIds.includes(userId) +} + +/** 处理模型的排序 **/ +const handleModelSort = () => { + // 保存初始数据 + originalData.value = cloneDeep(props.categoryInfo.modelList) + isModelSorting.value = true + initSort() +} + +/** 处理模型的排序提交 */ +const handleModelSortSubmit = async () => { + // 保存排序 + const ids = modelList.value.map((item: any) => item.id) + await ModelApi.updateModelSortBatch(ids) + // 刷新列表 + isModelSorting.value = false + message.success('排序模型成功') + emit('success') +} + +/** 处理模型的排序取消 */ +const handleModelSortCancel = () => { + // 恢复初始数据 + modelList.value = cloneDeep(originalData.value) + isModelSorting.value = false +} + +/** 创建拖拽实例 */ +const tableRef = ref() +const initSort = () => { + const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`) + Sortable.create(table, { + group: 'shared', + animation: 150, + draggable: '.el-table__row', + handle: '.drag-icon', + // 结束拖动事件 + onEnd: ({ newDraggableIndex, oldDraggableIndex }) => { + if (oldDraggableIndex !== newDraggableIndex) { + modelList.value.splice( + newDraggableIndex, + 0, + modelList.value.splice(oldDraggableIndex, 1)[0] + ) + } + } + }) +} + +/** 更新 modelList 模型列表 */ +const updateModeList = () => { + modelList.value = cloneDeep(props.categoryInfo.modelList) + if (props.categoryInfo.modelList.length > 0) { + isExpand.value = true + } +} + +/** 重命名弹窗确定 */ +const renameCategoryVisible = ref(false) +const renameCategoryForm = ref({ + name: '' +}) +const handleRenameConfirm = async () => { + if (renameCategoryForm.value?.name.length === 0) { + return message.warning('请输入名称') + } + // 发起修改 + await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO) + message.success('重命名成功') + // 刷新列表 + renameCategoryVisible.value = false + emit('success') +} + +/** 删除分类 */ +const handleDeleteCategory = async () => { + try { + if (props.categoryInfo.modelList.length > 0) { + return message.warning('该分类下仍有流程定义,不允许删除') + } + await message.confirm('确认删除分类吗?') + // 发起删除 + await CategoryApi.deleteCategory(props.categoryInfo.id) + message.success(t('common.delSuccess')) + // 刷新列表 + emit('success') + } catch {} +} + +/** 添加流程模型弹窗 */ +const modelFormRef = ref() +const openModelForm = (type: string, id?: number) => { + if (type === 'create') { + push({ name: 'BpmModelCreate' }) + } else { + push({ + name: 'BpmModelUpdate', + params: { id } + }) + } +} + +watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true }) +watch( + () => props.isCategorySorting, + (val) => { + if (val) isExpand.value = false + }, + { immediate: true } +) +</script> + +<style lang="scss"> +.rename-dialog.el-dialog { + padding: 0 !important; + + .el-dialog__header { + border-bottom: none; + } + + .el-dialog__footer { + border-top: none !important; + } +} +</style> +<style lang="scss" scoped> +:deep() { + .el-table__cell { + overflow: hidden; + border-bottom: none !important; + } +} +</style> diff --git a/src/views/bpm/model/ModelForm.vue b/src/views/bpm/model/ModelForm.vue index ce60edc..095b0ac 100644 --- a/src/views/bpm/model/ModelForm.vue +++ b/src/views/bpm/model/ModelForm.vue @@ -8,12 +8,7 @@ label-width="110px" > <el-form-item label="流程标识" prop="key"> - <el-input - v-model="formData.key" - :disabled="!!formData.id" - placeholder="请输入流标标识" - style="width: 330px" - /> + <el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" /> <el-tooltip v-if="!formData.id" class="item" @@ -35,7 +30,7 @@ placeholder="请输入流程名称" /> </el-form-item> - <el-form-item v-if="formData.id" label="流程分类" prop="category"> + <el-form-item label="流程分类" prop="category"> <el-select v-model="formData.category" clearable @@ -50,120 +45,223 @@ /> </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 label="流程图标" prop="icon"> + <UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" /> </el-form-item> <el-form-item label="流程描述" prop="description"> <el-input v-model="formData.description" clearable type="textarea" /> </el-form-item> - <div v-if="formData.id"> - <el-form-item label="表单类型" prop="formType"> - <el-radio-group v-model="formData.formType"> - <el-radio - v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" - :key="dict.value" - :label="dict.value" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId"> - <el-select v-model="formData.formId" clearable style="width: 100%"> - <el-option - v-for="form in formList" - :key="form.id" - :label="form.name" - :value="form.id" + <el-form-item label="流程类型" prop="type"> + <el-radio-group v-model="formData.type"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="表单类型" prop="formType"> + <el-radio-group v-model="formData.formType"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" + :key="dict.value" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId"> + <el-select v-model="formData.formId" clearable style="width: 100%"> + <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> + </el-select> + </el-form-item> + <el-form-item + v-if="formData.formType === 20" + label="表单提交路由" + prop="formCustomCreatePath" + > + <el-input + v-model="formData.formCustomCreatePath" + placeholder="请输入表单提交路由" + style="width: 330px" + /> + <el-tooltip + class="item" + content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue" + effect="light" + placement="top" + > + <i class="el-icon-question" style="padding-left: 5px"></i> + </el-tooltip> + </el-form-item> + <el-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath"> + <el-input + v-model="formData.formCustomViewPath" + placeholder="请输入表单查看的组件地址" + style="width: 330px" + /> + <el-tooltip + class="item" + content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue" + effect="light" + placement="top" + > + <i class="el-icon-question" style="padding-left: 5px"></i> + </el-tooltip> + </el-form-item> + <el-form-item label="是否可见" prop="visible"> + <el-radio-group v-model="formData.visible"> + <el-radio + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" + :key="dict.value as string" + :label="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="谁可以发起" prop="startUserType"> + <el-select + v-model="formData.startUserType" + placeholder="请选择谁可以发起" + @change="handleStartUserTypeChange" + > + <el-option label="全员" :value="0" /> + <el-option label="指定人员" :value="1" /> + <el-option label="均不可提交" :value="2" /> + </el-select> + <div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2"> + <div + v-for="user in selectedStartUsers" + :key="user.id" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + <Icon + icon="ep:close" + class="ml-2 cursor-pointer hover:text-red-500" + @click="handleRemoveStartUser(user)" /> - </el-select> - </el-form-item> - <el-form-item - v-if="formData.formType === 20" - label="表单提交路由" - prop="formCustomCreatePath" + </div> + <el-button type="primary" link @click="openStartUserSelect"> + <Icon icon="ep:plus" />选择人员 + </el-button> + </div> + </el-form-item> + <el-form-item label="流程管理员" prop="managerUserType"> + <el-select + v-model="formData.managerUserType" + placeholder="请选择流程管理员" + @change="handleManagerUserTypeChange" > - <el-input - v-model="formData.formCustomCreatePath" - placeholder="请输入表单提交路由" - style="width: 330px" - /> - <el-tooltip - class="item" - content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create" - effect="light" - placement="top" + <el-option label="全员" :value="0" /> + <el-option label="指定人员" :value="1" /> + <el-option label="均不可提交" :value="2" /> + </el-select> + <div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2"> + <div + v-for="user in selectedManagerUsers" + :key="user.id" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" > - <i class="el-icon-question" style="padding-left: 5px"></i> - </el-tooltip> - </el-form-item> - <el-form-item - v-if="formData.formType === 20" - label="表单查看地址" - prop="formCustomViewPath" - > - <el-input - v-model="formData.formCustomViewPath" - placeholder="请输入表单查看的组件地址" - style="width: 330px" - /> - <el-tooltip - class="item" - content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail" - effect="light" - placement="top" - > - <i class="el-icon-question" style="padding-left: 5px"></i> - </el-tooltip> - </el-form-item> - </div> + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + <Icon + icon="ep:close" + class="ml-2 cursor-pointer hover:text-red-500" + @click="handleRemoveManagerUser(user)" + /> + </div> + <el-button type="primary" link @click="openManagerUserSelect"> + <Icon icon="ep:plus" />选择人员 + </el-button> + </div> + </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> + <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> </template> <script lang="ts" setup> -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { propTypes } from '@/utils/propTypes' +import { DICT_TYPE, getBoolDictOptions, 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' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' +import { BpmModelFormType, BpmModelType } from '@/utils/constants' +import { UserVO } from '@/api/system/user' +import * as UserApi from '@/api/system/user' +import { useUserStoreWithOut } from '@/store/modules/user' +import { FormVO } from '@/api/bpm/form' defineOptions({ name: 'ModelForm' }) const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 - +const userStore = useUserStoreWithOut() // 用户信息缓存 +const props = defineProps({ + categoryId: propTypes.number +}) const dialogVisible = ref(false) // 弹窗的是否展示 const dialogTitle = ref('') // 弹窗的标题 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formType = ref('') // 表单的类型:create - 新增;update - 修改 -const formData = ref({ - formType: 10, +const formData: any = ref({ + id: undefined, name: '', + key: '', category: undefined, icon: undefined, description: '', + type: BpmModelType.BPMN, + formType: BpmModelFormType.NORMAL, formId: '', formCustomCreatePath: '', - formCustomViewPath: '' + formCustomViewPath: '', + visible: true, + startUserType: undefined, + managerUserType: undefined, + startUserIds: [], + managerUserIds: [] }) const formRules = reactive({ - 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' }] + name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }], + key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], + category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], + icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }], + type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], + formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], + formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }], + formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }], + formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }], + visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], + managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref -const formList = ref([]) // 流程表单的下拉框的数据 -const categoryList = ref([]) // 流程分类列表 +const formList = ref<FormVO[]>([]) // 流程表单的下拉框的数据 +const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 +const userList = ref<UserVO[]>([]) // 用户列表 +const selectedStartUsers = ref<UserVO[]>([]) // 已选择的发起人列表 +const selectedManagerUsers = ref<UserVO[]>([]) // 已选择的管理员列表 +const userSelectFormRef = ref() // 用户选择弹窗 ref +const currentSelectType = ref<'start' | 'manager'>('start') // 当前选择的是发起人还是管理员 /** 打开弹窗 */ -const open = async (type: string, id?: number) => { +const open = async (type: string, id?: string) => { dialogVisible.value = true dialogTitle.value = t('action.' + type) formType.value = type @@ -176,11 +274,31 @@ } finally { formLoading.value = false } + // 加载数据时,根据已有的用户ID列表初始化已选用户 + if (formData.value.startUserIds?.length) { + formData.value.startUserType = 1 + selectedStartUsers.value = userList.value.filter((user) => + formData.value.startUserIds.includes(user.id) + ) + } + if (formData.value.managerUserIds?.length) { + formData.value.managerUserType = 1 + selectedManagerUsers.value = userList.value.filter((user) => + formData.value.managerUserIds.includes(user.id) + ) + } + } else { + formData.value.managerUserIds.push(userStore.getUser.id) } // 获得流程表单的下拉框的数据 formList.value = await FormApi.getFormSimpleList() // 查询流程分类列表 categoryList.value = await CategoryApi.getCategorySimpleList() + // 查询用户列表 + userList.value = await UserApi.getSimpleUserList() + if (props.categoryId) { + formData.value.category = props.categoryId + } } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 @@ -199,11 +317,10 @@ await ModelApi.createModel(data) // 提示,引导用户做后续的操作 await ElMessageBox.alert( - '<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' + - '<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' + - '<div>2. 点击【设计流程】按钮,绘制流程图</div>' + - '<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' + - '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', + '<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' + + '<div>1. 点击【设计流程】按钮,绘制流程图</div>' + + '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' + + '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', '重要提示', { dangerouslyUseHTMLString: true, @@ -225,15 +342,99 @@ /** 重置表单 */ const resetForm = () => { formData.value = { - formType: 10, + id: undefined, name: '', + key: '', category: undefined, - icon: '', + icon: undefined, description: '', + type: BpmModelType.BPMN, + formType: BpmModelFormType.NORMAL, formId: '', formCustomCreatePath: '', - formCustomViewPath: '' + formCustomViewPath: '', + visible: true, + startUserType: undefined, + managerUserType: undefined, + startUserIds: [], + managerUserIds: [] } formRef.value?.resetFields() + selectedStartUsers.value = [] + selectedManagerUsers.value = [] +} + +/** 处理发起人类型变化 */ +const handleStartUserTypeChange = (value: number) => { + if (value !== 1) { + selectedStartUsers.value = [] + formData.value.startUserIds = [] + } +} + +/** 处理管理员类型变化 */ +const handleManagerUserTypeChange = (value: number) => { + if (value !== 1) { + selectedManagerUsers.value = [] + formData.value.managerUserIds = [] + } +} + +/** 打开发起人选择 */ +const openStartUserSelect = () => { + currentSelectType.value = 'start' + userSelectFormRef.value.open(0, selectedStartUsers.value) +} + +/** 打开管理员选择 */ +const openManagerUserSelect = () => { + currentSelectType.value = 'manager' + userSelectFormRef.value.open(0, selectedManagerUsers.value) +} + +/** 处理用户选择确认 */ +const handleUserSelectConfirm = (_, users: UserVO[]) => { + if (currentSelectType.value === 'start') { + selectedStartUsers.value = users + formData.value.startUserIds = users.map((u) => u.id) + } else { + selectedManagerUsers.value = users + formData.value.managerUserIds = users.map((u) => u.id) + } +} + +/** 移除发起人 */ +const handleRemoveStartUser = (user: UserVO) => { + selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id) + formData.value.startUserIds = formData.value.startUserIds.filter((id: number) => id !== user.id) +} + +/** 移除管理员 */ +const handleRemoveManagerUser = (user: UserVO) => { + selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id) + formData.value.managerUserIds = formData.value.managerUserIds.filter( + (id: number) => id !== user.id + ) } </script> + +<style lang="scss" scoped> +.bg-gray-100 { + background-color: #f5f7fa; + transition: all 0.3s; + + &:hover { + background-color: #e6e8eb; + } + + .ep-close { + font-size: 14px; + color: #909399; + transition: color 0.3s; + + &:hover { + color: #f56c6c; + } + } +} +</style> diff --git a/src/views/bpm/model/ModelImportForm.vue b/src/views/bpm/model/ModelImportForm.vue deleted file mode 100644 index 9a91e1d..0000000 --- a/src/views/bpm/model/ModelImportForm.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="导入流程" width="400"> - <div> - <el-upload - ref="uploadRef" - v-model:file-list="fileList" - :action="importUrl" - :auto-upload="false" - :data="formData" - :disabled="formLoading" - :headers="uploadHeaders" - :limit="1" - :on-error="submitFormError" - :on-exceed="handleExceed" - :on-success="submitFormSuccess" - accept=".bpmn, .xml" - drag - name="bpmnFile" - > - <Icon class="el-icon--upload" icon="ep:upload-filled" /> - <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div> - <template #tip> - <div class="el-upload__tip" style="color: red"> - 提示:仅允许导入“bpm”或“xml”格式文件! - </div> - <div> - <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px"> - <el-form-item label="流程标识" prop="key"> - <el-input - v-model="formData.key" - placeholder="请输入流标标识" - style="width: 250px" - /> - </el-form-item> - <el-form-item label="流程名称" prop="name"> - <el-input v-model="formData.name" clearable placeholder="请输入流程名称" /> - </el-form-item> - <el-form-item label="流程描述" prop="description"> - <el-input v-model="formData.description" clearable type="textarea" /> - </el-form-item> - </el-form> - </div> - </template> - </el-upload> - </div> - <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 { getAccessToken, getTenantId } from '@/utils/auth' - -defineOptions({ name: 'ModelImportForm' }) - -const message = useMessage() // 消息弹窗 - -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - key: '', - name: '', - description: '' -}) -const formRules = reactive({ - key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], - name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }] -}) -const formRef = ref() // 表单 Ref -const uploadRef = ref() // 上传 Ref -const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import' -const uploadHeaders = ref() // 上传 Header 头 -const fileList = ref([]) // 文件列表 - -/** 打开弹窗 */ -const open = async () => { - dialogVisible.value = true - resetForm() -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - -/** 提交表单 */ -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - if (fileList.value.length == 0) { - message.error('请上传文件') - return - } - // 提交请求 - uploadHeaders.value = { - Authorization: 'Bearer ' + getAccessToken(), - 'tenant-id': getTenantId() - } - formLoading.value = true - uploadRef.value!.submit() -} - -/** 文件上传成功 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitFormSuccess = async (response: any) => { - if (response.code !== 0) { - message.error(response.msg) - formLoading.value = false - return - } - // 提示成功 - message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】') - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') -} - -/** 上传错误提示 */ -const submitFormError = (): void => { - message.error('导入流程失败,请您重新上传!') - formLoading.value = false -} - -/** 重置表单 */ -const resetForm = () => { - // 重置上传状态和文件 - formLoading.value = false - uploadRef.value?.clearFiles() - // 重置表单 - formData.value = { - key: '', - name: '', - description: '' - } - formRef.value?.resetFields() -} - -/** 文件数超出提示 */ -const handleExceed = (): void => { - message.error('最多只能上传一个文件!') -} -</script> diff --git a/src/views/bpm/model/editor/index.vue b/src/views/bpm/model/editor/index.vue index 29bca71..37eff73 100644 --- a/src/views/bpm/model/editor/index.vue +++ b/src/views/bpm/model/editor/index.vue @@ -3,7 +3,6 @@ <!-- 流程设计器,负责绘制流程等 --> <MyProcessDesigner key="designer" - v-if="xmlString !== undefined" v-model="xmlString" :value="xmlString" v-bind="controlForm" @@ -11,12 +10,14 @@ ref="processDesigner" @init-finished="initModeler" :additionalModel="controlForm.additionalModel" + :model="model" @save="save" /> <!-- 流程属性器,负责编辑每个流程节点的属性 --> <MyProcessPenal + v-if="isModelerReady && modeler" key="penal" - :bpmnModeler="modeler as any" + :bpmnModeler="modeler" :prefix="controlForm.prefix" class="process-panel" :model="model" @@ -34,12 +35,26 @@ defineOptions({ name: 'BpmModelEditor' }) -const router = useRouter() // 路由 -const { query } = useRoute() // 路由的查询 +const props = defineProps<{ + modelId?: string + modelKey?: string + modelName?: string + value?: string +}>() + +const emit = defineEmits(['success', 'init-finished']) const message = useMessage() // 国际化 -const xmlString = ref(undefined) // BPMN XML -const modeler = ref(null) // BPMN Modeler +// 表单信息 +const formFields = ref<string[]>([]) +const formType = ref(20) +provide('formFields', formFields) +provide('formType', formType) + +const xmlString = ref<string>('') // BPMN XML +const modeler = shallowRef() // BPMN Modeler +const processDesigner = ref() +const isModelerReady = ref(false) const controlForm = ref({ simulation: true, labelEditing: false, @@ -50,66 +65,215 @@ }) const model = ref<ModelApi.ModelVO>() // 流程模型的信息 +// 初始化 bpmnInstances +const initBpmnInstances = () => { + if (!modeler.value) return false + try { + const instances = { + modeler: modeler.value, + modeling: modeler.value.get('modeling'), + moddle: modeler.value.get('moddle'), + eventBus: modeler.value.get('eventBus'), + bpmnFactory: modeler.value.get('bpmnFactory'), + elementFactory: modeler.value.get('elementFactory'), + elementRegistry: modeler.value.get('elementRegistry'), + replace: modeler.value.get('replace'), + selection: modeler.value.get('selection') + } + + // 检查所有实例是否都存在 + return Object.values(instances).every((instance) => instance) + } catch (error) { + console.error('初始化 bpmnInstances 失败:', error) + return false + } +} + /** 初始化 modeler */ -const initModeler = (item) => { - setTimeout(() => { +const initModeler = async (item) => { + try { modeler.value = item - }, 10) + // 等待 modeler 初始化完成 + await nextTick() + + // 确保 modeler 的所有实例都已经准备好 + if (initBpmnInstances()) { + isModelerReady.value = true + emit('init-finished') + + // 初始化完成后,设置初始值 + if (props.modelId) { + // 编辑模式 + const data = await ModelApi.getModel(props.modelId) + model.value = { + ...data, + bpmnXml: undefined // 清空 bpmnXml 属性 + } + xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name) + } else if (props.modelKey && props.modelName) { + // 新建模式 + xmlString.value = props.value || getDefaultBpmnXml(props.modelKey, props.modelName) + model.value = { + key: props.modelKey, + name: props.modelName + } as ModelApi.ModelVO + } + + // 导入XML并刷新视图 + await nextTick() + try { + await modeler.value.importXML(xmlString.value) + if (processDesigner.value?.refresh) { + processDesigner.value.refresh() + } + } catch (error) { + console.error('导入XML失败:', error) + } + } else { + console.error('modeler 实例未完全初始化') + } + } catch (error) { + console.error('初始化 modeler 失败:', error) + } +} + +/** 获取默认的BPMN XML */ +const getDefaultBpmnXml = (key: string, name: string) => { + return `<?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="${key}" name="${name}" isExecutable="true" /> + <bpmndi:BPMNDiagram id="BPMNDiagram"> + <bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" /> + </bpmndi:BPMNDiagram> +</definitions>` } /** 添加/修改模型 */ -const save = async (bpmnXml) => { - const data = { - ...model.value, - bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得 - } as unknown as ModelApi.ModelVO - // 提交 - if (data.id) { - await ModelApi.updateModel(data) - message.success('修改成功') - } else { - await ModelApi.createModel(data) - message.success('新增成功') +const save = async (bpmnXml: string) => { + try { + xmlString.value = bpmnXml + if (props.modelId) { + // 编辑模式 + const data = { + ...model.value, + bpmnXml: bpmnXml + } as unknown as ModelApi.ModelVO + await ModelApi.updateModelBpmn(data) + emit('success') + } else { + // 新建模式,直接返回XML + emit('success', bpmnXml) + } + } catch (error) { + console.error('保存失败:', error) + message.error('保存失败') } - // 跳转回去 - close() } -/** 关闭按钮 */ -const close = () => { - router.push({ path: '/bpm/manager/model' }) +// 监听 key、name 和 value 的变化 +watch( + [() => props.modelKey, () => props.modelName, () => props.value], + async ([newKey, newName, newValue]) => { + if (!props.modelId && isModelerReady.value) { + let shouldRefresh = false + + if (newKey && newName) { + const newXml = newValue || getDefaultBpmnXml(newKey, newName) + if (newXml !== xmlString.value) { + xmlString.value = newXml + shouldRefresh = true + } + model.value = { + ...model.value, + key: newKey, + name: newName + } as ModelApi.ModelVO + } else if (newValue && newValue !== xmlString.value) { + xmlString.value = newValue + shouldRefresh = true + } + + if (shouldRefresh) { + // 确保更新后重新渲染 + await nextTick() + if (processDesigner.value?.refresh) { + try { + await modeler.value?.importXML(xmlString.value) + processDesigner.value.refresh() + } catch (error) { + console.error('导入XML失败:', error) + } + } + } + } + }, + { deep: true } +) + +// 在组件卸载时清理 +onBeforeUnmount(() => { + isModelerReady.value = false + modeler.value = null + // 清理全局实例 + const w = window as any + if (w.bpmnInstances) { + w.bpmnInstances = null + } +}) + +/** 获取 XML 字符串 */ +const saveXML = async () => { + if (!modeler.value) { + return { xml: xmlString.value } + } + try { + const result = await modeler.value.saveXML({ format: true }) + xmlString.value = result.xml + return result + } catch (error) { + console.error('获取XML失败:', error) + return { xml: xmlString.value } + } } -/** 初始化 */ -onMounted(async () => { - const modelId = query.modelId as unknown as number - if (!modelId) { - message.error('缺少模型 modelId 编号') - return +/** 获取SVG字符串 */ +const saveSVG = async () => { + if (!modeler.value) { + return { svg: undefined } } - // 查询模型 - const data = await ModelApi.getModel(modelId) - 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>` + try { + return await modeler.value.saveSVG() + } catch (error) { + console.error('获取SVG失败:', error) + return { svg: undefined } } - model.value = { - ...data, - bpmnXml: undefined // 清空 bpmnXml 属性 +} + +/** 刷新视图 */ +const refresh = async () => { + if (processDesigner.value?.refresh && modeler.value) { + try { + await modeler.value.importXML(xmlString.value) + processDesigner.value.refresh() + } catch (error) { + console.error('刷新视图失败:', error) + } } - xmlString.value = data.bpmnXml +} + +// 暴露必要的属性和方法给父组件 +defineExpose({ + modeler, + isModelerReady, + saveXML, + saveSVG, + refresh }) </script> <style lang="scss"> .process-panel__container { position: absolute; - top: 90px; - right: 60px; + top: 172px; + right: 70px; } </style> diff --git a/src/views/bpm/model/form/BasicInfo.vue b/src/views/bpm/model/form/BasicInfo.vue new file mode 100644 index 0000000..0359ea8 --- /dev/null +++ b/src/views/bpm/model/form/BasicInfo.vue @@ -0,0 +1,301 @@ +<template> + <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> + <el-form-item label="流程标识" prop="key" class="mb-20px"> + <div class="flex items-center"> + <el-input + class="!w-440px" + v-model="modelData.key" + :disabled="!!modelData.id" + placeholder="请输入流标标识" + /> + <el-tooltip + class="item" + :content="modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'" + effect="light" + placement="top" + > + <Icon icon="ep:question-filled" class="ml-5px" /> + </el-tooltip> + </div> + </el-form-item> + <el-form-item label="流程名称" prop="name" class="mb-20px"> + <el-input + v-model="modelData.name" + :disabled="!!modelData.id" + clearable + placeholder="请输入流程名称" + /> + </el-form-item> + <el-form-item label="流程分类" prop="category" class="mb-20px"> + <el-select + class="!w-full" + v-model="modelData.category" + clearable + placeholder="请选择流程分类" + > + <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="icon" class="mb-20px"> + <UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" /> + </el-form-item> + <el-form-item label="流程描述" prop="description" class="mb-20px"> + <el-input v-model="modelData.description" clearable type="textarea" /> + </el-form-item> + <el-form-item label="流程类型" prop="type" class="mb-20px"> + <el-radio-group v-model="modelData.type"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)" + :key="dict.value" + :value="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="是否可见" prop="visible" class="mb-20px"> + <el-radio-group v-model="modelData.visible"> + <el-radio + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" + :key="dict.value" + :value="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="谁可以发起" prop="startUserType" class="mb-20px"> + <el-select + v-model="modelData.startUserType" + placeholder="请选择谁可以发起" + @change="handleStartUserTypeChange" + > + <el-option label="全员" :value="0" /> + <el-option label="指定人员" :value="1" /> + <el-option label="均不可提交" :value="2" /> + </el-select> + <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2"> + <div + v-for="user in selectedStartUsers" + :key="user.id" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + <Icon + icon="ep:close" + class="ml-2 cursor-pointer hover:text-red-500" + @click="handleRemoveStartUser(user)" + /> + </div> + <el-button type="primary" link @click="openStartUserSelect"> + <Icon icon="ep:plus" />选择人员 + </el-button> + </div> + </el-form-item> + <el-form-item label="流程管理员" prop="managerUserType" class="mb-20px"> + <el-select + v-model="modelData.managerUserType" + placeholder="请选择流程管理员" + @change="handleManagerUserTypeChange" + > + <el-option label="全员" :value="0" /> + <el-option label="指定人员" :value="1" /> + <el-option label="均不可提交" :value="2" /> + </el-select> + <div v-if="modelData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2"> + <div + v-for="user in selectedManagerUsers" + :key="user.id" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + <Icon + icon="ep:close" + class="ml-2 cursor-pointer hover:text-red-500" + @click="handleRemoveManagerUser(user)" + /> + </div> + <el-button type="primary" link @click="openManagerUserSelect"> + <Icon icon="ep:plus" />选择人员 + </el-button> + </div> + </el-form-item> + </el-form> + + <!-- 用户选择弹窗 --> + <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> +</template> + +<script lang="ts" setup> +import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict' +import { UserVO } from '@/api/system/user' + +const props = defineProps({ + modelValue: { + type: Object, + required: true + }, + categoryList: { + type: Array, + required: true + }, + userList: { + type: Array, + required: true + } +}) + +const emit = defineEmits(['update:modelValue']) + +const formRef = ref() +const selectedStartUsers = ref<UserVO[]>([]) +const selectedManagerUsers = ref<UserVO[]>([]) +const userSelectFormRef = ref() +const currentSelectType = ref<'start' | 'manager'>('start') + +const rules = { + name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }], + key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], + category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], + icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }], + type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], + visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], + managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] +} + +// 创建本地数据副本 +const modelData = computed({ + get: () => props.modelValue, + set: (val) => emit('update:modelValue', val) +}) + +// 初始化选中的用户 +watch( + () => props.modelValue, + (newVal) => { + if (newVal.startUserIds?.length) { + selectedStartUsers.value = props.userList.filter((user: UserVO) => + newVal.startUserIds.includes(user.id) + ) as UserVO[] + } + if (newVal.managerUserIds?.length) { + selectedManagerUsers.value = props.userList.filter((user: UserVO) => + newVal.managerUserIds.includes(user.id) + ) as UserVO[] + } + }, + { immediate: true } +) + +/** 打开发起人选择 */ +const openStartUserSelect = () => { + currentSelectType.value = 'start' + userSelectFormRef.value.open(0, selectedStartUsers.value) +} + +/** 打开管理员选择 */ +const openManagerUserSelect = () => { + currentSelectType.value = 'manager' + userSelectFormRef.value.open(0, selectedManagerUsers.value) +} + +/** 处理用户选择确认 */ +const handleUserSelectConfirm = (_, users: UserVO[]) => { + if (currentSelectType.value === 'start') { + selectedStartUsers.value = users + emit('update:modelValue', { + ...modelData.value, + startUserIds: users.map((u) => u.id) + }) + } else { + selectedManagerUsers.value = users + emit('update:modelValue', { + ...modelData.value, + managerUserIds: users.map((u) => u.id) + }) + } +} + +/** 处理发起人类型变化 */ +const handleStartUserTypeChange = (value: number) => { + if (value !== 1) { + selectedStartUsers.value = [] + emit('update:modelValue', { + ...modelData.value, + startUserIds: [] + }) + } +} + +/** 处理管理员类型变化 */ +const handleManagerUserTypeChange = (value: number) => { + if (value !== 1) { + selectedManagerUsers.value = [] + emit('update:modelValue', { + ...modelData.value, + managerUserIds: [] + }) + } +} + +/** 移除发起人 */ +const handleRemoveStartUser = (user: UserVO) => { + selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id) + emit('update:modelValue', { + ...modelData.value, + startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id) + }) +} + +/** 移除管理员 */ +const handleRemoveManagerUser = (user: UserVO) => { + selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id) + emit('update:modelValue', { + ...modelData.value, + managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id) + }) +} + +/** 表单校验 */ +const validate = async () => { + await formRef.value?.validate() +} + +defineExpose({ + validate +}) +</script> + +<style lang="scss" scoped> +.bg-gray-100 { + background-color: #f5f7fa; + transition: all 0.3s; + + &:hover { + background-color: #e6e8eb; + } + + .ep-close { + font-size: 14px; + color: #909399; + transition: color 0.3s; + + &:hover { + color: #f56c6c; + } + } +} +</style> diff --git a/src/views/bpm/model/form/FormDesign.vue b/src/views/bpm/model/form/FormDesign.vue new file mode 100644 index 0000000..98aee6d --- /dev/null +++ b/src/views/bpm/model/form/FormDesign.vue @@ -0,0 +1,137 @@ +<template> + <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> + <el-form-item label="表单类型" prop="formType" class="mb-20px"> + <el-radio-group v-model="modelData.formType"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" + :key="dict.value" + :value="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item v-if="modelData.formType === 10" label="流程表单" prop="formId"> + <el-select v-model="modelData.formId" clearable style="width: 100%"> + <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> + </el-select> + </el-form-item> + <el-form-item v-if="modelData.formType === 20" label="表单提交路由" prop="formCustomCreatePath"> + <el-input + v-model="modelData.formCustomCreatePath" + placeholder="请输入表单提交路由" + style="width: 330px" + /> + <el-tooltip + class="item" + content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue" + effect="light" + placement="top" + > + <Icon icon="ep:question" class="ml-5px" /> + </el-tooltip> + </el-form-item> + <el-form-item v-if="modelData.formType === 20" label="表单查看地址" prop="formCustomViewPath"> + <el-input + v-model="modelData.formCustomViewPath" + placeholder="请输入表单查看的组件地址" + style="width: 330px" + /> + <el-tooltip + class="item" + content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue" + effect="light" + placement="top" + > + <Icon icon="ep:question" class="ml-5px" /> + </el-tooltip> + </el-form-item> + <!-- 表单预览 --> + <div + v-if="modelData.formType === 10 && modelData.formId && formPreview.rule.length > 0" + class="mt-20px" + > + <div class="flex items-center mb-15px"> + <div class="h-15px w-4px bg-[#1890ff] mr-10px"></div> + <span class="text-15px font-bold">表单预览</span> + </div> + <form-create + v-model="formPreview.formData" + :rule="formPreview.rule" + :option="formPreview.option" + /> + </div> + </el-form> +</template> + +<script lang="ts" setup> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import * as FormApi from '@/api/bpm/form' +import { setConfAndFields2 } from '@/utils/formCreate' + +const props = defineProps({ + modelValue: { + type: Object, + required: true + }, + formList: { + type: Array, + required: true + } +}) + +const emit = defineEmits(['update:modelValue']) + +const formRef = ref() + +// 创建本地数据副本 +const modelData = computed({ + get: () => props.modelValue, + set: (val) => emit('update:modelValue', val) +}) + +// 表单预览数据 +const formPreview = ref({ + formData: {}, + rule: [], + option: { + submitBtn: false, + resetBtn: false, + formData: {} + } +}) + +// 监听表单ID变化,加载表单数据 +watch( + () => modelData.value.formId, + async (newFormId) => { + if (newFormId && modelData.value.formType === 10) { + const data = await FormApi.getForm(newFormId) + setConfAndFields2(formPreview.value, data.conf, data.fields) + // 设置只读 + formPreview.value.rule.forEach((item: any) => { + item.props = { ...item.props, disabled: true } + }) + } else { + formPreview.value.rule = [] + } + }, + { immediate: true } +) + +const rules = { + formType: [{ required: true, message: '表单类型不能为空', trigger: 'blur' }], + formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }], + formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }], + formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }] +} + +/** 表单校验 */ +const validate = async () => { + await formRef.value?.validate() +} + +defineExpose({ + validate +}) +</script> diff --git a/src/views/bpm/model/form/ProcessDesign.vue b/src/views/bpm/model/form/ProcessDesign.vue new file mode 100644 index 0000000..40d35ab --- /dev/null +++ b/src/views/bpm/model/form/ProcessDesign.vue @@ -0,0 +1,235 @@ +<template> + <!-- BPMN设计器 --> + <template v-if="modelData.type === BpmModelType.BPMN"> + <BpmModelEditor + v-if="showDesigner" + :model-id="modelData.id" + :model-key="modelData.key" + :model-name="modelData.name" + :value="currentBpmnXml" + ref="bpmnEditorRef" + @success="handleDesignSuccess" + @init-finished="handleEditorInit" + /> + </template> + + <!-- Simple设计器 --> + <template v-else> + <SimpleModelDesign + v-if="showDesigner" + :model-id="modelData.id" + :model-key="modelData.key" + :model-name="modelData.name" + :start-user-ids="modelData.startUserIds" + :value="currentSimpleModel" + ref="simpleEditorRef" + @success="handleDesignSuccess" + @init-finished="handleEditorInit" + /> + </template> +</template> + +<script lang="ts" setup> +import { BpmModelType } from '@/utils/constants' +import BpmModelEditor from '../editor/index.vue' +import SimpleModelDesign from '../../simple/SimpleModelDesign.vue' + +const props = defineProps({ + modelValue: { + type: Object, + required: true + } +}) + +const emit = defineEmits(['update:modelValue', 'success']) + +const bpmnEditorRef = ref() +const simpleEditorRef = ref() +const isEditorInitialized = ref(false) + +// 创建本地数据副本 +const modelData = computed({ + get: () => props.modelValue, + set: (val) => emit('update:modelValue', val) +}) + +// 保存当前的流程XML或数据 +const currentBpmnXml = ref('') +const currentSimpleModel = ref('') + +// 初始化或更新当前的XML数据 +const initOrUpdateXmlData = () => { + if (modelData.value) { + if (modelData.value.type === BpmModelType.BPMN) { + currentBpmnXml.value = modelData.value.bpmnXml || '' + } else { + currentSimpleModel.value = modelData.value.simpleModel || '' + } + } +} + +// 监听modelValue的变化,更新数据 +watch( + () => props.modelValue, + (newVal) => { + if (newVal) { + if (newVal.type === BpmModelType.BPMN) { + if (newVal.bpmnXml && newVal.bpmnXml !== currentBpmnXml.value) { + currentBpmnXml.value = newVal.bpmnXml + // 如果编辑器已经初始化,刷新视图 + if (isEditorInitialized.value && bpmnEditorRef.value?.refresh) { + nextTick(() => { + bpmnEditorRef.value.refresh() + }) + } + } + } else { + if (newVal.simpleModel && newVal.simpleModel !== currentSimpleModel.value) { + currentSimpleModel.value = newVal.simpleModel + // 如果编辑器已经初始化,刷新视图 + if (isEditorInitialized.value && simpleEditorRef.value?.refresh) { + nextTick(() => { + simpleEditorRef.value.refresh() + }) + } + } + } + } + }, + { immediate: true, deep: true } +) + +/** 编辑器初始化完成的回调 */ +const handleEditorInit = async () => { + isEditorInitialized.value = true + + // 等待下一个tick,确保编辑器已经准备好 + await nextTick() + + // 初始化完成后,设置初始值 + if (modelData.value.type === BpmModelType.BPMN) { + if (modelData.value.bpmnXml) { + currentBpmnXml.value = modelData.value.bpmnXml + if (bpmnEditorRef.value?.refresh) { + await nextTick() + bpmnEditorRef.value.refresh() + } + } + } else { + if (modelData.value.simpleModel) { + currentSimpleModel.value = modelData.value.simpleModel + if (simpleEditorRef.value?.refresh) { + await nextTick() + simpleEditorRef.value.refresh() + } + } + } +} + +/** 获取当前流程数据 */ +const getProcessData = async () => { + try { + if (modelData.value.type === BpmModelType.BPMN) { + if (!bpmnEditorRef.value || !isEditorInitialized.value) { + return currentBpmnXml.value || undefined + } + const { xml } = await bpmnEditorRef.value.saveXML() + if (xml) { + currentBpmnXml.value = xml + return xml + } + } else { + if (!simpleEditorRef.value || !isEditorInitialized.value) { + return currentSimpleModel.value || undefined + } + const flowData = await simpleEditorRef.value.getCurrentFlowData() + if (flowData) { + currentSimpleModel.value = flowData + return flowData + } + } + return modelData.value.type === BpmModelType.BPMN + ? currentBpmnXml.value + : currentSimpleModel.value + } catch (error) { + console.error('获取流程数据失败:', error) + return modelData.value.type === BpmModelType.BPMN + ? currentBpmnXml.value + : currentSimpleModel.value + } +} + +/** 表单校验 */ +const validate = async () => { + try { + // 获取最新的流程数据 + const processData = await getProcessData() + if (!processData) { + throw new Error('请设计流程') + } + return true + } catch (error) { + throw error + } +} + +/** 处理设计器保存成功 */ +const handleDesignSuccess = async (data?: any) => { + if (data) { + if (modelData.value.type === BpmModelType.BPMN) { + currentBpmnXml.value = data + } else { + currentSimpleModel.value = data + } + + // 创建新的对象以触发响应式更新 + const newModelData = { + ...modelData.value, + bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, + simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data + } + + // 使用emit更新父组件的数据 + await nextTick() + emit('update:modelValue', newModelData) + emit('success', data) + } +} + +/** 是否显示设计器 */ +const showDesigner = computed(() => { + return Boolean(modelData.value?.key && modelData.value?.name) +}) + +// 组件创建时初始化数据 +onMounted(() => { + initOrUpdateXmlData() +}) + +// 组件卸载前保存数据 +onBeforeUnmount(async () => { + try { + // 获取并保存最新的流程数据 + const data = await getProcessData() + if (data) { + // 创建新的对象以触发响应式更新 + const newModelData = { + ...modelData.value, + bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, + simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data + } + + // 使用emit更新父组件的数据 + await nextTick() + emit('update:modelValue', newModelData) + } + } catch (error) { + console.error('保存数据失败:', error) + } +}) + +defineExpose({ + validate, + getProcessData +}) +</script> diff --git a/src/views/bpm/model/form/index.vue b/src/views/bpm/model/form/index.vue new file mode 100644 index 0000000..4585fc6 --- /dev/null +++ b/src/views/bpm/model/form/index.vue @@ -0,0 +1,439 @@ +<template> + <ContentWrap> + <div class="mx-auto"> + <!-- 头部导航栏 --> + <div + class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px" + > + <!-- 左侧标题 --> + <div class="w-200px flex items-center overflow-hidden"> + <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" /> + <span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'"> + {{ formData.name || '创建流程' }} + </span> + </div> + + <!-- 步骤条 --> + <div class="flex-1 flex items-center justify-center h-full"> + <div class="w-400px flex items-center justify-between h-full"> + <div + v-for="(step, index) in steps" + :key="index" + class="flex items-center cursor-pointer mx-15px relative h-full" + :class="[ + currentStep === index + ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid' + : 'text-gray-500' + ]" + @click="handleStepClick(index)" + > + <div + class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px" + :class="[ + currentStep === index + ? 'bg-[#3473ff] text-white border-[#3473ff]' + : 'border-gray-300 bg-white text-gray-500' + ]" + > + {{ index + 1 }} + </div> + <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span> + </div> + </div> + </div> + + <!-- 右侧按钮 --> + <div class="w-200px flex items-center justify-end gap-2"> + <el-button v-if="route.params.id" type="success" @click="handleDeploy">发 布</el-button> + <el-button type="primary" @click="handleSave">保 存</el-button> + </div> + </div> + + <!-- 主体内容 --> + <div class="mt-50px"> + <!-- 第一步:基本信息 --> + <div v-if="currentStep === 0" class="mx-auto w-560px"> + <BasicInfo + v-model="formData" + :categoryList="categoryList" + :userList="userList" + ref="basicInfoRef" + /> + </div> + + <!-- 第二步:表单设计 --> + <div v-if="currentStep === 1" class="mx-auto w-560px"> + <FormDesign v-model="formData" :formList="formList" ref="formDesignRef" /> + </div> + + <!-- 第三步:流程设计 --> + <ProcessDesign + v-if="currentStep === 2" + v-model="formData" + ref="processDesignRef" + @success="handleDesignSuccess" + /> + </div> + </div> + </ContentWrap> +</template> + +<script lang="ts" setup> +import { useRoute, useRouter } from 'vue-router' +import { useMessage } from '@/hooks/web/useMessage' +import * as ModelApi from '@/api/bpm/model' +import * as FormApi from '@/api/bpm/form' +import { CategoryApi } from '@/api/bpm/category' +import * as UserApi from '@/api/system/user' +import { useUserStoreWithOut } from '@/store/modules/user' +import { BpmModelFormType, BpmModelType } from '@/utils/constants' +import BasicInfo from './BasicInfo.vue' +import FormDesign from './FormDesign.vue' +import ProcessDesign from './ProcessDesign.vue' +import { useTagsViewStore } from '@/store/modules/tagsView' + +const router = useRouter() +const { delView } = useTagsViewStore() // 视图操作 +const route = useRoute() +const message = useMessage() +const userStore = useUserStoreWithOut() + +// 组件引用 +const basicInfoRef = ref() +const formDesignRef = ref() +const processDesignRef = ref() + +/** 步骤校验函数 */ +const validateBasic = async () => { + await basicInfoRef.value?.validate() +} + +/** 表单设计校验 */ +const validateForm = async () => { + await formDesignRef.value?.validate() +} + +/** 流程设计校验 */ +const validateProcess = async () => { + await processDesignRef.value?.validate() +} + +const currentStep = ref(0) // 步骤控制 +const steps = [ + { title: '基本信息', validator: validateBasic }, + { title: '表单设计', validator: validateForm }, + { title: '流程设计', validator: validateProcess } +] + +// 表单数据 +const formData: any = ref({ + id: undefined, + name: '', + key: '', + category: undefined, + icon: undefined, + description: '', + type: BpmModelType.BPMN, + formType: BpmModelFormType.NORMAL, + formId: '', + formCustomCreatePath: '', + formCustomViewPath: '', + visible: true, + startUserType: undefined, + managerUserType: undefined, + startUserIds: [], + managerUserIds: [] +}) + +// 数据列表 +const formList = ref([]) +const categoryList = ref([]) +const userList = ref<UserApi.UserVO[]>([]) + +/** 初始化数据 */ +const initData = async () => { + const modelId = route.params.id as string + if (modelId) { + // 修改场景 + formData.value = await ModelApi.getModel(modelId) + } else { + // 新增场景 + formData.value.managerUserIds.push(userStore.getUser.id) + } + + // 获取表单列表 + formList.value = await FormApi.getFormSimpleList() + // 获取分类列表 + categoryList.value = await CategoryApi.getCategorySimpleList() + // 获取用户列表 + userList.value = await UserApi.getSimpleUserList() +} + +/** 校验所有步骤数据是否完整 */ +const validateAllSteps = async () => { + try { + // 基本信息校验 + await basicInfoRef.value?.validate() + if (!formData.value.key || !formData.value.name || !formData.value.category) { + currentStep.value = 0 + throw new Error('请完善基本信息') + } + + // 表单设计校验 + await formDesignRef.value?.validate() + if (formData.value.formType === 10 && !formData.value.formId) { + currentStep.value = 1 + throw new Error('请选择流程表单') + } + if ( + formData.value.formType === 20 && + (!formData.value.formCustomCreatePath || !formData.value.formCustomViewPath) + ) { + currentStep.value = 1 + throw new Error('请完善自定义表单信息') + } + + // 流程设计校验 + // 如果已经有流程数据,则不需要重新校验 + if (!formData.value.bpmnXml && !formData.value.simpleModel) { + // 如果当前不在第三步,需要先保存当前步骤数据 + if (currentStep.value !== 2) { + await steps[currentStep.value].validator() + // 切换到第三步 + currentStep.value = 2 + // 等待组件渲染完成 + await nextTick() + } + + // 校验流程设计 + await processDesignRef.value?.validate() + const processData = await processDesignRef.value?.getProcessData() + if (!processData) { + throw new Error('请设计流程') + } + + // 保存流程数据 + if (formData.value.type === BpmModelType.BPMN) { + formData.value.bpmnXml = processData + formData.value.simpleModel = null + } else { + formData.value.bpmnXml = null + formData.value.simpleModel = processData + } + } + + return true + } catch (error) { + throw error + } +} + +/** 保存操作 */ +const handleSave = async () => { + try { + // 保存前校验所有步骤的数据 + await validateAllSteps() + + // 更新表单数据 + const modelData = { + ...formData.value + } + + // 如果当前在第三步,获取最新的流程设计数据 + if (currentStep.value === 2) { + const processData = await processDesignRef.value?.getProcessData() + if (processData) { + if (formData.value.type === BpmModelType.BPMN) { + modelData.bpmnXml = processData + modelData.simpleModel = null + } else { + modelData.bpmnXml = null + modelData.simpleModel = processData + } + } + } + + if (formData.value.id) { + // 修改场景 + await ModelApi.updateModel(modelData) + // 询问是否发布流程 + try { + await message.confirm('修改流程成功,是否发布流程?') + // 用户点击确认,执行发布 + await handleDeploy() + } catch { + // 用户点击取消,停留在当前页面 + } + } else { + // 新增场景 + formData.value.id = await ModelApi.createModel(modelData) + message.success('新增成功') + try { + await message.confirm('创建流程成功,是否继续编辑?') + // 用户点击继续编辑,跳转到编辑页面 + await nextTick() + // 先删除当前页签 + delView(unref(router.currentRoute)) + // 跳转到编辑页面 + await router.push({ + name: 'BpmModelUpdate', + params: { id: formData.value.id } + }) + } catch { + // 先删除当前页签 + delView(unref(router.currentRoute)) + // 用户点击返回列表 + await router.push({ name: 'BpmModel' }) + } + } + } catch (error: any) { + console.error('保存失败:', error) + message.warning(error.message || '请完善所有步骤的必填信息') + } +} + +/** 发布操作 */ +const handleDeploy = async () => { + try { + // 修改场景下直接发布,新增场景下需要先确认 + if (!formData.value.id) { + await message.confirm('是否确认发布该流程?') + } + + // 校验所有步骤 + await validateAllSteps() + + // 更新表单数据 + const modelData = { + ...formData.value + } + + // 如果当前在第三步,获取最新的流程设计数据 + if (currentStep.value === 2) { + const processData = await processDesignRef.value?.getProcessData() + if (processData) { + if (formData.value.type === BpmModelType.BPMN) { + modelData.bpmnXml = processData + modelData.simpleModel = null + } else { + modelData.bpmnXml = null + modelData.simpleModel = processData + } + } + } + + // 先保存所有数据 + if (formData.value.id) { + await ModelApi.updateModel(modelData) + } else { + const result = await ModelApi.createModel(modelData) + formData.value.id = result.id + } + + // 发布 + await ModelApi.deployModel(formData.value.id) + message.success('发布成功') + // 返回列表页 + await router.push({ name: 'BpmModel' }) + } catch (error: any) { + console.error('发布失败:', error) + message.warning(error.message || '发布失败') + } +} + +/** 步骤切换处理 */ +const handleStepClick = async (index: number) => { + try { + // 如果是切换到第三步(流程设计),需要校验key和name + if (index === 2) { + if (!formData.value.key || !formData.value.name) { + message.warning('请先填写流程标识和流程名称') + return + } + } + + // 保存当前步骤的数据 + if (currentStep.value === 2) { + const processData = await processDesignRef.value?.getProcessData() + if (processData) { + if (formData.value.type === BpmModelType.BPMN) { + formData.value.bpmnXml = processData + formData.value.simpleModel = null + } else { + formData.value.bpmnXml = null + formData.value.simpleModel = processData + } + } + } else { + // 只有在向后切换时才进行校验 + if (index > currentStep.value) { + if (typeof steps[currentStep.value].validator === 'function') { + await steps[currentStep.value].validator() + } + } + } + + // 切换步骤 + currentStep.value = index + + // 如果切换到流程设计步骤,等待组件渲染完成后刷新设计器 + if (index === 2) { + await nextTick() + // 等待更长时间确保组件完全初始化 + await new Promise(resolve => setTimeout(resolve, 200)) + if (processDesignRef.value?.refresh) { + await processDesignRef.value.refresh() + } + } + } catch (error) { + console.error('步骤切换失败:', error) + message.warning('请先完善当前步骤必填信息') + } +} + +/** 处理设计器保存成功 */ +const handleDesignSuccess = (bpmnXml?: string) => { + if (bpmnXml) { + formData.value.bpmnXml = bpmnXml + } +} + +/** 返回列表页 */ +const handleBack = () => { + // 先删除当前页签 + delView(unref(router.currentRoute)) + // 跳转到列表页 + router.push({ name: 'BpmModel' }) +} + +/** 初始化 */ +onMounted(async () => { + await initData() +}) + +// 添加组件卸载前的清理代码 +onBeforeUnmount(() => { + // 清理所有的引用 + basicInfoRef.value = null + formDesignRef.value = null + processDesignRef.value = null +}) +</script> + +<style lang="scss" scoped> +.border-bottom { + border-bottom: 1px solid #dcdfe6; +} + +.text-primary { + color: #3473ff; +} + +.bg-primary { + background-color: #3473ff; +} + +.border-primary { + border-color: #3473ff; +} +</style> diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue index a20ea4e..c7d9417 100644 --- a/src/views/bpm/model/index.vue +++ b/src/views/bpm/model/index.vue @@ -1,352 +1,138 @@ <template> <ContentWrap> - <!-- 搜索工作栏 --> - <el-form - class="-mb-15px" - :model="queryParams" - ref="queryFormRef" - :inline="true" - label-width="68px" - > - <el-form-item label="流程标识" prop="key"> - <el-input - v-model="queryParams.key" - placeholder="请输入流程标识" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="流程名称" prop="name"> - <el-input - v-model="queryParams.name" - placeholder="请输入流程名称" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="流程分类" prop="category"> - <el-select - v-model="queryParams.category" - placeholder="请选择流程分类" - clearable - class="!w-240px" - > - <el-option - v-for="category in categoryList" - :key="category.code" - :label="category.name" - :value="category.code" - /> - </el-select> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button - type="primary" - plain - @click="openForm('create')" - v-hasPermi="['bpm:model:create']" - > - <Icon icon="ep:plus" class="mr-5px" /> 新建流程 - </el-button> - <el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']"> - <Icon icon="ep:upload" class="mr-5px" /> 导入流程 - </el-button> - </el-form-item> - </el-form> - </ContentWrap> + <div class="flex justify-between pl-20px items-center"> + <h3 class="font-extrabold">流程模型</h3> + <!-- 搜索工作栏 --> + <el-form + v-if="!isCategorySorting" + class="-mb-15px flex mr-10px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + @submit.prevent + > + <el-form-item prop="name" class="ml-auto"> + <el-input + v-model="queryParams.name" + placeholder="搜索流程" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + > + <template #prefix> + <Icon icon="ep:search" class="mx-10px" /> + </template> + </el-input> + </el-form-item> + <!-- 右上角:新建模型、更多操作 --> + <el-form-item> + <el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']"> + <Icon icon="ep:plus" class="mr-5px" /> 新建模型 + </el-button> + </el-form-item> + <el-form-item> + <el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end"> + <el-button class="w-30px" plain> + <Icon icon="ep:setting" /> + </el-button> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item command="handleCategoryAdd"> + <Icon icon="ep:circle-plus" :size="13" class="mr-5px" /> + 新建分类 + </el-dropdown-item> + <el-dropdown-item command="handleCategorySort"> + <Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" /> + 分类排序 + </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </el-form-item> + </el-form> + <div class="mr-20px" v-else> + <el-button @click="handleCategorySortCancel"> 取 消 </el-button> + <el-button type="primary" @click="handleCategorySortSubmit"> 保存排序 </el-button> + </div> + </div> - <!-- 列表 --> - <ContentWrap> - <el-table v-loading="loading" :data="list"> - <el-table-column label="流程标识" align="center" prop="key" width="200" /> - <el-table-column label="流程名称" align="center" prop="name" width="200"> - <template #default="scope"> - <el-button type="primary" link @click="handleBpmnDetail(scope.row)"> - <span>{{ scope.row.name }}</span> - </el-button> - </template> - </el-table-column> - <el-table-column label="流程图标" align="center" prop="icon" width="100"> - <template #default="scope"> - <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 - v-if="scope.row.formType === 10" - type="primary" - link - @click="handleFormDetail(scope.row)" + <el-divider /> + + <!-- 按照分类,展示其所属的模型列表 --> + <div class="px-15px"> + <draggable + :disabled="!isCategorySorting" + v-model="categoryGroup" + item-key="id" + :animation="400" + > + <template #item="{ element }"> + <ContentWrap + class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl" + v-loading="loading" + :body-style="{ padding: 0 }" + :key="element.id" > - <span>{{ scope.row.formName }}</span> - </el-button> - <el-button - v-else-if="scope.row.formType === 20" - type="primary" - link - @click="handleFormDetail(scope.row)" - > - <span>{{ scope.row.formCustomCreatePath }}</span> - </el-button> - <label v-else>暂无表单</label> - </template> - </el-table-column> - <el-table-column - label="创建时间" - align="center" - prop="createTime" - width="180" - :formatter="dateFormatter" - /> - <el-table-column label="最新部署的流程定义" align="center"> - <el-table-column - label="流程版本" - align="center" - prop="processDefinition.version" - width="100" - > - <template #default="scope"> - <el-tag v-if="scope.row.processDefinition"> - v{{ scope.row.processDefinition.version }} - </el-tag> - <el-tag v-else type="warning">未部署</el-tag> - </template> - </el-table-column> - <el-table-column - label="激活状态" - align="center" - prop="processDefinition.version" - width="85" - > - <template #default="scope"> - <el-switch - v-if="scope.row.processDefinition" - v-model="scope.row.processDefinition.suspensionState" - :active-value="1" - :inactive-value="2" - @change="handleChangeState(scope.row)" + <CategoryDraggableModel + :isCategorySorting="isCategorySorting" + :categoryInfo="element" + @success="getList" /> - </template> - </el-table-column> - <el-table-column label="部署时间" align="center" prop="deploymentTime" width="180"> - <template #default="scope"> - <span v-if="scope.row.processDefinition"> - {{ formatDate(scope.row.processDefinition.deploymentTime) }} - </span> - </template> - </el-table-column> - </el-table-column> - <el-table-column label="操作" align="center" width="240" fixed="right"> - <template #default="scope"> - <el-button - link - type="primary" - @click="openForm('update', scope.row.id)" - v-hasPermi="['bpm:model:update']" - > - 修改流程 - </el-button> - <el-button - link - type="primary" - @click="handleDesign(scope.row)" - v-hasPermi="['bpm:model:update']" - > - 设计流程 - </el-button> - <el-button - link - type="primary" - @click="handleDeploy(scope.row)" - v-hasPermi="['bpm:model:deploy']" - > - 发布流程 - </el-button> - <el-button - link - type="primary" - v-hasPermi="['bpm:process-definition:query']" - @click="handleDefinitionList(scope.row)" - > - 流程定义 - </el-button> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['bpm:model:delete']" - > - 删除 - </el-button> + </ContentWrap> </template> - </el-table-column> - </el-table> - <!-- 分页 --> - <Pagination - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> + </draggable> + </div> </ContentWrap> <!-- 表单弹窗:添加/修改流程 --> <ModelForm ref="formRef" @success="getList" /> - - <!-- 表单弹窗:导入流程 --> - <ModelImportForm ref="importFormRef" @success="getList" /> - + <!-- 表单弹窗:添加分类 --> + <CategoryForm ref="categoryFormRef" @success="getList" /> <!-- 弹窗:表单详情 --> <Dialog title="表单详情" v-model="formDetailVisible" width="800"> <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> </Dialog> - - <!-- 弹窗:流程模型图的预览 --> - <Dialog title="流程图" v-model="bpmnDetailVisible" width="800"> - <MyProcessViewer - key="designer" - v-model="bpmnXML" - :value="bpmnXML as any" - v-bind="bpmnControlForm" - :prefix="bpmnControlForm.prefix" - /> - </Dialog> </template> <script lang="ts" setup> -import { dateFormatter, formatDate } from '@/utils/formatTime' -import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package' -import * as ModelApi from '@/api/bpm/model' -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 draggable from 'vuedraggable' import { CategoryApi } from '@/api/bpm/category' +import * as ModelApi from '@/api/bpm/model' +import ModelForm from './ModelForm.vue' +import CategoryForm from '../category/CategoryForm.vue' +import { cloneDeep } from 'lodash-es' +import CategoryDraggableModel from './CategoryDraggableModel.vue' defineOptions({ name: 'BpmModel' }) +const { push } = useRouter() const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 -const { push } = useRouter() // 路由 - const loading = ref(true) // 列表的加载中 -const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 +const isCategorySorting = ref(false) // 是否 category 正处于排序状态 const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - key: undefined, - name: undefined, - category: undefined + name: undefined }) const queryFormRef = ref() // 搜索的表单 -const categoryList = ref([]) // 流程分类列表 - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - const data = await ModelApi.getModelPage(queryParams) - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } -} +const categoryGroup: any = ref([]) // 按照 category 分组的数据 +const originalData: any = ref([]) // 原始数据 /** 搜索按钮操作 */ 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 importFormRef = ref() -const openImportForm = () => { - importFormRef.value.open() -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await ModelApi.deleteModel(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 更新状态操作 */ -const handleChangeState = async (row) => { - const state = row.processDefinition.suspensionState - try { - // 修改状态的二次确认 - const id = row.id - const statusState = state === 1 ? '激活' : '挂起' - const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?' - await message.confirm(content) - // 发起修改状态 - await ModelApi.updateModelState(id, state) - // 刷新列表 - await getList() - } catch { - // 取消后,进行恢复按钮 - row.processDefinition.suspensionState = state === 1 ? 2 : 1 + if (type === 'create') { + push({ name: 'BpmModelCreate' }) + } else { + push({ + name: 'BpmModelUpdate', + params: { id } + }) } -} - -/** 设计流程 */ -const handleDesign = (row) => { - push({ - name: 'BpmModelEditor', - query: { - modelId: row.id - } - }) -} - -/** 发布流程 */ -const handleDeploy = async (row) => { - try { - // 删除的二次确认 - await message.confirm('是否部署该流程!!') - // 发起部署 - await ModelApi.deployModel(row.id) - message.success(t('部署成功')) - // 刷新列表 - await getList() - } catch {} -} - -/** 跳转到指定流程定义列表 */ -const handleDefinitionList = (row) => { - push({ - name: 'BpmProcessDefinition', - query: { - key: row.key - } - }) } /** 流程表单的详情按钮操作 */ @@ -355,36 +141,89 @@ rule: [], option: {} }) -const handleFormDetail = async (row) => { - if (row.formType == 10) { - // 设置表单 - const data = await FormApi.getForm(row.formId) - setConfAndFields2(formDetailPreview, data.conf, data.fields) - // 弹窗打开 - formDetailVisible.value = true - } else { - await push({ - path: row.formCustomCreatePath - }) + +/** 右上角设置按钮 */ +const handleCommand = (command: string) => { + switch (command) { + case 'handleCategoryAdd': + handleCategoryAdd() + break + case 'handleCategorySort': + handleCategorySort() + break + default: + break } } -/** 流程图的详情按钮操作 */ -const bpmnDetailVisible = ref(false) -const bpmnXML = ref(null) -const bpmnControlForm = ref({ - prefix: 'flowable' -}) -const handleBpmnDetail = async (row) => { - const data = await ModelApi.getModel(row.id) - bpmnXML.value = data.bpmnXml || '' - bpmnDetailVisible.value = true +/** 新建分类 */ +const categoryFormRef = ref() +const handleCategoryAdd = () => { + categoryFormRef.value.open('create') +} + +/** 分类排序的提交 */ +const handleCategorySort = () => { + // 保存初始数据 + originalData.value = cloneDeep(categoryGroup.value) + isCategorySorting.value = true +} + +/** 分类排序的取消 */ +const handleCategorySortCancel = () => { + // 恢复初始数据 + categoryGroup.value = cloneDeep(originalData.value) + isCategorySorting.value = false +} + +/** 分类排序的保存 */ +const handleCategorySortSubmit = async () => { + // 保存排序 + const ids = categoryGroup.value.map((item: any) => item.id) + await CategoryApi.updateCategorySortBatch(ids) + // 刷新列表 + isCategorySorting.value = false + message.success('排序分类成功') + await getList() +} + +/** 加载数据 */ +const getList = async () => { + loading.value = true + try { + // 查询模型 + 分裂的列表 + const modelList = await ModelApi.getModelList(queryParams.name) + const categoryList = await CategoryApi.getCategorySimpleList() + // 按照 category 聚合 + // 注意:必须一次性赋值给 categoryGroup,否则每次操作后,列表会重新渲染,滚动条的位置会偏离!!! + categoryGroup.value = categoryList.map((category: any) => ({ + ...category, + modelList: modelList.filter((model: any) => model.categoryName == category.name) + })) + } finally { + loading.value = false + } } /** 初始化 **/ -onMounted(async () => { - await getList() - // 查询流程分类列表 - categoryList.value = await CategoryApi.getCategorySimpleList() +onMounted(() => { + getList() }) </script> + +<style lang="scss" scoped> +:deep() { + .el-table--fit .el-table__inner-wrapper:before { + height: 0; + } + .el-card { + border-radius: 8px; + } + .el-form--inline .el-form-item { + margin-right: 10px; + } + .el-divider--horizontal { + margin-top: 6px; + } +} +</style> diff --git a/src/views/bpm/model/index_old.vue b/src/views/bpm/model/index_old.vue new file mode 100644 index 0000000..9cb6420 --- /dev/null +++ b/src/views/bpm/model/index_old.vue @@ -0,0 +1,404 @@ +<template> + <doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" /> + <doc-alert + title="流程设计器(钉钉、飞书)" + url="https://doc.iocoder.cn/bpm/model-designer-bpmn/" + /> + <doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" /> + <doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" /> + + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="流程标识" prop="key"> + <el-input + v-model="queryParams.key" + placeholder="请输入流程标识" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="流程名称" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入流程名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="流程分类" prop="category"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程分类" + clearable + class="!w-240px" + > + <el-option + v-for="category in categoryList" + :key="category.code" + :label="category.name" + :value="category.code" + /> + </el-select> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['bpm:model:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新建 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column label="流程名称" align="center" prop="name" min-width="200" /> + <el-table-column label="流程图标" align="center" prop="icon" min-width="100"> + <template #default="scope"> + <el-image :src="scope.row.icon" class="h-32px w-32px" /> + </template> + </el-table-column> + <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100"> + <template #default="scope"> + <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0"> + 全部可见 + </el-text> + <el-text v-else-if="scope.row.startUsers.length == 1"> + {{ scope.row.startUsers[0].nickname }} + </el-text> + <el-text v-else> + <el-tooltip + class="box-item" + effect="dark" + placement="top" + :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')" + > + {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见 + </el-tooltip> + </el-text> + </template> + </el-table-column> + <el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" /> + <el-table-column label="表单信息" align="center" prop="formType" min-width="200"> + <template #default="scope"> + <el-button + v-if="scope.row.formType === 10" + type="primary" + link + @click="handleFormDetail(scope.row)" + > + <span>{{ scope.row.formName }}</span> + </el-button> + <el-button + v-else-if="scope.row.formType === 20" + type="primary" + link + @click="handleFormDetail(scope.row)" + > + <span>{{ scope.row.formCustomCreatePath }}</span> + </el-button> + <label v-else>暂无表单</label> + </template> + </el-table-column> + <el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250"> + <template #default="scope"> + <span v-if="scope.row.processDefinition"> + {{ formatDate(scope.row.processDefinition.deploymentTime) }} + </span> + <el-tag v-if="scope.row.processDefinition" class="ml-10px"> + v{{ scope.row.processDefinition.version }} + </el-tag> + <el-tag v-else type="warning">未部署</el-tag> + <el-tag + v-if="scope.row.processDefinition?.suspensionState === 2" + type="warning" + class="ml-10px" + > + 已停用 + </el-tag> + </template> + </el-table-column> + <el-table-column label="操作" align="center" width="200" fixed="right"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['bpm:model:update']" + :disabled="!isManagerUser(scope.row)" + > + 修改 + </el-button> + <el-button + link + class="!ml-5px" + type="primary" + @click="handleDesign(scope.row)" + v-hasPermi="['bpm:model:update']" + :disabled="!isManagerUser(scope.row)" + > + 设计 + </el-button> + <el-button + link + class="!ml-5px" + type="primary" + @click="handleDeploy(scope.row)" + v-hasPermi="['bpm:model:deploy']" + :disabled="!isManagerUser(scope.row)" + > + 发布 + </el-button> + <el-dropdown + class="!align-middle ml-5px" + @command="(command) => handleCommand(command, scope.row)" + v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']" + > + <el-button type="primary" link>更多</el-button> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item + command="handleDefinitionList" + v-if="checkPermi(['bpm:process-definition:query'])" + > + 历史 + </el-dropdown-item> + <el-dropdown-item + command="handleChangeState" + v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition" + :disabled="!isManagerUser(scope.row)" + > + {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }} + </el-dropdown-item> + <el-dropdown-item + type="danger" + command="handleDelete" + v-if="checkPermi(['bpm:model:delete'])" + :disabled="!isManagerUser(scope.row)" + > + 删除 + </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改流程 --> + <ModelForm ref="formRef" @success="getList" /> + + <!-- 弹窗:表单详情 --> + <Dialog title="表单详情" v-model="formDetailVisible" width="800"> + <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> + </Dialog> +</template> + +<script lang="ts" setup> +import { formatDate } from '@/utils/formatTime' +import * as ModelApi from '@/api/bpm/model' +import * as FormApi from '@/api/bpm/form' +import ModelForm from './ModelForm.vue' +import { setConfAndFields2 } from '@/utils/formCreate' +import { CategoryApi } from '@/api/bpm/category' +import { BpmModelType } from '@/utils/constants' +import { checkPermi } from '@/utils/permission' +import { useUserStoreWithOut } from '@/store/modules/user' + +defineOptions({ name: 'BpmModel' }) + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 +const { push } = useRouter() // 路由 +const userStore = useUserStoreWithOut() // 用户信息缓存 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + key: undefined, + name: undefined, + category: undefined +}) +const queryFormRef = ref() // 搜索的表单 +const categoryList = ref([]) // 流程分类列表 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await ModelApi.getModelList(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** '更多'操作按钮 */ +const handleCommand = (command: string, row: any) => { + switch (command) { + case 'handleDefinitionList': + handleDefinitionList(row) + break + case 'handleDelete': + handleDelete(row) + break + case 'handleChangeState': + handleChangeState(row) + break + default: + break + } +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (row: any) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await ModelApi.deleteModel(row.id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +/** 更新状态操作 */ +const handleChangeState = async (row: any) => { + const state = row.processDefinition.suspensionState + const newState = state === 1 ? 2 : 1 + try { + // 修改状态的二次确认 + const id = row.id + debugger + const statusState = state === 1 ? '停用' : '启用' + const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?' + await message.confirm(content) + // 发起修改状态 + await ModelApi.updateModelState(id, newState) + message.success(statusState + '成功') + // 刷新列表 + await getList() + } catch {} +} + +/** 设计流程 */ +const handleDesign = (row: any) => { + if (row.type == BpmModelType.BPMN) { + push({ + name: 'BpmModelEditor', + query: { + modelId: row.id + } + }) + } else { + push({ + name: 'SimpleModelDesign', + query: { + modelId: row.id + } + }) + } +} + +/** 发布流程 */ +const handleDeploy = async (row: any) => { + try { + // 删除的二次确认 + await message.confirm('是否部署该流程!!') + // 发起部署 + await ModelApi.deployModel(row.id) + message.success(t('部署成功')) + // 刷新列表 + await getList() + } catch {} +} + +/** 跳转到指定流程定义列表 */ +const handleDefinitionList = (row) => { + push({ + name: 'BpmProcessDefinition', + query: { + key: row.key + } + }) +} + +/** 流程表单的详情按钮操作 */ +const formDetailVisible = ref(false) +const formDetailPreview = ref({ + rule: [], + option: {} +}) +const handleFormDetail = async (row: any) => { + if (row.formType == 10) { + // 设置表单 + const data = await FormApi.getForm(row.formId) + setConfAndFields2(formDetailPreview, data.conf, data.fields) + // 弹窗打开 + formDetailVisible.value = true + } else { + await push({ + path: row.formCustomCreatePath + }) + } +} + +/** 判断是否可以操作 */ +const isManagerUser = (row: any) => { + const userId = userStore.getUser.id + return row.managerUserIds && row.managerUserIds.includes(userId) +} + +/** 初始化 **/ +onMounted(async () => { + await getList() + // 查询流程分类列表 + categoryList.value = await CategoryApi.getCategorySimpleList() +}) +</script> diff --git a/src/views/bpm/processExpression/ProcessExpressionForm.vue b/src/views/bpm/processExpression/ProcessExpressionForm.vue index acf0667..2e5ed2e 100644 --- a/src/views/bpm/processExpression/ProcessExpressionForm.vue +++ b/src/views/bpm/processExpression/ProcessExpressionForm.vue @@ -15,7 +15,7 @@ <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" - :label="dict.value" + :value="dict.value" > {{ dict.label }} </el-radio> diff --git a/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue b/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue new file mode 100644 index 0000000..7eaf0f4 --- /dev/null +++ b/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue @@ -0,0 +1,298 @@ +<template> + <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }"> + <div class="processInstance-wrap-main"> + <el-scrollbar> + <div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div> + <el-divider class="!my-8px" /> + + <!-- 中间主要内容 tab 栏 --> + <el-tabs v-model="activeTab"> + <!-- 表单信息 --> + <el-tab-pane label="表单填写" name="form"> + <div class="form-scroll-area" v-loading="processInstanceStartLoading"> + <el-scrollbar> + <el-row> + <el-col :span="17"> + <form-create + :rule="detailForm.rule" + v-model:api="fApi" + v-model="detailForm.value" + :option="detailForm.option" + @submit="submitForm" + /> + </el-col> + + <el-col :span="6" :offset="1"> + <!-- 流程时间线 --> + <ProcessInstanceTimeline + ref="timelineRef" + :activity-nodes="activityNodes" + :show-status-icon="false" + @select-user-confirm="selectUserConfirm" + /> + </el-col> + </el-row> + </el-scrollbar> + </div> + </el-tab-pane> + <!-- 流程图 --> + <el-tab-pane label="流程图" name="diagram"> + <div class="form-scroll-area"> + <!-- BPMN 流程图预览 --> + <ProcessInstanceBpmnViewer + :bpmn-xml="bpmnXML" + v-if="BpmModelType.BPMN === selectProcessDefinition.modelType" + /> + + <!-- Simple 流程图预览 --> + <ProcessInstanceSimpleViewer + :simple-json="simpleJson" + v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType" + /> + </div> + </el-tab-pane> + </el-tabs> + + <!-- 底部操作栏 --> + <div class="b-t-solid border-t-1px border-[var(--el-border-color)]"> + <!-- 操作栏按钮 --> + <div + v-if="activeTab === 'form'" + class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container" + > + <el-button plain type="success" @click="submitForm"> + <Icon icon="ep:select" /> 发起 + </el-button> + <el-button plain type="danger" @click="handleCancel"> + <Icon icon="ep:close" /> 取消 + </el-button> + </div> + </div> + </el-scrollbar> + </div> + </ContentWrap> +</template> +<script lang="ts" setup> +import { decodeFields, setConfAndFields2 } from '@/utils/formCreate' +import { BpmModelType } from '@/utils/constants' +import { + CandidateStrategy, + NodeId, + FieldPermissionType +} from '@/components/SimpleProcessDesignerV2/src/consts' +import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue' +import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue' +import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue' +import type { ApiAttrs } from '@form-create/element-ui/types/config' +import { useTagsViewStore } from '@/store/modules/tagsView' +import * as ProcessInstanceApi from '@/api/bpm/processInstance' +import * as DefinitionApi from '@/api/bpm/definition' +import { ApprovalNodeInfo } from '@/api/bpm/processInstance' + +defineOptions({ name: 'ProcessDefinitionDetail' }) +const props = defineProps<{ + selectProcessDefinition: any +}>() +const emit = defineEmits(['cancel']) +const processInstanceStartLoading = ref(false) // 流程实例发起中 +const { push, currentRoute } = useRouter() // 路由 +const message = useMessage() // 消息弹窗 +const { delView } = useTagsViewStore() // 视图操作 + +const detailForm: any = ref({ + rule: [], + option: {}, + value: {} +}) // 流程表单详情 +const fApi = ref<ApiAttrs>() +// 指定审批人 +const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人或抄送人的任务列表 +const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据 +const bpmnXML: any = ref(null) // BPMN 数据 +const simpleJson = ref<string | undefined>() // Simple 设计器数据 json 格式 + +const activeTab = ref('form') // 当前的 Tab +const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息 + +/** 设置表单信息、获取流程图数据 **/ +const initProcessInfo = async (row: any, formVariables?: any) => { + // 重置指定审批人 + startUserSelectTasks.value = [] + startUserSelectAssignees.value = {} + + // 情况一:流程表单 + if (row.formType == 10) { + // 设置表单 + // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。 + // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。 + // 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!! + const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field) + for (const key in formVariables) { + if (!allowedFields.includes(key)) { + delete formVariables[key] + } + } + setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) + + await nextTick() + fApi.value?.btn.show(false) // 隐藏提交按钮 + + // 获取流程审批信息 + await getApprovalDetail(row) + + // 加载流程图 + const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id) + if (processDefinitionDetail) { + bpmnXML.value = processDefinitionDetail.bpmnXml + simpleJson.value = processDefinitionDetail.simpleModel + } + // 情况二:业务表单 + } else if (row.formCustomCreatePath) { + await push({ + path: row.formCustomCreatePath + }) + // 这里暂时无需加载流程图,因为跳出到另外个 Tab; + } +} + +/** 获取审批详情 */ +const getApprovalDetail = async (row: any) => { + try { + // TODO 获取审批详情,设置 activityId 为发起人节点(为了获取字段权限。暂时只对 Simple 设计器有效) + const data = await ProcessInstanceApi.getApprovalDetail({ + processDefinitionId: row.id, + activityId: NodeId.START_USER_NODE_ID + }) + + if (!data) { + message.error('查询不到审批详情信息!') + return + } + + // 获取发起人自选的任务 + startUserSelectTasks.value = data.activityNodes?.filter( + (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy + ) + if (startUserSelectTasks.value?.length > 0) { + for (const node of startUserSelectTasks.value) { + startUserSelectAssignees.value[node.id] = [] + } + } + + // 获取审批节点,显示 Timeline 的数据 + activityNodes.value = data.activityNodes + // 获取表单字段权限 + const formFieldsPermission = data.formFieldsPermission + // 设置表单字段权限 + if (formFieldsPermission) { + Object.keys(formFieldsPermission).forEach((item) => { + setFieldPermission(item, formFieldsPermission[item]) + }) + } + } finally { + } +} + +/** + * 设置表单权限 + */ +const setFieldPermission = (field: string, permission: string) => { + if (permission === FieldPermissionType.READ) { + //@ts-ignore + fApi.value?.disabled(true, field) + } + if (permission === FieldPermissionType.WRITE) { + //@ts-ignore + fApi.value?.disabled(false, field) + } + if (permission === FieldPermissionType.NONE) { + //@ts-ignore + fApi.value?.hidden(true, field) + } +} + +/** 提交按钮 */ +const submitForm = async () => { + if (!fApi.value || !props.selectProcessDefinition) { + return + } + // 流程表单校验 + await fApi.value.validate() + // 如果有指定审批人,需要校验 + if (startUserSelectTasks.value?.length > 0) { + for (const userTask of startUserSelectTasks.value) { + if ( + Array.isArray(startUserSelectAssignees.value[userTask.id]) && + startUserSelectAssignees.value[userTask.id].length === 0 + ) + return message.warning(`请选择${userTask.name}的候选人`) + } + } + + // 提交请求 + processInstanceStartLoading.value = true + try { + await ProcessInstanceApi.createProcessInstance({ + processDefinitionId: props.selectProcessDefinition.id, + variables: detailForm.value.value, + startUserSelectAssignees: startUserSelectAssignees.value + }) + // 提示 + message.success('发起流程成功') + // 跳转回去 + delView(unref(currentRoute)) + await push({ + name: 'BpmProcessInstanceMy' + }) + } finally { + processInstanceStartLoading.value = false + } +} + +/** 取消发起审批 */ +const handleCancel = () => { + emit('cancel') +} + +/** 选择发起人 */ +const selectUserConfirm = (id: string, userList: any[]) => { + startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id) +} + +defineExpose({ initProcessInfo }) +</script> + +<style lang="scss" scoped> +$wrap-padding-height: 20px; +$wrap-margin-height: 15px; +$button-height: 51px; +$process-header-height: 105px; + +.processInstance-wrap-main { + height: calc( + 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px + ); + max-height: calc( + 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px + ); + overflow: auto; + + .form-scroll-area { + height: calc( + 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - + $process-header-height - 40px + ); + max-height: calc( + 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - + $process-header-height - 40px + ); + overflow: auto; + } +} + +.form-box { + :deep(.el-card) { + border: none; + } +} +</style> diff --git a/src/views/bpm/processInstance/create/index.vue b/src/views/bpm/processInstance/create/index.vue index 49da30a..284cbdb 100644 --- a/src/views/bpm/processInstance/create/index.vue +++ b/src/views/bpm/processInstance/create/index.vue @@ -1,132 +1,115 @@ <template> - <!-- 第一步,通过流程定义的列表,选择对应的流程 --> - <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 v-if="!selectProcessDefinition"> + <el-input + v-model="searchName" + class="!w-50% mb-15px" + placeholder="请输入流程名称" + clearable + @input="handleQuery" + @clear="handleQuery" + > + <template #prefix> + <Icon icon="ep:search" /> + </template> + </el-input> + <ContentWrap + :class="{ 'process-definition-container': filteredProcessDefinitionList?.length }" + class="position-relative pb-20px h-700px" + v-loading="loading" + > + <el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap"> + <el-col :span="5"> + <div class="flex flex-col"> + <div + v-for="category in availableCategories" + :key="category.code" + class="flex items-center p-10px cursor-pointer text-14px rounded-md" + :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''" + @click="handleCategoryClick(category)" > - <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> + {{ category.name }} + </div> + </div> + </el-col> + <el-col :span="19"> + <el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll"> + <div + class="mb-20px pl-10px" + v-for="(definitions, categoryCode) in processDefinitionGroup" + :key="categoryCode" + :ref="`category-${categoryCode}`" + > + <h3 class="text-18px font-bold mb-10px mt-5px"> + {{ getCategoryName(categoryCode as any) }} + </h3> + <div class="grid grid-cols-3 gap3"> + <el-tooltip + v-for="definition in definitions" + :key="definition.id" + :content="definition.description" + :disabled="!definition.description || definition.description.trim().length === 0" + placement="top" + > + <el-card + shadow="hover" + class="cursor-pointer definition-item-card" + @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-tooltip> + </div> + </div> + </el-scrollbar> + </el-col> + </el-row> + <el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else /> + </ContentWrap> + </template> <!-- 第二步,填写表单,进行流程的提交 --> - <ContentWrap v-else> - <el-card class="box-card"> - <div class="clearfix"> - <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span> - <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined"> - <Icon icon="ep:delete" /> 选择其它流程 - </el-button> - </div> - <el-col :span="16" :offset="6" style="margin-top: 20px"> - <form-create - :rule="detailForm.rule" - v-model:api="fApi" - v-model="detailForm.value" - :option="detailForm.option" - @submit="submitForm" - > - <template #type-startUserSelect> - <el-col :span="24"> - <el-card class="mb-10px"> - <template #header>指定审批人</template> - <el-form - :model="startUserSelectAssignees" - :rules="startUserSelectAssigneesFormRules" - ref="startUserSelectAssigneesFormRef" - > - <el-form-item - v-for="userTask in startUserSelectTasks" - :key="userTask.id" - :label="`任务【${userTask.name}】`" - :prop="userTask.id" - > - <el-select - v-model="startUserSelectAssignees[userTask.id]" - multiple - placeholder="请选择审批人" - > - <el-option - v-for="user in userList" - :key="user.id" - :label="user.nickname" - :value="user.id" - /> - </el-select> - </el-form-item> - </el-form> - </el-card> - </el-col> - </template> - </form-create> - </el-col> - </el-card> - <!-- 流程图预览 --> - <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" /> - </ContentWrap> + <ProcessDefinitionDetail + v-else + ref="processDefinitionDetailRef" + :selectProcessDefinition="selectProcessDefinition" + @cancel="selectProcessDefinition = undefined" + /> </template> + <script lang="ts" setup> 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' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' +import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue' +import { groupBy } from 'lodash-es' defineOptions({ name: 'BpmProcessInstanceCreate' }) +const { proxy } = getCurrentInstance() as any const route = useRoute() // 路由 -const { push, currentRoute } = useRouter() // 路由 const message = useMessage() // 消息 -const { delView } = useTagsViewStore() // 视图操作 -const processInstanceId = route.query.processInstanceId +const searchName = ref('') // 当前搜索关键字 +const processInstanceId: any = route.query.processInstanceId // 流程实例编号。场景:重新发起时 const loading = ref(true) // 加载中 -const categoryList = ref([]) // 分类的列表 -const categoryActive = ref('') // 选中的分类 +const categoryList: any = ref([]) // 分类的列表 +const categoryActive: any = ref({}) // 选中的分类 const processDefinitionList = ref([]) // 流程定义的列表 /** 查询列表 */ const getList = async () => { loading.value = true try { - // 流程分类 - categoryList.value = await CategoryApi.getCategorySimpleList() - if (categoryList.value.length > 0) { - categoryActive.value = categoryList.value[0].code - } - // 流程定义 - processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({ - suspensionState: 1 - }) + // 所有流程分类数据 + await getCategoryList() + // 所有流程定义数据 + await getProcessDefinitionList() // 如果 processInstanceId 非空,说明是重新发起 if (processInstanceId?.length > 0) { @@ -136,7 +119,7 @@ return } const processDefinition = processDefinitionList.value.find( - (item) => item.key == processInstance.processDefinition?.key + (item: any) => item.key == processInstance.processDefinition?.key ) if (!processDefinition) { message.error('重新发起流程失败,原因:流程定义不存在') @@ -149,108 +132,168 @@ } } -/** 选中分类对应的流程定义列表 */ -const categoryProcessDefinitionList = computed(() => { - return processDefinitionList.value.filter((item) => item.category == categoryActive.value) +/** 获取所有流程分类数据 */ +const getCategoryList = async () => { + try { + // 流程分类 + categoryList.value = await CategoryApi.getCategorySimpleList() + } finally { + } +} + +/** 获取所有流程定义数据 */ +const getProcessDefinitionList = async () => { + try { + // 流程定义 + processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({ + suspensionState: 1 + }) + // 初始化过滤列表为全部流程定义 + filteredProcessDefinitionList.value = processDefinitionList.value + + // 在获取完所有数据后,设置第一个有效分类为激活状态 + if (availableCategories.value.length > 0 && !categoryActive.value?.code) { + categoryActive.value = availableCategories.value[0] + } + } finally { + } +} + +/** 搜索流程 */ +const filteredProcessDefinitionList = ref([]) // 用于存储搜索过滤后的流程定义 +const handleQuery = () => { + if (searchName.value.trim()) { + // 如果有搜索关键字,进行过滤 + filteredProcessDefinitionList.value = processDefinitionList.value.filter( + (definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) // 假设搜索依据是流程定义的名称 + ) + } else { + // 如果没有搜索关键字,恢复所有数据 + filteredProcessDefinitionList.value = processDefinitionList.value + } +} + +/** 流程定义的分组 */ +const processDefinitionGroup: any = computed(() => { + if (!processDefinitionList.value?.length) { + return {} + } + + const grouped = groupBy(filteredProcessDefinitionList.value, 'category') + // 按照 categoryList 的顺序重新组织数据 + const orderedGroup = {} + categoryList.value.forEach((category: any) => { + if (grouped[category.code]) { + orderedGroup[category.code] = grouped[category.code] + } + }) + return orderedGroup }) -// ========== 表单相关 ========== -const fApi = ref<ApiAttrs>() -const detailForm = ref({ - rule: [], - option: {}, - value: {} -}) // 流程表单详情 -const selectProcessDefinition = ref() // 选择的流程定义 +/** 左侧分类切换 */ +const handleCategoryClick = (category: any) => { + categoryActive.value = category + const categoryRef = proxy.$refs[`category-${category.code}`] // 获取点击分类对应的 DOM 元素 + if (categoryRef?.length) { + const scrollWrapper = proxy.$refs.scrollWrapper // 获取右侧滚动容器 + const categoryOffsetTop = categoryRef[0].offsetTop -// 指定审批人 -const bpmnXML = ref(null) // BPMN 数据 -const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表 -const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据 -const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref -const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules -const userList = ref<any[]>([]) // 用户列表 + // 滚动到对应位置 + scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' }) + } +} + +/** 通过分类 code 获取对应的名称 */ +const getCategoryName = (categoryCode: string) => { + return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name +} + +// ========== 表单相关 ========== +const selectProcessDefinition = ref() // 选择的流程定义 +const processDefinitionDetailRef = ref() /** 处理选择流程的按钮操作 **/ -const handleSelect = async (row, formVariables) => { +const handleSelect = async (row, formVariables?) => { // 设置选择的流程 selectProcessDefinition.value = row + // 初始化流程定义详情 + await nextTick() + processDefinitionDetailRef.value?.initProcessInfo(row, formVariables) +} - // 重置指定审批人 - startUserSelectTasks.value = [] - startUserSelectAssignees.value = {} - startUserSelectAssigneesFormRules.value = {} +/** 处理滚动事件,和左侧分类联动 */ +const handleScroll = (e: any) => { + // 直接使用事件对象获取滚动位置 + const scrollTop = e.scrollTop - // 情况一:流程表单 - if (row.formType == 10) { - // 设置表单 - setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) - // 加载流程图 - const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id) - if (processDefinitionDetail) { - bpmnXML.value = processDefinitionDetail.bpmnXml - startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks - - // 设置指定审批人 - if (startUserSelectTasks.value?.length > 0) { - 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' } - ] + // 获取所有分类区域的位置信息 + const categoryPositions = categoryList.value + .map((category: CategoryVO) => { + const categoryRef = proxy.$refs[`category-${category.code}`] + if (categoryRef?.[0]) { + return { + code: category.code, + offsetTop: categoryRef[0].offsetTop, + height: categoryRef[0].offsetHeight } - // 加载用户列表 - userList.value = await UserApi.getSimpleUserList() } + return null + }) + .filter(Boolean) + + // 查找当前滚动位置对应的分类 + let currentCategory = categoryPositions[0] + for (const position of categoryPositions) { + // 为了更好的用户体验,可以添加一个缓冲区域(比如 50px) + if (scrollTop >= position.offsetTop - 50) { + currentCategory = position + } else { + break } - // 情况二:业务表单 - } else if (row.formCustomCreatePath) { - await push({ - path: row.formCustomCreatePath - }) - // 这里暂时无需加载流程图,因为跳出到另外个 Tab; + } + + // 更新当前 active 的分类 + if (currentCategory && categoryActive.value.code !== currentCategory.code) { + categoryActive.value = categoryList.value.find( + (c: CategoryVO) => c.code === currentCategory.code + ) } } -/** 提交按钮 */ -const submitForm = async (formData) => { - if (!fApi.value || !selectProcessDefinition.value) { - return - } - // 如果有指定审批人,需要校验 - if (startUserSelectTasks.value?.length > 0) { - await startUserSelectAssigneesFormRef.value.validate() +/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */ +const availableCategories = computed(() => { + if (!categoryList.value?.length || !processDefinitionGroup.value) { + return [] } - // 提交请求 - fApi.value.btn.loading(true) - try { - await ProcessInstanceApi.createProcessInstance({ - processDefinitionId: selectProcessDefinition.value.id, - variables: formData, - startUserSelectAssignees: startUserSelectAssignees.value - }) - // 提示 - message.success('发起流程成功') - // 跳转回去 - delView(unref(currentRoute)) - await push({ - name: 'BpmProcessInstanceMy' - }) - } finally { - fApi.value.btn.loading(false) - } -} + // 获取所有有流程的分类代码 + const availableCategoryCodes = Object.keys(processDefinitionGroup.value) + + // 过滤出有流程的分类 + return categoryList.value.filter((category: CategoryVO) => + availableCategoryCodes.includes(category.code) + ) +}) /** 初始化 */ onMounted(() => { getList() }) </script> + +<style lang="scss" scoped> +.process-definition-container::before { + content: ''; + border-left: 1px solid #e6e6e6; + position: absolute; + left: 20.8%; + height: 100%; +} +:deep() { + .definition-item-card { + .el-card__body { + padding: 14px; + } + } +} +</style> diff --git a/src/views/bpm/processInstance/create/index_old.vue b/src/views/bpm/processInstance/create/index_old.vue new file mode 100644 index 0000000..856a289 --- /dev/null +++ b/src/views/bpm/processInstance/create/index_old.vue @@ -0,0 +1,266 @@ +<template> + + <!-- 第一步,通过流程定义的列表,选择对应的流程 --> + <ContentWrap v-if="!selectProcessDefinition" v-loading="loading"> + <el-tabs tab-position="left" v-model="categoryActive"> + <el-tab-pane + :label="category.name" + :name="category.code" + :key="category.code" + v-for="category in categoryList" + > + <el-row :gutter="20"> + <el-col + :lg="6" + :sm="12" + :xs="24" + v-for="definition in categoryProcessDefinitionList" + :key="definition.id" + > + <el-card + shadow="hover" + class="mb-20px cursor-pointer" + @click="handleSelect(definition)" + > + <template #default> + <div class="flex"> + <el-image :src="definition.icon" class="w-32px h-32px" /> + <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text> + </div> + </template> + </el-card> + </el-col> + </el-row> + </el-tab-pane> + </el-tabs> + </ContentWrap> + + <!-- 第二步,填写表单,进行流程的提交 --> + <ContentWrap v-else> + <el-card class="box-card"> + <div class="clearfix"> + <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span> + <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined"> + <Icon icon="ep:delete" /> 选择其它流程 + </el-button> + </div> + <el-col :span="16" :offset="6" style="margin-top: 20px"> + <form-create + :rule="detailForm.rule" + v-model:api="fApi" + v-model="detailForm.value" + :option="detailForm.option" + @submit="submitForm" + > + <template #type-startUserSelect> + <el-col :span="24"> + <el-card class="mb-10px"> + <template #header>指定审批人</template> + <el-form + :model="startUserSelectAssignees" + :rules="startUserSelectAssigneesFormRules" + ref="startUserSelectAssigneesFormRef" + > + <el-form-item + v-for="userTask in startUserSelectTasks" + :key="userTask.id" + :label="`任务【${userTask.name}】`" + :prop="userTask.id" + > + <el-select + v-model="startUserSelectAssignees[userTask.id]" + multiple + placeholder="请选择审批人" + > + <el-option + v-for="user in userList" + :key="user.id" + :label="user.nickname" + :value="user.id" + /> + </el-select> + </el-form-item> + </el-form> + </el-card> + </el-col> + </template> + </form-create> + </el-col> + </el-card> + <!-- 流程图预览 --> + <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" /> + </ContentWrap> +</template> +<script lang="ts" setup> +import * as DefinitionApi from '@/api/bpm/definition' +import * as ProcessInstanceApi from '@/api/bpm/processInstance' +import { decodeFields, setConfAndFields2 } from '@/utils/formCreate' +import type { ApiAttrs } from '@form-create/element-ui/types/config' +import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue' +import { CategoryApi } from '@/api/bpm/category' +import { useTagsViewStore } from '@/store/modules/tagsView' +import * as UserApi from '@/api/system/user' + +defineOptions({ name: 'BpmProcessInstanceCreate' }) + +const route = useRoute() // 路由 +const { push, currentRoute } = useRouter() // 路由 +const message = useMessage() // 消息 +const { delView } = useTagsViewStore() // 视图操作 + +const processInstanceId = route.query.processInstanceId +const loading = ref(true) // 加载中 +const categoryList = ref([]) // 分类的列表 +const categoryActive = ref('') // 选中的分类 +const processDefinitionList = ref([]) // 流程定义的列表 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + // 流程分类 + categoryList.value = await CategoryApi.getCategorySimpleList() + if (categoryList.value.length > 0) { + categoryActive.value = categoryList.value[0].code + } + // 流程定义 + processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({ + suspensionState: 1 + }) + + // 如果 processInstanceId 非空,说明是重新发起 + if (processInstanceId?.length > 0) { + const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId) + if (!processInstance) { + message.error('重新发起流程失败,原因:流程实例不存在') + return + } + const processDefinition = processDefinitionList.value.find( + (item) => item.key == processInstance.processDefinition?.key + ) + if (!processDefinition) { + message.error('重新发起流程失败,原因:流程定义不存在') + return + } + await handleSelect(processDefinition, processInstance.formVariables) + } + } finally { + loading.value = false + } +} + +/** 选中分类对应的流程定义列表 */ +const categoryProcessDefinitionList = computed(() => { + return processDefinitionList.value.filter((item) => item.category == categoryActive.value) +}) + +// ========== 表单相关 ========== +const fApi = ref<ApiAttrs>() +const detailForm = ref({ + rule: [], + option: {}, + value: {} +}) // 流程表单详情 +const selectProcessDefinition = ref() // 选择的流程定义 + +// 指定审批人 +const bpmnXML = ref(null) // BPMN 数据 +const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表 +const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据 +const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref +const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules +const userList = ref<any[]>([]) // 用户列表 + +/** 处理选择流程的按钮操作 **/ +const handleSelect = async (row, formVariables) => { + // 设置选择的流程 + selectProcessDefinition.value = row + + // 重置指定审批人 + startUserSelectTasks.value = [] + startUserSelectAssignees.value = {} + startUserSelectAssigneesFormRules.value = {} + + // 情况一:流程表单 + if (row.formType == 10) { + // 设置表单 + // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。 + // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。 + // 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!! + const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field) + for (const key in formVariables) { + if (!allowedFields.includes(key)) { + delete formVariables[key] + } + } + setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) + + // 加载流程图 + const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id) + if (processDefinitionDetail) { + bpmnXML.value = processDefinitionDetail.bpmnXml + startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks + + // 设置指定审批人 + if (startUserSelectTasks.value?.length > 0) { + detailForm.value.rule.push({ + type: 'startUserSelect', + props: { + title: '指定审批人' + } + }) + // 设置校验规则 + for (const userTask of startUserSelectTasks.value) { + startUserSelectAssignees.value[userTask.id] = [] + startUserSelectAssigneesFormRules.value[userTask.id] = [ + { required: true, message: '请选择审批人', trigger: 'blur' } + ] + } + // 加载用户列表 + userList.value = await UserApi.getSimpleUserList() + } + } + // 情况二:业务表单 + } else if (row.formCustomCreatePath) { + await push({ + path: row.formCustomCreatePath + }) + // 这里暂时无需加载流程图,因为跳出到另外个 Tab; + } +} + +/** 提交按钮 */ +const submitForm = async (formData) => { + if (!fApi.value || !selectProcessDefinition.value) { + return + } + // 如果有指定审批人,需要校验 + if (startUserSelectTasks.value?.length > 0) { + await startUserSelectAssigneesFormRef.value.validate() + } + + // 提交请求 + fApi.value.btn.loading(true) + try { + await ProcessInstanceApi.createProcessInstance({ + processDefinitionId: selectProcessDefinition.value.id, + variables: formData, + startUserSelectAssignees: startUserSelectAssignees.value + }) + // 提示 + message.success('发起流程成功') + // 跳转回去 + delView(unref(currentRoute)) + await push({ + name: 'BpmProcessInstanceMy' + }) + } finally { + fApi.value.btn.loading(false) + } +} + +/** 初始化 */ +onMounted(() => { + getList() +}) +</script> diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue index 8912593..781263d 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue @@ -1,54 +1,61 @@ <template> <el-card v-loading="loading" class="box-card"> - <template #header> - <span class="el-icon-picture-outline">流程图</span> - </template> - <MyProcessViewer - key="designer" - :activityData="activityList" - :prefix="bpmnControlForm.prefix" - :processInstanceData="processInstance" - :taskData="tasks" - :value="bpmnXml" - v-bind="bpmnControlForm" - /> + <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" /> </el-card> </template> <script lang="ts" setup> import { propTypes } from '@/utils/propTypes' import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package' -import * as ActivityApi from '@/api/bpm/activity' defineOptions({ name: 'BpmProcessInstanceBpmnViewer' }) const props = defineProps({ - loading: propTypes.bool, // 是否加载中 - id: propTypes.string, // 流程实例的编号 - processInstance: propTypes.any, // 流程实例的信息 - tasks: propTypes.array, // 流程任务的数组 - bpmnXml: propTypes.string // BPMN XML + loading: propTypes.bool.def(false), // 是否加载中 + bpmnXml: propTypes.string, // BPMN XML + modelView: propTypes.object }) -const bpmnControlForm = ref({ - prefix: 'flowable' -}) -const activityList = ref([]) // 任务列表 +const view = ref({ + bpmnXml: '' +}) // BPMN 流程图数据 + /** 只有 loading 完成时,才去加载流程列表 */ watch( - () => props.loading, - async (value) => { - if (value && props.id) { - activityList.value = await ActivityApi.getActivityList({ - processInstanceId: props.id - }) + () => props.modelView, + async (newModelView) => { + // 加载最新 + if (newModelView) { + //@ts-ignore + view.value = newModelView } } ) + +/** 监听 bpmnXml */ +watch( + () => props.bpmnXml, + (value) => { + view.value.bpmnXml = value + } +) </script> -<style> +<style lang="scss" scoped> .box-card { + height: 100%; width: 100%; - margin-bottom: 20px; + margin-bottom: 0; + + :deep(.el-card__body) { + height: 100%; + padding: 0; + } + + :deep(.process-viewer) { + height: 100% !important; + min-height: 100%; + width: 100%; + overflow: auto; + } } </style> diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue new file mode 100644 index 0000000..1b3ebc5 --- /dev/null +++ b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue @@ -0,0 +1,989 @@ +<template> + <div + class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container" + > + <!-- 【通过】按钮 --> + <el-popover + :visible="popOverVisible.approve" + placement="top-end" + :width="420" + trigger="click" + v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.APPROVE)" + > + <template #reference> + <el-button plain type="success" @click="openPopover('approve')"> + <Icon icon="ep:select" /> {{ getButtonDisplayName(OperationButtonType.APPROVE) }} + </el-button> + </template> + <!-- 审批表单 --> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="approveFormRef" + :model="approveReasonForm" + :rules="approveReasonRule" + label-width="100px" + > + <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px"> + <template #header> + <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}】 </span> + </template> + <form-create + v-model="approveForm.value" + v-model:api="approveFormFApi" + :option="approveForm.option" + :rule="approveForm.rule" + /> + </el-card> + <el-form-item label="审批意见" prop="reason"> + <el-input + v-model="approveReasonForm.reason" + placeholder="请输入审批意见" + type="textarea" + :rows="4" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="success" @click="handleAudit(true, approveFormRef)"> + {{ getButtonDisplayName(OperationButtonType.APPROVE) }} + </el-button> + <el-button @click="closePropover('approve', approveFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + + <!-- 【拒绝】按钮 --> + <el-popover + :visible="popOverVisible.reject" + placement="top-end" + :width="420" + trigger="click" + v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.REJECT)" + > + <template #reference> + <el-button class="mr-20px" plain type="danger" @click="openPopover('reject')"> + <Icon icon="ep:close" /> {{ getButtonDisplayName(OperationButtonType.REJECT) }} + </el-button> + </template> + <!-- 审批表单 --> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="rejectFormRef" + :model="rejectReasonForm" + :rules="rejectReasonRule" + label-width="100px" + > + <el-form-item label="审批意见" prop="reason"> + <el-input + v-model="rejectReasonForm.reason" + placeholder="请输入审批意见" + type="textarea" + :rows="4" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="danger" @click="handleAudit(false,rejectFormRef)"> + {{ getButtonDisplayName(OperationButtonType.REJECT) }} + </el-button> + <el-button @click="closePropover('reject', rejectFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + + <!-- 【抄送】按钮 --> + <el-popover + :visible="popOverVisible.copy" + placement="top-start" + :width="420" + trigger="click" + v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.COPY)" + > + <template #reference> + <div @click="openPopover('copy')" class="hover-bg-gray-100 rounded-xl p-6px"> + <Icon :size="14" icon="svg-icon:send" /> + {{ getButtonDisplayName(OperationButtonType.COPY) }} + </div> + </template> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="copyFormRef" + :model="copyForm" + :rules="copyFormRule" + label-width="100px" + > + <el-form-item label="抄送人" prop="copyUserIds"> + <el-select + v-model="copyForm.copyUserIds" + clearable + style="width: 100%" + multiple + placeholder="请选择抄送人" + > + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="抄送意见" prop="copyReason"> + <el-input + v-model="copyForm.copyReason" + clearable + placeholder="请输入抄送意见" + type="textarea" + :rows="3" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="primary" @click="handleCopy"> + {{ getButtonDisplayName(OperationButtonType.COPY) }} + </el-button> + <el-button @click="closePropover('copy', copyFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + + <!-- 【转办】按钮 --> + <el-popover + :visible="popOverVisible.transfer" + placement="top-start" + :width="420" + trigger="click" + v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.TRANSFER)" + > + <template #reference> + <div @click="openPopover('transfer')" class="hover-bg-gray-100 rounded-xl p-6px"> + <Icon :size="14" icon="fa:share-square-o" /> + {{ getButtonDisplayName(OperationButtonType.TRANSFER) }} + </div> + </template> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="transferFormRef" + :model="transferForm" + :rules="transferFormRule" + label-width="100px" + > + <el-form-item label="新审批人" prop="assigneeUserId"> + <el-select v-model="transferForm.assigneeUserId" clearable style="width: 100%"> + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="审批意见" prop="reason"> + <el-input + v-model="transferForm.reason" + clearable + placeholder="请输入审批意见" + type="textarea" + :rows="3" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="primary" @click="handleTransfer()"> + {{ getButtonDisplayName(OperationButtonType.TRANSFER) }} + </el-button> + <el-button @click="closePropover('transfer', transferFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + + <!-- 【委派】按钮 --> + <el-popover + :visible="popOverVisible.delegate" + placement="top-start" + :width="420" + trigger="click" + v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.DELEGATE)" + > + <template #reference> + <div @click="openPopover('delegate')" class="hover-bg-gray-100 rounded-xl p-6px"> + <Icon :size="14" icon="ep:position" /> + {{ getButtonDisplayName(OperationButtonType.DELEGATE) }} + </div> + </template> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="delegateFormRef" + :model="delegateForm" + :rules="delegateFormRule" + label-width="100px" + > + <el-form-item label="接收人" prop="delegateUserId"> + <el-select v-model="delegateForm.delegateUserId" clearable style="width: 100%"> + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="审批意见" prop="reason"> + <el-input + v-model="delegateForm.reason" + clearable + placeholder="请输入审批意见" + type="textarea" + :rows="3" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="primary" @click="handleDelegate()"> + {{ getButtonDisplayName(OperationButtonType.DELEGATE) }} + </el-button> + <el-button @click="closePropover('delegate', delegateFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + + <!-- 【加签】按钮 当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> + <el-popover + :visible="popOverVisible.addSign" + placement="top-start" + :width="420" + trigger="click" + v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.ADD_SIGN)" + > + <template #reference> + <div @click="openPopover('addSign')" class="hover-bg-gray-100 rounded-xl p-6px"> + <Icon :size="14" icon="ep:plus" /> + {{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }} + </div> + </template> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="addSignFormRef" + :model="addSignForm" + :rules="addSignFormRule" + label-width="100px" + > + <el-form-item label="加签处理人" prop="addSignUserIds"> + <el-select v-model="addSignForm.addSignUserIds" multiple clearable style="width: 100%"> + <el-option + v-for="item in userOptions" + :key="item.id" + :label="item.nickname" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="审批意见" prop="reason"> + <el-input + v-model="addSignForm.reason" + clearable + placeholder="请输入审批意见" + type="textarea" + :rows="3" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('before')"> + 向前{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }} + </el-button> + <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')"> + 向后{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }} + </el-button> + <el-button @click="closePropover('addSign', addSignFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + + <!-- 【减签】按钮 --> + <el-popover + :visible="popOverVisible.deleteSign" + placement="top-start" + :width="420" + trigger="click" + v-if="runningTask?.children.length > 0" + > + <template #reference> + <div @click="openPopover('deleteSign')" class="hover-bg-gray-100 rounded-xl p-6px"> + <Icon :size="14" icon="ep:semi-select" /> 减签 + </div> + </template> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="deleteSignFormRef" + :model="deleteSignForm" + :rules="deleteSignFormRule" + label-width="100px" + > + <el-form-item label="减签人员" prop="deleteSignTaskId"> + <el-select v-model="deleteSignForm.deleteSignTaskId" clearable style="width: 100%"> + <el-option + v-for="item in runningTask.children" + :key="item.id" + :label="getDeleteSignUserLabel(item)" + :value="item.id" + /> + </el-select> + </el-form-item> + <el-form-item label="审批意见" prop="reason"> + <el-input + v-model="deleteSignForm.reason" + clearable + placeholder="请输入审批意见" + type="textarea" + :rows="3" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()"> + 减签 + </el-button> + <el-button @click="closePropover('deleteSign', deleteSignFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + + <!-- 【退回】按钮 --> + <el-popover + :visible="popOverVisible.return" + placement="top-start" + :width="420" + trigger="click" + v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)" + > + <template #reference> + <div @click="openPopover('return')" class="hover-bg-gray-100 rounded-xl p-6px"> + <Icon :size="14" icon="ep:back" /> + {{ getButtonDisplayName(OperationButtonType.RETURN) }} + </div> + </template> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="returnFormRef" + :model="returnForm" + :rules="returnFormRule" + label-width="100px" + > + <el-form-item label="退回节点" prop="targetTaskDefinitionKey"> + <el-select v-model="returnForm.targetTaskDefinitionKey" clearable style="width: 100%"> + <el-option + v-for="item in returnList" + :key="item.taskDefinitionKey" + :label="item.name" + :value="item.taskDefinitionKey" + /> + </el-select> + </el-form-item> + <el-form-item label="退回理由" prop="returnReason"> + <el-input + v-model="returnForm.returnReason" + clearable + placeholder="请输入退回理由" + type="textarea" + :rows="3" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="primary" @click="handleReturn()"> + {{ getButtonDisplayName(OperationButtonType.RETURN) }} + </el-button> + <el-button @click="closePropover('return', returnFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + + <!--【取消】按钮 这个对应发起人的取消, 只有发起人可以取消 --> + <el-popover + :visible="popOverVisible.cancel" + placement="top-start" + :width="420" + trigger="click" + v-if=" + userId === processInstance?.startUser?.id && !isEndProcessStatus(processInstance?.status) + " + > + <template #reference> + <div @click="openPopover('cancel')" class="hover-bg-gray-100 rounded-xl p-6px"> + <Icon :size="14" icon="fa:mail-reply" /> 取消 + </div> + </template> + <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading"> + <el-form + label-position="top" + class="mb-auto" + ref="cancelFormRef" + :model="cancelForm" + :rules="cancelFormRule" + label-width="100px" + > + <el-form-item label="取消理由" prop="cancelReason"> + <span class="text-#878c93 text-12px"> 取消后,该审批流程将自动结束</span> + <el-input + v-model="cancelForm.cancelReason" + clearable + placeholder="请输入取消理由" + type="textarea" + :rows="3" + /> + </el-form-item> + <el-form-item> + <el-button :disabled="formLoading" type="primary" @click="handleCancel()"> + 确认 + </el-button> + <el-button @click="closePropover('cancel', cancelFormRef)"> 取消 </el-button> + </el-form-item> + </el-form> + </div> + </el-popover> + <!-- 【再次提交】 按钮--> + <div + @click="handleReCreate()" + class="hover-bg-gray-100 rounded-xl p-6px" + v-if=" + userId === processInstance?.startUser?.id && + isEndProcessStatus(processInstance?.status) && + processDefinition?.formType === 10 + " + > + <Icon :size="14" icon="ep:refresh" /> 再次提交 + </div> + </div> +</template> +<script lang="ts" setup> +import { useUserStoreWithOut } from '@/store/modules/user' +import { setConfAndFields2 } from '@/utils/formCreate' +import * as TaskApi from '@/api/bpm/task' +import * as ProcessInstanceApi from '@/api/bpm/processInstance' +import * as UserApi from '@/api/system/user' +import { + OperationButtonType, + OPERATION_BUTTON_NAME +} from '@/components/SimpleProcessDesignerV2/src/consts' +import { BpmProcessInstanceStatus, BpmModelFormType } from '@/utils/constants' +import type { FormInstance, FormRules } from 'element-plus' +defineOptions({ name: 'ProcessInstanceBtnContainer' }) + +const router = useRouter() // 路由 +const message = useMessage() // 消息弹窗 + +const userId = useUserStoreWithOut().getUser.id // 当前登录的编号 +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 + +const props = defineProps< { + processInstance: any, // 流程实例信息 + processDefinition: any, // 流程定义信息 + userOptions: UserApi.UserVO[], + normalForm: any, // 流程表单 formCreate + normalFormApi: any, // 流程表单 formCreate Api + writableFields: string[] // 流程表单可以编辑的字段 +}>() + +const formLoading = ref(false) // 表单加载中 +const popOverVisible = ref({ + approve: false, + reject: false, + transfer: false, + delegate: false, + addSign: false, + return: false, + copy: false, + cancel: false, + deleteSign: false +}) // 气泡卡是否展示 +const returnList = ref([] as any) // 退回节点 + +// ========== 审批信息 ========== +const runningTask = ref<any>() // 运行中的任务 +const approveForm = ref<any>({}) // 审批通过时,额外的补充信息 +const approveFormFApi = ref<any>({}) // approveForms 的 fAPi + +// 审批通过意见表单 +const approveFormRef = ref<FormInstance>() +const approveReasonForm = reactive({ + reason: '' +}) +const approveReasonRule = reactive<FormRules<typeof approveReasonForm>>({ + reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], +}) +// 拒绝表单 +const rejectFormRef = ref<FormInstance>() +const rejectReasonForm = reactive({ + reason: '' +}) +const rejectReasonRule = reactive<FormRules<typeof rejectReasonForm>>({ + reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], +}) + +// 抄送表单 +const copyFormRef = ref<FormInstance>() +const copyForm = reactive({ + copyUserIds: [], + copyReason: '' +}) +const copyFormRule = reactive<FormRules<typeof copyForm>>({ + copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }] +}) + +// 转办表单 +const transferFormRef = ref<FormInstance>() +const transferForm = reactive({ + assigneeUserId: undefined, + reason: '' +}) +const transferFormRule = reactive<FormRules<typeof transferForm>>({ + assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }], + reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], +}) + +// 委派表单 +const delegateFormRef = ref<FormInstance>() +const delegateForm = reactive({ + delegateUserId: undefined, + reason: '' +}) +const delegateFormRule = reactive<FormRules<typeof delegateForm>>({ + delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }], + reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], +}) + +// 加签表单 +const addSignFormRef = ref<FormInstance>() +const addSignForm = reactive({ + addSignUserIds: undefined, + reason: '' +}) +const addSignFormRule = reactive<FormRules<typeof addSignForm>>({ + addSignUserIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }], + reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], +}) + +// 减签表单 +const deleteSignFormRef = ref<FormInstance>() +const deleteSignForm = reactive({ + deleteSignTaskId: undefined, + reason: '' +}) +const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({ + deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }], + reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], +}) + +// 退回表单 +const returnFormRef = ref<FormInstance>() +const returnForm = reactive({ + targetTaskDefinitionKey: undefined, + returnReason: '' +}) +const returnFormRule = reactive<FormRules<typeof returnForm>>({ + targetTaskDefinitionKey: [{ required: true, message: '退回节点不能为空', trigger: 'change' }], + returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }] +}) + +// 取消表单 +const cancelFormRef = ref<FormInstance>() +const cancelForm = reactive({ + cancelReason: '' +}) +const cancelFormRule = reactive<FormRules<typeof cancelForm>>({ + cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }], +}) + +/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */ +watch( + () => approveFormFApi.value, + (val) => { + val?.btn?.show(false) + val?.resetBtn?.show(false) + }, + { + deep: true + } +) + +/** 弹出气泡卡 */ +const openPopover = async (type: string) => { + if (type === 'approve') { + // 校验流程表单 + const valid = await validateNormalForm(); + if (!valid) { + message.warning('表单校验不通过,请先完善表单!!') + return; + } + } + if (type === 'return') { + // 获取退回节点 + returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id) + if (returnList.value.length === 0) { + message.warning('当前没有可退回的节点') + return + } + } + Object.keys(popOverVisible.value).forEach((item) => { + popOverVisible.value[item] = item === type + }) + // await nextTick() + // formRef.value.resetFields() +} + +/** 关闭气泡卡 */ +const closePropover = (type: string, formRef: FormInstance | undefined) => { + if (formRef) { + formRef.resetFields() + } + popOverVisible.value[type] = false +} + +/** 处理审批通过和不通过的操作 */ +const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => { + formLoading.value = true + try { + // 校验表单 + if (!formRef) return + await formRef.validate() + if (pass) { + // 获取修改的流程变量, 暂时只支持流程表单 + const variables = getUpdatedProcessInstanceVaiables(); + // 审批通过数据 + const data = { + id: runningTask.value.id, + reason: approveReasonForm.reason, + variables // 审批通过, 把修改的字段值赋于流程实例变量 + } + // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交 + // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突 + const formCreateApi = approveFormFApi.value + if (Object.keys(formCreateApi)?.length > 0) { + await formCreateApi.validate() + // @ts-ignore + data.variables = approveForm.value.value + } + await TaskApi.approveTask(data) + popOverVisible.value.approve = false + message.success('审批通过成功') + } else { + // 审批不通过数据 + const data = { + id: runningTask.value.id, + reason: rejectReasonForm.reason, + } + await TaskApi.rejectTask(data) + popOverVisible.value.reject = false + message.success('审批不通过成功') + } + // 重置表单 + formRef.resetFields() + // 加载最新数据 + reload() + } finally { + formLoading.value = false + } +} + +/** 处理抄送 */ +const handleCopy = async () => { + formLoading.value = true + try { + // 1. 校验表单 + if (!copyFormRef.value) return + await copyFormRef.value.validate() + // 2. 提交抄送 + const data = { + id: runningTask.value.id, + reason: copyForm.copyReason, + copyUserIds:copyForm.copyUserIds + } + await TaskApi.copyTask(data) + copyFormRef.value.resetFields() + popOverVisible.value.copy = false + message.success('操作成功') + } finally { + formLoading.value = false + } +} + +/** 处理转交 */ +const handleTransfer = async () => { + formLoading.value = true + try { + // 1.1 校验表单 + if (!transferFormRef.value) return + await transferFormRef.value.validate() + // 1.2 提交转交 + const data = { + id: runningTask.value.id, + reason: transferForm.reason, + assigneeUserId: transferForm.assigneeUserId + } + await TaskApi.transferTask(data) + transferFormRef.value.resetFields() + popOverVisible.value.transfer = false + message.success('操作成功') + // 2. 加载最新数据 + reload() + } finally { + formLoading.value = false + } +} + +/** 处理委派 */ +const handleDelegate = async () => { + formLoading.value = true + try { + + // 1.1 校验表单 + if (!delegateFormRef.value) return + await delegateFormRef.value.validate() + // 1.2 处理委派 + const data = { + id: runningTask.value.id, + reason: delegateForm.reason, + delegateUserId: delegateForm.delegateUserId + } + + await TaskApi.delegateTask(data) + popOverVisible.value.delegate = false + delegateFormRef.value.resetFields() + message.success('操作成功') + // 2. 加载最新数据 + reload() + } finally { + formLoading.value = false + } +} + +/** 处理加签 */ +const handlerAddSign = async (type: string) => { + formLoading.value = true + try { + // 1.1 校验表单 + if (!addSignFormRef.value) return + await addSignFormRef.value.validate() + // 1.2 提交加签 + const data = { + id: runningTask.value.id, + type, + reason: addSignForm.reason, + userIds: addSignForm.addSignUserIds + } + await TaskApi.signCreateTask(data) + message.success('操作成功') + addSignFormRef.value.resetFields() + popOverVisible.value.addSign = false + // 2 加载最新数据 + reload() + } finally { + formLoading.value = false + } +} + +/** 处理退回 */ +const handleReturn = async () => { + formLoading.value = true + try { + // 1.1 校验表单 + if (!returnFormRef.value) return + await returnFormRef.value.validate() + // 1.2 提交退回 + const data = { + id: runningTask.value.id, + reason: returnForm.returnReason, + targetTaskDefinitionKey: returnForm.targetTaskDefinitionKey + } + + await TaskApi.returnTask(data) + popOverVisible.value.return = false + returnFormRef.value.resetFields() + message.success('操作成功') + // 2 重新加载数据 + reload() + } finally { + formLoading.value = false + } +} + +/** 处理取消 */ +const handleCancel = async () => { + formLoading.value = true + try { + // 1.1 校验表单 + if (!cancelFormRef.value) return + await cancelFormRef.value.validate() + // 1.2 提交取消 + await ProcessInstanceApi.cancelProcessInstanceByStartUser( + props.processInstance.id, + cancelForm.cancelReason + ) + popOverVisible.value.return = false + message.success('操作成功') + cancelFormRef.value.resetFields() + // 2 重新加载数据 + reload() + } finally { + formLoading.value = false + } +} + +/** 处理再次提交 */ +const handleReCreate = async () => { + // 跳转发起流程界面 + await router.push({ + name: 'BpmProcessInstanceCreate', + query: { processInstanceId: props.processInstance?.id } + }) +} + +/** 获取减签人员标签 */ +const getDeleteSignUserLabel = (task: any): string => { + const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName + const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname + return `${nickname} ( 所属部门:${deptName} )` +} +/** 处理减签 */ +const handlerDeleteSign = async () => { + formLoading.value = true + try { + // 1.1 校验表单 + if (!deleteSignFormRef.value) return + await deleteSignFormRef.value.validate() + // 1.2 提交减签 + const data = { + id: deleteSignForm.deleteSignTaskId, + reason: deleteSignForm.reason + } + await TaskApi.signDeleteTask(data) + message.success('减签成功') + deleteSignFormRef.value.resetFields() + popOverVisible.value.deleteSign = false + // 2 加载最新数据 + reload() + } finally { + formLoading.value = false + } +} +/** 重新加载数据 */ +const reload = () => { + emit('success') +} + +/** 任务是否为处理中状态 */ +const isHandleTaskStatus = () => { + let canHandle = false + if (TaskApi.TaskStatusEnum.RUNNING === runningTask.value?.status) { + canHandle = true + } + return canHandle +} + +/** 流程状态是否为结束状态 */ +const isEndProcessStatus = (status: number) => { + let isEndStatus = false + if ( + BpmProcessInstanceStatus.APPROVE === status || + BpmProcessInstanceStatus.REJECT === status || + BpmProcessInstanceStatus.CANCEL === status + ) { + isEndStatus = true + } + return isEndStatus +} + +/** 是否显示按钮 */ +const isShowButton = (btnType: OperationButtonType): boolean => { + let isShow = true + if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) { + isShow = runningTask.value.buttonsSetting[btnType].enable + } + return isShow +} + +/** 获取按钮的显示名称 */ +const getButtonDisplayName = (btnType: OperationButtonType) => { + let displayName = OPERATION_BUTTON_NAME.get(btnType) + if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) { + displayName = runningTask.value.buttonsSetting[btnType].displayName + } + return displayName +} + +const loadTodoTask = (task: any) => { + approveForm.value = {} + approveFormFApi.value = {} + runningTask.value = task + // 处理 approve 表单. + if (task && task.formId && task.formConf) { + const tempApproveForm = {} + setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables) + approveForm.value = tempApproveForm + } else { + approveForm.value = {} // 占位,避免为空 + } +} + +/** 校验流程表单 */ +const validateNormalForm = async () => { + if (props.processDefinition?.formType === BpmModelFormType.NORMAL) { + let valid = true + try { + await props.normalFormApi?.validate() + } catch { + valid = false; + } + return valid; + } else { + return true; + } +} +/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */ +const getUpdatedProcessInstanceVaiables = ()=> { + const variables = {} + props.writableFields.forEach( (field) => { + const fieldValue = props.normalFormApi.getValue(field) + variables[field] = fieldValue; + }) + return variables +} + +defineExpose({ loadTodoTask }) +</script> + +<style lang="scss" scoped> +:deep(.el-affix--fixed) { + background-color: var(--el-bg-color); +} + +.btn-container { + > div { + display: flex; + margin: 0 8px; + cursor: pointer; + align-items: center; + + &:hover { + color: #6db5ff; + } + } +} +</style> diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue b/src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue new file mode 100644 index 0000000..0808bec --- /dev/null +++ b/src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue @@ -0,0 +1,168 @@ +<template> + <div v-loading="loading" class="process-viewer-container"> + <SimpleProcessViewer + :flow-node="simpleModel" + :tasks="tasks" + :process-instance="processInstance" + class="process-viewer" + /> + </div> +</template> +<script lang="ts" setup> +import { propTypes } from '@/utils/propTypes' +import { TaskStatusEnum } from '@/api/bpm/task' +import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts' +import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/' +defineOptions({ name: 'BpmProcessInstanceSimpleViewer' }) + +const props = defineProps({ + loading: propTypes.bool.def(false), // 是否加载中 + modelView: propTypes.object, + simpleJson: propTypes.string // Simple 模型结构数据 (json 格式) +}) +const simpleModel = ref() +// 用户任务 +const tasks = ref([]) +// 流程实例 +const processInstance = ref() + +/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */ +watch( + () => props.modelView, + async (newModelView) => { + if (newModelView) { + tasks.value = newModelView.tasks + processInstance.value = newModelView.processInstance + // 已经拒绝的活动节点编号集合,只包括 UserTask + const rejectedTaskActivityIds: string[] = newModelView.rejectedTaskActivityIds + // 进行中的活动节点编号集合, 只包括 UserTask + const unfinishedTaskActivityIds: string[] = newModelView.unfinishedTaskActivityIds + // 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等 + const finishedActivityIds: string[] = newModelView.finishedTaskActivityIds + // 已经完成的连线节点编号集合,只包括 SequenceFlow + const finishedSequenceFlowActivityIds: string[] = newModelView.finishedSequenceFlowActivityIds + setSimpleModelNodeTaskStatus( + newModelView.simpleModel, + newModelView.processInstance.status, + rejectedTaskActivityIds, + unfinishedTaskActivityIds, + finishedActivityIds, + finishedSequenceFlowActivityIds + ) + simpleModel.value = newModelView.simpleModel + } + } +) +/** 监控模型结构数据 */ +watch( + () => props.simpleJson, + async (value) => { + if (value) { + simpleModel.value = JSON.parse(value) + } + } +) +const setSimpleModelNodeTaskStatus = ( + simpleModel: SimpleFlowNode | undefined, + processStatus: number, + rejectedTaskActivityIds: string[], + unfinishedTaskActivityIds: string[], + finishedActivityIds: string[], + finishedSequenceFlowActivityIds: string[] +) => { + if (!simpleModel) { + return + } + // 结束节点 + if (simpleModel.type === NodeType.END_EVENT_NODE) { + if (finishedActivityIds.includes(simpleModel.id)) { + simpleModel.activityStatus = processStatus + } else { + simpleModel.activityStatus = TaskStatusEnum.NOT_START + } + return + } + + // 审批节点 + if ( + simpleModel.type === NodeType.START_USER_NODE || + simpleModel.type === NodeType.USER_TASK_NODE + ) { + simpleModel.activityStatus = TaskStatusEnum.NOT_START + if (rejectedTaskActivityIds.includes(simpleModel.id)) { + simpleModel.activityStatus = TaskStatusEnum.REJECT + } else if (unfinishedTaskActivityIds.includes(simpleModel.id)) { + simpleModel.activityStatus = TaskStatusEnum.RUNNING + } else if (finishedActivityIds.includes(simpleModel.id)) { + simpleModel.activityStatus = TaskStatusEnum.APPROVE + } + // TODO 是不是还缺一个 cancel 的状态 + } + + // 抄送节点 + if (simpleModel.type === NodeType.COPY_TASK_NODE) { + // 抄送节点 只有通过和未执行状态 + if (finishedActivityIds.includes(simpleModel.id)) { + simpleModel.activityStatus = TaskStatusEnum.APPROVE + } else { + simpleModel.activityStatus = TaskStatusEnum.NOT_START + } + } + // 条件节点 对应 SequenceFlow + if (simpleModel.type === NodeType.CONDITION_NODE) { + // 条件节点。只有通过和未执行状态 + if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) { + simpleModel.activityStatus = TaskStatusEnum.APPROVE + } else { + simpleModel.activityStatus = TaskStatusEnum.NOT_START + } + } + + // 网关节点 + if ( + simpleModel.type === NodeType.CONDITION_BRANCH_NODE || + simpleModel.type === NodeType.PARALLEL_BRANCH_NODE || + simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE + ) { + // 网关节点。只有通过和未执行状态 + if (finishedActivityIds.includes(simpleModel.id)) { + simpleModel.activityStatus = TaskStatusEnum.APPROVE + } else { + simpleModel.activityStatus = TaskStatusEnum.NOT_START + } + simpleModel.conditionNodes?.forEach((node) => { + setSimpleModelNodeTaskStatus( + node, + processStatus, + rejectedTaskActivityIds, + unfinishedTaskActivityIds, + finishedActivityIds, + finishedSequenceFlowActivityIds + ) + }) + } + + setSimpleModelNodeTaskStatus( + simpleModel.childNode, + processStatus, + rejectedTaskActivityIds, + unfinishedTaskActivityIds, + finishedActivityIds, + finishedSequenceFlowActivityIds + ) +} +</script> + +<style lang="scss" scoped> +.process-viewer-container { + height: 100%; + width: 100%; + + :deep(.process-viewer) { + height: 100% !important; + min-height: 100%; + width: 100%; + overflow: auto; + } +} +</style> diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue index f82e800..8690e58 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue @@ -1,85 +1,50 @@ <template> - <el-card v-loading="loading" class="box-card"> - <template #header> - <span class="el-icon-picture-outline">审批记录</span> - </template> - <el-col :offset="3" :span="17"> - <div class="block"> - <el-timeline> - <el-timeline-item - v-if="processInstance.endTime" - :type="getProcessInstanceTimelineItemType(processInstance)" - > - <p style="font-weight: 700"> - 结束流程:在 {{ 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 - class="ml-10px" - v-if="!isEmpty(item.children)" - @click="openChildrenTask(item)" - size="small" - > - <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' }"> - <label v-if="item.assigneeUser" style="margin-right: 30px; font-weight: normal"> - 审批人:{{ item.assigneeUser.nickname }} - <el-tag size="small" type="info">{{ item.assigneeUser.deptName }}</el-tag> - </label> - <label v-if="item.createTime" style="font-weight: normal">创建时间:</label> - <label style="font-weight: normal; color: #8a909c"> - {{ formatDate(item?.createTime) }} - </label> - <label v-if="item.endTime" style="margin-left: 30px; font-weight: normal"> - 审批时间: - </label> - <label v-if="item.endTime" style="font-weight: normal; color: #8a909c"> - {{ formatDate(item?.endTime) }} - </label> - <label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal"> - 耗时: - </label> - <label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c"> - {{ formatPast2(item?.durationInMillis) }} - </label> - <p v-if="item.reason"> 审批建议:{{ item.reason }} </p> - </el-card> - </el-timeline-item> - <el-timeline-item type="success"> - <p style="font-weight: 700"> - 发起流程:【{{ processInstance.startUser?.nickname }}】在 - {{ formatDate(processInstance?.startTime) }} 发起【 {{ processInstance.name }} 】流程 - </p> - </el-timeline-item> - </el-timeline> - </div> - </el-col> - </el-card> + <el-table :data="tasks" border header-cell-class-name="table-header-gray"> + <el-table-column label="审批节点" prop="name" min-width="120" align="center" /> + <el-table-column label="审批人" min-width="100" align="center"> + <template #default="scope"> + {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} + </template> + </el-table-column> + <el-table-column + :formatter="dateFormatter" + align="center" + label="开始时间" + prop="createTime" + min-width="140" + /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="结束时间" + prop="endTime" + min-width="140" + /> + <el-table-column align="center" label="审批状态" prop="status" min-width="90"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column align="center" label="审批建议" prop="reason" min-width="200"> + <template #default="scope"> + {{ scope.row.reason }} + <el-button + class="ml-10px" + size="small" + v-if="scope.row.formId > 0" + @click="handleFormDetail(scope.row)" + > + <Icon icon="ep:document" /> 查看表单 + </el-button> + </template> + </el-table-column> + <el-table-column align="center" label="耗时" prop="durationInMillis" min-width="100"> + <template #default="scope"> + {{ formatPast2(scope.row.durationInMillis) }} + </template> + </el-table-column> + </el-table> - <!-- 弹窗:子任务 --> - <TaskSignList ref="taskSignListRef" @success="refresh" /> <!-- 弹窗:表单 --> <Dialog title="表单详情" v-model="taskFormVisible" width="600"> <form-create @@ -91,61 +56,20 @@ </Dialog> </template> <script lang="ts" setup> -import { formatDate, formatPast2 } from '@/utils/formatTime' +import { dateFormatter, formatPast2 } from '@/utils/formatTime' import { propTypes } from '@/utils/propTypes' import { DICT_TYPE } from '@/utils/dict' -import { isEmpty } from '@/utils/is' -import TaskSignList from './dialog/TaskSignList.vue' import type { ApiAttrs } from '@form-create/element-ui/types/config' import { setConfAndFields2 } from '@/utils/formCreate' +import * as TaskApi from '@/api/bpm/task' defineOptions({ name: 'BpmProcessInstanceTaskList' }) -defineProps({ - loading: propTypes.bool, // 是否加载中 - processInstance: propTypes.object, // 流程实例 - tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组 +const props = defineProps({ + loading: propTypes.bool.def(false), // 是否加载中 + id: propTypes.string // 流程实例的编号 }) - -/** 获得流程实例对应的颜色 */ -const getProcessInstanceTimelineItemType = (item: any) => { - if (item.status === 2) { - return 'success' - } - if (item.status === 3) { - return 'danger' - } - if (item.status === 4) { - return 'warning' - } - return '' -} - -/** 获得任务对应的颜色 */ -const getTaskTimelineItemType = (item: any) => { - if ([0, 1, 6, 7].includes(item.status)) { - return 'primary' - } - if (item.status === 2) { - return 'success' - } - if (item.status === 3) { - return 'danger' - } - if (item.status === 4) { - return 'info' - } - if (item.status === 5) { - return 'warning' - } - return '' -} - -/** 子任务 */ -const taskSignListRef = ref() -const openChildrenTask = (item: any) => { - taskSignListRef.value.open(item) -} +const tasks = ref([]) // 流程任务的数组 /** 查看表单 */ const fApi = ref<ApiAttrs>() // form-create 的 API 操作类 @@ -155,7 +79,7 @@ value: {} }) // 流程任务的表单详情 const taskFormVisible = ref(false) -const handleFormDetail = async (row) => { +const handleFormDetail = async (row: any) => { // 设置表单 setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables) // 弹窗打开 @@ -167,9 +91,13 @@ fApi.value?.fapi?.disabled(true) } -/** 刷新数据 */ -const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调 -const refresh = () => { - emit('refresh') -} +/** 只有 loading 完成时,才去加载流程列表 */ +watch( + () => props.loading, + async (value) => { + if (value) { + tasks.value = await TaskApi.getTaskListByProcessInstanceId(props.id) + } + } +) </script> diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue new file mode 100644 index 0000000..e24316c --- /dev/null +++ b/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue @@ -0,0 +1,292 @@ +<!-- 审批详情的右侧:审批流 --> +<template> + <el-timeline class="pt-20px"> + <!-- 遍历每个审批节点 --> + <el-timeline-item + v-for="(activity, index) in activityNodes" + :key="index" + size="large" + :icon="getApprovalNodeIcon(activity.status, activity.nodeType)" + :color="getApprovalNodeColor(activity.status)" + > + <template #dot> + <div + class="position-absolute left--10px top--6px rounded-full border border-solid border-#dedede w-30px h-30px flex justify-center items-center bg-#3f73f7 p-5px" + > + <img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" /> + <div + v-if="showStatusIcon" + class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid" + :style="{ backgroundColor: getApprovalNodeColor(activity.status) }" + > + <el-icon :size="11" color="#fff"> + <component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" /> + </el-icon> + </div> + </div> + </template> + <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`"> + <!-- 第一行:节点名称、时间 --> + <div class="flex w-full"> + <div class="font-bold"> {{ activity.name }}</div> + <!-- 信息:时间 --> + <div + v-if="activity.status !== TaskStatusEnum.NOT_START" + class="text-#a5a5a5 text-13px mt-1 ml-auto" + > + {{ getApprovalNodeTime(activity) }} + </div> + </div> + <!-- 需要自定义选择审批人 --> + <div + class="flex flex-wrap gap2 items-center" + v-if=" + isEmpty(activity.tasks) && + isEmpty(activity.candidateUsers) && + CandidateStrategy.START_USER_SELECT === activity.candidateStrategy + " + > + <!-- && activity.nodeType === NodeType.USER_TASK_NODE --> + + <el-tooltip content="添加用户" placement="left"> + <el-button + class="!px-6px" + @click="handleSelectUser(activity.id, customApproveUsers[activity.id])" + > + <img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" /> + </el-button> + </el-tooltip> + <div + v-for="(user, idx1) in customApproveUsers[activity.id]" + :key="idx1" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + </div> + </div> + <div v-else class="flex items-center flex-wrap mt-1 gap2"> + <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 --> + <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2"> + <div + class="position-relative flex flex-wrap gap2" + v-if="task.assigneeUser || task.ownerUser" + > + <!-- 信息:头像昵称 --> + <div + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname"> + <el-avatar + class="!m-5px" + :size="28" + v-if="task.assigneeUser?.avatar" + :src="task.assigneeUser?.avatar" + /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ task.assigneeUser?.nickname.substring(0, 1) }} + </el-avatar> + {{ task.assigneeUser?.nickname }} + </template> + <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname"> + <el-avatar + class="!m-5px" + :size="28" + v-if="task.ownerUser?.avatar" + :src="task.ownerUser?.avatar" + /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ task.ownerUser?.nickname.substring(0, 1) }} + </el-avatar> + {{ task.ownerUser?.nickname }} + </template> + <!-- 信息:任务 ICON --> + <div + v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)" + class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid" + :style="{ backgroundColor: statusIconMap2[task.status]?.color }" + > + <Icon :size="11" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" /> + </div> + </div> + </div> + <teleport defer :to="`#activity-task-${activity.id}-${index}`"> + <div + v-if=" + task.reason && + [NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(activity.nodeType) + " + class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md" + > + 审批意见:{{ task.reason }} + </div> + </teleport> + </div> + <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 --> + <div + v-for="(user, idx1) in activity.candidateUsers" + :key="idx1" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + + <!-- 信息:任务 ICON --> + <div + v-if="showStatusIcon" + class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid" + :style="{ backgroundColor: statusIconMap2['-1']?.color }" + > + <Icon :size="11" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" /> + </div> + </div> + </div> + </div> + </el-timeline-item> + </el-timeline> + + <!-- 用户选择弹窗 --> + <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> +</template> + +<script lang="ts" setup> +import { formatDate } from '@/utils/formatTime' +import * as ProcessInstanceApi from '@/api/bpm/processInstance' +import { TaskStatusEnum } from '@/api/bpm/task' +import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts' +import { isEmpty } from '@/utils/is' +import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue' +import starterSvg from '@/assets/svgs/bpm/starter.svg' +import auditorSvg from '@/assets/svgs/bpm/auditor.svg' +import copySvg from '@/assets/svgs/bpm/copy.svg' +import conditionSvg from '@/assets/svgs/bpm/condition.svg' +import parallelSvg from '@/assets/svgs/bpm/parallel.svg' +import finishSvg from '@/assets/svgs/bpm/finish.svg' + +defineOptions({ name: 'BpmProcessInstanceTimeline' }) +withDefaults( + defineProps<{ + activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息 + showStatusIcon?: boolean // 是否显示头像右下角状态图标 + }>(), + { + showStatusIcon: true // 默认值为 true + } +) + +// 审批节点 +const statusIconMap2 = { + // 未开始 + '-1': { color: '#909398', icon: 'ep-clock' }, + // 待审批 + '0': { color: '#00b32a', icon: 'ep:loading' }, + // 审批中 + '1': { color: '#448ef7', icon: 'ep:loading' }, + // 审批通过 + '2': { color: '#00b32a', icon: 'ep:circle-check-filled' }, + // 审批不通过 + '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' }, + // 取消 + '4': { color: '#cccccc', icon: 'ep:delete-filled' }, + // 退回 + '5': { color: '#f46b6c', icon: 'ep:remove-filled' }, + // 委派中 + '6': { color: '#448ef7', icon: 'ep:loading' }, + // 审批通过中 + '7': { color: '#00b32a', icon: 'ep:circle-check-filled' } +} + +const statusIconMap = { + // 审批未开始 + '-1': { color: '#909398', icon: Clock }, + '0': { color: '#00b32a', icon: Clock }, + // 审批中 + '1': { color: '#448ef7', icon: Loading }, + // 审批通过 + '2': { color: '#00b32a', icon: Check }, + // 审批不通过 + '3': { color: '#f46b6c', icon: Close }, + // 已取消 + '4': { color: '#cccccc', icon: Delete }, + // 退回 + '5': { color: '#f46b6c', icon: Minus }, + // 委派中 + '6': { color: '#448ef7', icon: Loading }, + // 审批通过中 + '7': { color: '#00b32a', icon: Check } +} + +const nodeTypeSvgMap = { + // 结束节点 + [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg }, + // 发起人节点 + [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg }, + // 审批人节点 + [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg }, + // 抄送人节点 + [NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg }, + // 条件分支节点 + [NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg }, + // 并行分支节点 + [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg } +} + +// 只有只有状态是 -1、0、1 才展示头像右小角状态小icon +const onlyStatusIconShow = [-1, 0, 1] + +// timeline时间线上icon图标 +const getApprovalNodeImg = (nodeType: NodeType) => { + return nodeTypeSvgMap[nodeType]?.svg +} + +const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => { + if (taskStatus == TaskStatusEnum.NOT_START) { + return statusIconMap[taskStatus]?.icon + } + + if ( + nodeType === NodeType.START_USER_NODE || + nodeType === NodeType.USER_TASK_NODE || + nodeType === NodeType.END_EVENT_NODE + ) { + return statusIconMap[taskStatus]?.icon + } +} + +const getApprovalNodeColor = (taskStatus: number) => { + return statusIconMap[taskStatus]?.color +} + +const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => { + if (node.nodeType === NodeType.START_USER_NODE && node.startTime) { + return `${formatDate(node.startTime)}` + } + if (node.endTime) { + return `${formatDate(node.endTime)}` + } + if (node.startTime) { + return `${formatDate(node.startTime)}` + } +} + +// 选择自定义审批人 +const userSelectFormRef = ref() +const handleSelectUser = (activityId, selectedList) => { + userSelectFormRef.value.open(activityId, selectedList) +} +const emit = defineEmits<{ + selectUserConfirm: [id: any, userList: any[]] +}>() +const customApproveUsers: any = ref({}) // key:activityId,value:用户列表 +// 选择完成 +const handleUserSelectConfirm = (activityId: string, userList: any[]) => { + customApproveUsers.value[activityId] = userList || [] + emit('selectUserConfirm', activityId, userList) +} +</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue deleted file mode 100644 index 178b1b9..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="委派任务" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="接收人" prop="delegateUserId"> - <el-select v-model="formData.delegateUserId" clearable style="width: 100%"> - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="委派理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入委派理由" /> - </el-form-item> - </el-form> - <template #footer> - <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script lang="ts" setup> -import * as TaskApi from '@/api/bpm/task' -import * as UserApi from '@/api/system/user' - -defineOptions({ name: 'BpmTaskDelegateForm' }) - -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - delegateUserId: undefined, - reason: '' -}) -const formRules = ref({ - delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }], - reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }] -}) - -const formRef = ref() // 表单 Ref -const userList = ref<any[]>([]) // 用户列表 - -/** 打开弹窗 */ -const open = async (id: string) => { - dialogVisible.value = true - resetForm() - formData.value.id = id - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - await TaskApi.delegateTask(formData.value) - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - delegateUserId: undefined, - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue deleted file mode 100644 index a139169..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue +++ /dev/null @@ -1,90 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="回退任务" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="退回节点" prop="targetTaskDefinitionKey"> - <el-select v-model="formData.targetTaskDefinitionKey" clearable style="width: 100%"> - <el-option - v-for="item in returnList" - :key="item.taskDefinitionKey" - :label="item.name" - :value="item.taskDefinitionKey" - /> - </el-select> - </el-form-item> - <el-form-item label="回退理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入回退理由" /> - </el-form-item> - </el-form> - <template #footer> - <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script lang="ts" name="TaskRollbackDialogForm" setup> -import * as TaskApi from '@/api/bpm/task' - -const message = useMessage() // 消息弹窗 -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - targetTaskDefinitionKey: undefined, - reason: '' -}) -const formRules = ref({ - targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }], - reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }] -}) - -const formRef = ref() // 表单 Ref -const returnList = ref([] as any) -/** 打开弹窗 */ -const open = async (id: string) => { - returnList.value = await TaskApi.getTaskListByReturn(id) - if (returnList.value.length === 0) { - message.warning('当前没有可回退的节点') - return false - } - dialogVisible.value = true - resetForm() - formData.value.id = id -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - await TaskApi.returnTask(formData.value) - message.success('回退成功') - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - targetTaskDefinitionKey: undefined, - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue deleted file mode 100644 index 9e4998c..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="加签" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="加签处理人" prop="userIds"> - <el-select v-model="formData.userIds" multiple clearable style="width: 100%"> - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="加签理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入加签理由" /> - </el-form-item> - </el-form> - <template #footer> - <el-button :disabled="formLoading" type="primary" @click="submitForm('before')"> - 向前加签 - </el-button> - <el-button :disabled="formLoading" type="primary" @click="submitForm('after')"> - 向后加签 - </el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script lang="ts" setup> -import * as TaskApi from '@/api/bpm/task' -import * as UserApi from '@/api/system/user' - -defineOptions({ name: 'TaskSignCreateForm' }) - -const message = useMessage() // 消息弹窗 -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - userIds: [], - type: '', - reason: '' -}) -const formRules = ref({ - userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }], - reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }] -}) - -const formRef = ref() // 表单 Ref -const userList = ref<any[]>([]) // 用户列表 - -/** 打开弹窗 */ -const open = async (id: string) => { - dialogVisible.value = true - resetForm() - formData.value.id = id - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async (type: string) => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - formData.value.type = type - try { - await TaskApi.signCreateTask(formData.value) - message.success('加签成功') - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - userIds: [], - type: '', - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue deleted file mode 100644 index 19bb2dc..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="减签" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="减签任务" prop="id"> - <el-radio-group v-model="formData.id"> - <el-radio-button v-for="item in childrenTaskList" :key="item.id" :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> - <el-form-item label="减签理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入减签理由" /> - </el-form-item> - </el-form> - <template #footer> - <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script lang="ts" setup> -import * as TaskApi from '@/api/bpm/task' -import { isEmpty } from '@/utils/is' - -defineOptions({ name: 'TaskSignDeleteForm' }) - -const message = useMessage() // 消息弹窗 -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - reason: '' -}) -const formRules = ref({ - id: [{ required: true, message: '必须选择减签任务', trigger: 'change' }], - reason: [{ required: true, message: '减签理由不能为空', trigger: 'blur' }] -}) - -const formRef = ref() // 表单 Ref -const childrenTaskList = ref([]) -/** 打开弹窗 */ -const open = async (id: string) => { - childrenTaskList.value = await TaskApi.getChildrenTaskList(id) - if (isEmpty(childrenTaskList.value)) { - message.warning('当前没有可减签的任务') - return false - } - dialogVisible.value = true - resetForm() -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - await TaskApi.signDeleteTask(formData.value) - message.success('减签成功') - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue deleted file mode 100644 index 648e86b..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue +++ /dev/null @@ -1,106 +0,0 @@ -<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/dialog/TaskTransferForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue deleted file mode 100644 index c1012ac..0000000 --- a/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> - <Dialog v-model="dialogVisible" title="转派任务" width="500"> - <el-form - ref="formRef" - v-loading="formLoading" - :model="formData" - :rules="formRules" - label-width="110px" - > - <el-form-item label="新审批人" prop="assigneeUserId"> - <el-select v-model="formData.assigneeUserId" clearable style="width: 100%"> - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="转派理由" prop="reason"> - <el-input v-model="formData.reason" clearable placeholder="请输入转派理由" /> - </el-form-item> - </el-form> - <template #footer> - <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script lang="ts" setup> -import * as TaskApi from '@/api/bpm/task' -import * as UserApi from '@/api/system/user' - -defineOptions({ name: 'TaskTransferForm' }) - -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中 -const formData = ref({ - id: '', - assigneeUserId: undefined, - reason: '' -}) -const formRules = ref({ - assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }], - reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }] -}) - -const formRef = ref() // 表单 Ref -const userList = ref<any[]>([]) // 用户列表 - -/** 打开弹窗 */ -const open = async (id: string) => { - dialogVisible.value = true - resetForm() - formData.value.id = id - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -} -defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - await TaskApi.transferTask(formData.value) - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: '', - assigneeUserId: undefined, - reason: '' - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue index da54769..a6ed3b5 100644 --- a/src/views/bpm/processInstance/detail/index.vue +++ b/src/views/bpm/processInstance/detail/index.vue @@ -1,174 +1,170 @@ <template> - <ContentWrap> - <!-- 审批信息 --> - <el-card - v-for="(item, index) in runningTasks" - :key="index" - v-loading="processInstanceLoading" - class="box-card" - > - <template #header> - <span class="el-icon-picture-outline">审批任务【{{ item.name }}】</span> - </template> - <el-col :offset="6" :span="16"> - <el-form - :ref="'form' + index" - :model="auditForms[index]" - :rules="auditRule" - label-width="100px" - > - <el-form-item v-if="processInstance && processInstance.name" label="流程名"> - {{ processInstance.name }} - </el-form-item> - <el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人"> - {{ processInstance?.startUser.nickname }} - <el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag> - </el-form-item> - <el-card v-if="runningTasks[index].formId > 0" class="mb-15px !-mt-10px"> - <template #header> - <span class="el-icon-picture-outline"> - 填写表单【{{ runningTasks[index]?.formName }}】 - </span> - </template> - <form-create - v-model="approveForms[index].value" - v-model:api="approveFormFApis[index]" - :option="approveForms[index].option" - :rule="approveForms[index].rule" - /> - </el-card> - <el-form-item label="审批建议" prop="reason"> - <el-input - v-model="auditForms[index].reason" - placeholder="请输入审批建议" - 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)"> - <Icon icon="ep:select" /> - 通过 - </el-button> - <el-button type="danger" @click="handleAudit(item, false)"> - <Icon icon="ep:close" /> - 不通过 - </el-button> - <el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)"> - <Icon icon="ep:edit" /> - 转办 - </el-button> - <el-button type="primary" @click="handleDelegate(item)"> - <Icon icon="ep:position" /> - 委派 - </el-button> - <el-button type="primary" @click="handleSign(item)"> - <Icon icon="ep:plus" /> - 加签 - </el-button> - <el-button type="warning" @click="handleBack(item)"> - <Icon icon="ep:back" /> - 回退 - </el-button> - </div> - </el-col> - </el-card> - - <!-- 申请信息 --> - <el-card v-loading="processInstanceLoading" class="box-card"> - <template #header> - <span class="el-icon-document">申请信息【{{ processInstance.name }}】</span> - </template> - <!-- 情况一:流程表单 --> - <el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16"> - <form-create - v-model="detailForm.value" - v-model:api="fApi" - :option="detailForm.option" - :rule="detailForm.rule" + <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }" class="position-relative"> + <div class="processInstance-wrap-main"> + <el-scrollbar> + <img + class="position-absolute right-20px" + width="150" + :src="auditIconsMap[processInstance.status]" + alt="" /> - </el-col> - <!-- 情况二:业务表单 --> - <div v-if="processInstance?.processDefinition?.formType === 20"> - <BusinessFormComponent :id="processInstance.businessKey" /> - </div> - </el-card> + <div class="text-#878c93 h-15px">编号:{{ id }}</div> + <el-divider class="!my-8px" /> + <div class="flex items-center gap-5 mb-10px h-40px"> + <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div> + <dict-tag + v-if="processInstance.status" + :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" + :value="processInstance.status" + /> + </div> - <!-- 审批记录 --> - <ProcessInstanceTaskList - :loading="tasksLoad" - :process-instance="processInstance" - :tasks="tasks" - @refresh="getTaskList" - /> + <div class="flex items-center gap-5 mb-10px text-13px h-35px"> + <div + class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600" + > + <el-avatar + :size="28" + v-if="processInstance?.startUser?.avatar" + :src="processInstance?.startUser?.avatar" + /> + <el-avatar :size="28" v-else-if="processInstance?.startUser?.nickname"> + {{ processInstance?.startUser?.nickname.substring(0, 1) }} + </el-avatar> + {{ processInstance?.startUser?.nickname }} + </div> + <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div> + </div> - <!-- 高亮流程图 --> - <ProcessInstanceBpmnViewer - :id="`${id}`" - :bpmn-xml="bpmnXml" - :loading="processInstanceLoading" - :process-instance="processInstance" - :tasks="tasks" - /> + <el-tabs v-model="activeTab"> + <!-- 表单信息 --> + <el-tab-pane label="审批详情" name="form"> + <div class="form-scroll-area"> + <el-scrollbar> + <el-row> + <el-col :span="17" class="!flex !flex-col formCol"> + <!-- 表单信息 --> + <div + v-loading="processInstanceLoading" + class="form-box flex flex-col mb-30px flex-1" + > + <!-- 情况一:流程表单 --> + <el-col v-if="processDefinition?.formType === BpmModelFormType.NORMAL"> + <form-create + v-model="detailForm.value" + v-model:api="fApi" + :option="detailForm.option" + :rule="detailForm.rule" + /> + </el-col> + <!-- 情况二:业务表单 --> + <div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM"> + <BusinessFormComponent :id="processInstance.businessKey" /> + </div> + </div> + </el-col> + <el-col :span="7"> + <!-- 审批记录时间线 --> + <ProcessInstanceTimeline :activity-nodes="activityNodes" /> + </el-col> + </el-row> + </el-scrollbar> + </div> + </el-tab-pane> - <!-- 弹窗:转派审批人 --> - <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" /> - <!-- 弹窗:回退节点 --> - <TaskReturnForm ref="taskReturnFormRef" @success="getDetail" /> - <!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中--> - <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" /> - <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 --> - <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" /> + <!-- 流程图 --> + <el-tab-pane label="流程图" name="diagram"> + <div class="form-scroll-area"> + <ProcessInstanceSimpleViewer + v-show=" + processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE + " + :loading="processInstanceLoading" + :model-view="processModelView" + /> + <ProcessInstanceBpmnViewer + v-show=" + processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN + " + :loading="processInstanceLoading" + :model-view="processModelView" + /> + </div> + </el-tab-pane> + + <!-- 流转记录 --> + <el-tab-pane label="流转记录" name="record"> + <div class="form-scroll-area"> + <el-scrollbar> + <ProcessInstanceTaskList :loading="processInstanceLoading" :id="id" /> + </el-scrollbar> + </div> + </el-tab-pane> + + <!-- 流转评论 TODO 待开发 --> + <el-tab-pane label="流转评论" name="comment" v-if="false"> + <div class="form-scroll-area"> + <el-scrollbar> 流转评论 </el-scrollbar> + </div> + </el-tab-pane> + </el-tabs> + + <div class="b-t-solid border-t-1px border-[var(--el-border-color)]"> + <!-- 操作栏按钮 --> + <ProcessInstanceOperationButton + ref="operationButtonRef" + :process-instance="processInstance" + :process-definition="processDefinition" + :userOptions="userOptions" + :normal-form="detailForm" + :normal-form-api="fApi" + :writable-fields="writableFields" + @success="refresh" + /> + </div> + </el-scrollbar> + </div> </ContentWrap> </template> <script lang="ts" setup> -import { useUserStore } from '@/store/modules/user' +import { formatDate } from '@/utils/formatTime' +import { DICT_TYPE } from '@/utils/dict' +import { BpmModelType, BpmModelFormType } from '@/utils/constants' import { setConfAndFields2 } from '@/utils/formCreate' -import type { ApiAttrs } from '@form-create/element-ui/types/config' -import * as DefinitionApi from '@/api/bpm/definition' -import * as ProcessInstanceApi from '@/api/bpm/processInstance' -import * as TaskApi from '@/api/bpm/task' -import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue' -import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue' -import 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 type { ApiAttrs } from '@form-create/element-ui/types/config' +import * as ProcessInstanceApi from '@/api/bpm/processInstance' import * as UserApi from '@/api/system/user' +import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue' +import ProcessInstanceSimpleViewer from './ProcessInstanceSimpleViewer.vue' +import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue' +import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue' +import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue' +import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts' +import { TaskStatusEnum } from '@/api/bpm/task' +import runningSvg from '@/assets/svgs/bpm/running.svg' +import approveSvg from '@/assets/svgs/bpm/approve.svg' +import rejectSvg from '@/assets/svgs/bpm/reject.svg' +import cancelSvg from '@/assets/svgs/bpm/cancel.svg' defineOptions({ name: 'BpmProcessInstanceDetail' }) - -const { query } = useRoute() // 查询参数 +const props = defineProps<{ + id: string // 流程实例的编号 + taskId?: string // 任务编号 + activityId?: string //流程活动编号,用于抄送查看 +}>() const message = useMessage() // 消息弹窗 -const { proxy } = getCurrentInstance() as any - -const userId = useUserStore().getUser.id // 当前登录的编号 -const id = query.id as unknown as string // 流程实例的编号 const processInstanceLoading = ref(false) // 流程实例的加载中 const processInstance = ref<any>({}) // 流程实例 -const bpmnXml = ref('') // BPMN XML -const tasksLoad = ref(true) // 任务的加载中 -const tasks = ref<any[]>([]) // 任务列表 -// ========== 审批信息 ========== -const runningTasks = ref<any[]>([]) // 运行中的任务 -const auditForms = ref<any[]>([]) // 审批任务的表单 -const auditRule = reactive({ - reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }] -}) -const approveForms = ref<any[]>([]) // 审批通过时,额外的补充信息 -const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi +const processDefinition = ref<any>({}) // 流程定义 +const processModelView = ref<any>({}) // 流程模型视图 +const operationButtonRef = ref() // 操作按钮组件 ref +const auditIconsMap = { + [TaskStatusEnum.RUNNING]: runningSvg, + [TaskStatusEnum.APPROVE]: approveSvg, + [TaskStatusEnum.REJECT]: rejectSvg, + [TaskStatusEnum.CANCEL]: cancelSvg +} // ========== 申请信息 ========== const fApi = ref<ApiAttrs>() // @@ -178,198 +174,128 @@ value: {} }) // 流程实例的表单详情 -/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */ -watch( - () => approveFormFApis.value, - (value) => { - value?.forEach((api) => { - api.btn.show(false) - api.resetBtn.show(false) - }) - }, - { - deep: true - } -) - -/** 处理审批通过和不通过的操作 */ -const handleAudit = async (task, pass) => { - // 1.1 获得对应表单 - const index = runningTasks.value.indexOf(task) - const auditFormRef = proxy.$refs['form' + index][0] - // 1.2 校验表单 - const elForm = unref(auditFormRef) - if (!elForm) return - const valid = await elForm.validate() - if (!valid) return - - // 2.1 提交审批 - const data = { - id: task.id, - reason: auditForms.value[index].reason, - copyUserIds: auditForms.value[index].copyUserIds - } - if (pass) { - // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交 - const formCreateApi = approveFormFApis.value[index] - if (formCreateApi) { - await formCreateApi.validate() - data.variables = approveForms.value[index].value - } - await TaskApi.approveTask(data) - message.success('审批通过成功') - } else { - await TaskApi.rejectTask(data) - message.success('审批不通过成功') - } - // 2.2 加载最新数据 - getDetail() -} - -/** 转派审批人 */ -const taskTransferFormRef = ref() -const openTaskUpdateAssigneeForm = (id: string) => { - taskTransferFormRef.value.open(id) -} - -/** 处理审批退回的操作 */ -const taskDelegateForm = ref() -const handleDelegate = async (task) => { - taskDelegateForm.value.open(task.id) -} - -/** 处理审批退回的操作 */ -const taskReturnFormRef = ref() -const handleBack = async (task: any) => { - taskReturnFormRef.value.open(task.id) -} - -/** 处理审批加签的操作 */ -const taskSignCreateFormRef = ref() -const handleSign = async (task: any) => { - taskSignCreateFormRef.value.open(task.id) -} +const writableFields: Array<string> = [] // 表单可以编辑的字段 /** 获得详情 */ const getDetail = () => { - // 1. 获得流程实例相关 - getProcessInstance() - // 2. 获得流程任务列表(审批记录) - getTaskList() + getApprovalDetail() + + getProcessModelView() } /** 加载流程实例 */ -const BusinessFormComponent = ref(null) // 异步组件 -const getProcessInstance = async () => { +const BusinessFormComponent = ref<any>(null) // 异步组件 +/** 获取审批详情 */ +const getApprovalDetail = async () => { + processInstanceLoading.value = true try { - processInstanceLoading.value = true - const data = await ProcessInstanceApi.getProcessInstance(id) + const param = { + processInstanceId: props.id, + activityId: props.activityId, + taskId: props.taskId + } + const data = await ProcessInstanceApi.getApprovalDetail(param) if (!data) { + message.error('查询不到审批详情信息!') + return + } + if (!data.processDefinition || !data.processInstance) { message.error('查询不到流程信息!') return } - processInstance.value = data + processInstance.value = data.processInstance + processDefinition.value = data.processDefinition // 设置表单信息 - const processDefinition = data.processDefinition - if (processDefinition.formType === 10) { - setConfAndFields2( - detailForm, - processDefinition.formConf, - processDefinition.formFields, - data.formVariables - ) + if (processDefinition.value.formType === BpmModelFormType.NORMAL) { + // 获取表单字段权限 + const formFieldsPermission = data.formFieldsPermission + // 清空可编辑字段为空 + writableFields.splice(0) + if (detailForm.value.rule?.length > 0) { + // 避免刷新 form-create 显示不了 + detailForm.value.value = processInstance.value.formVariables + } else { + setConfAndFields2( + detailForm, + processDefinition.value.formConf, + processDefinition.value.formFields, + processInstance.value.formVariables + ) + } nextTick().then(() => { fApi.value?.btn.show(false) fApi.value?.resetBtn.show(false) + //@ts-ignore fApi.value?.disabled(true) + // 设置表单字段权限 + if (formFieldsPermission) { + Object.keys(data.formFieldsPermission).forEach((item) => { + setFieldPermission(item, formFieldsPermission[item]) + }) + } }) } else { // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath) } - // 加载流程图 - bpmnXml.value = ( - await DefinitionApi.getProcessDefinition(processDefinition.id as number) - )?.bpmnXml + // 获取审批节点,显示 Timeline 的数据 + activityNodes.value = data.activityNodes + + // 获取待办任务显示操作按钮 + operationButtonRef.value?.loadTodoTask(data.todoTask) } finally { processInstanceLoading.value = false } } -/** 加载任务列表 */ -const getTaskList = async () => { - runningTasks.value = [] - auditForms.value = [] - approveForms.value = [] - approveFormFApis.value = [] - try { - // 获得未取消的任务 - tasksLoad.value = true - const data = await TaskApi.getTaskListByProcessInstanceId(id) - tasks.value = [] - // 1.1 移除已取消的审批 - data.forEach((task) => { - if (task.status !== 4) { - tasks.value.push(task) - } - }) - // 1.2 排序,将未完成的排在前面,已完成的排在后面; - tasks.value.sort((a, b) => { - // 有已完成的情况,按照完成时间倒序 - if (a.endTime && b.endTime) { - return b.endTime - a.endTime - } else if (a.endTime) { - return 1 - } else if (b.endTime) { - return -1 - // 都是未完成,按照创建时间倒序 - } else { - return b.createTime - a.createTime - } - }) +/** 获取流程模型视图*/ +const getProcessModelView = async () => { + if (BpmModelType.BPMN === processDefinition.value?.modelType) { + // 重置,解决 BPMN 流程图刷新不会重新渲染问题 + processModelView.value = { + bpmnXml: '' + } + } + const data = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id) + if (data) { + processModelView.value = data + } +} - // 获得需要自己审批的任务 - loadRunningTask(tasks.value) - } finally { - tasksLoad.value = false +// 审批节点信息 +const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) +/** + * 设置表单权限 + */ +const setFieldPermission = (field: string, permission: string) => { + if (permission === FieldPermissionType.READ) { + //@ts-ignore + fApi.value?.disabled(true, field) + } + if (permission === FieldPermissionType.WRITE) { + //@ts-ignore + fApi.value?.disabled(false, field) + // 加入可以编辑的字段 + writableFields.push(field) + } + if (permission === FieldPermissionType.NONE) { + //@ts-ignore + fApi.value?.hidden(true, field) } } /** - * 设置 runningTasks 中的任务 + * 操作成功后刷新 */ -const loadRunningTask = (tasks) => { - tasks.forEach((task) => { - if (!isEmpty(task.children)) { - loadRunningTask(task.children) - } - // 2.1 只有待处理才需要 - if (task.status !== 1 && task.status !== 6) { - return - } - // 2.2 自己不是处理人 - if (!task.assigneeUser || task.assigneeUser.id !== userId) { - return - } - // 2.3 添加到处理任务 - runningTasks.value.push({ ...task }) - auditForms.value.push({ - reason: '', - copyUserIds: [] - }) - - // 2.4 处理 approve 表单 - if (task.formId && task.formConf) { - const approveForm = {} - setConfAndFields2(approveForm, task.formConf, task.formFields, task.formVariables) - approveForms.value.push(approveForm) - } else { - approveForms.value.push({}) // 占位,避免为空 - } - }) +const refresh = () => { + // 重新获取详情 + getDetail() } + +/** 当前的Tab */ +const activeTab = ref('form') /** 初始化 */ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 @@ -379,3 +305,50 @@ userOptions.value = await UserApi.getSimpleUserList() }) </script> + +<style lang="scss" scoped> +$wrap-padding-height: 20px; +$wrap-margin-height: 15px; +$button-height: 51px; +$process-header-height: 194px; + +.processInstance-wrap-main { + height: calc( + 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px + ); + max-height: calc( + 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px + ); + overflow: auto; + + .form-scroll-area { + display: flex; + height: calc( + 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - + $process-header-height - 40px + ); + max-height: calc( + 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - + $process-header-height - 40px + ); + overflow: auto; + flex-direction: column; + + :deep(.box-card) { + height: 100%; + flex: 1; + + .el-card__body { + height: 100%; + padding: 0; + } + } + } +} + +.form-box { + :deep(.el-card) { + border: none; + } +} +</style> diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue index 3951a83..2ffb162 100644 --- a/src/views/bpm/processInstance/index.vue +++ b/src/views/bpm/processInstance/index.vue @@ -1,5 +1,4 @@ <template> - <ContentWrap> <!-- 搜索工作栏 --> <el-form @@ -9,7 +8,7 @@ :inline="true" label-width="68px" > - <el-form-item label="流程名称" prop="name"> + <el-form-item label="" prop="name"> <el-input v-model="queryParams.name" placeholder="请输入流程名称" @@ -18,21 +17,20 @@ 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-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> </el-form-item> - <el-form-item label="流程分类" prop="category"> + + <!-- TODO @ tuituji:style 可以使用 unocss --> + <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }"> + <!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 RE:done & to check--> <el-select v-model="queryParams.category" placeholder="请选择流程分类" clearable - class="!w-240px" + class="!w-155px" + @change="handleQuery" > <el-option v-for="category in categoryList" @@ -42,12 +40,14 @@ /> </el-select> </el-form-item> - <el-form-item label="流程状态" prop="status"> + + <el-form-item label="" prop="status" :style="{ position: 'absolute', right: '130px' }"> <el-select v-model="queryParams.status" placeholder="请选择流程状态" clearable - class="!w-240px" + class="!w-155px" + @change="handleQuery" > <el-option v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" @@ -57,28 +57,69 @@ /> </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 - v-hasPermi="['bpm:process-instance:query']" - @click="handleCreate(undefined)" + + <!-- 高级筛选 --> + <!-- TODO @ tuituji:style 可以使用 unocss --> + <el-form-item :style="{ position: 'absolute', right: '0px' }"> + <el-popover + :visible="showPopover" + persistent + :width="400" + :show-arrow="false" + placement="bottom-end" > - <Icon icon="ep:plus" class="mr-5px" /> 发起流程 - </el-button> + <template #reference> + <el-button @click="showPopover = !showPopover"> + <Icon icon="ep:plus" class="mr-5px" />高级筛选 + </el-button> + </template> + <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程发起人" + clearable + class="!w-390px" + > + <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="所属流程" + class="bold-label" + label-position="top" + prop="processDefinitionKey" + > + <el-input + v-model="queryParams.processDefinitionKey" + placeholder="请输入流程定义的标识" + clearable + @keyup.enter="handleQuery" + class="!w-390px" + /> + </el-form-item> + <el-form-item label="发起时间" class="bold-label" label-position="top" prop="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> + <!-- TODO tuituiji:参考钉钉,1)按照清空、取消、确认排序。2)右对齐。3)确认增加 primary --> + <el-form-item class="bold-label" label-position="top"> + <el-button @click="handleQuery"> 确认</el-button> + <el-button @click="showPopover = false"> 取消</el-button> + <el-button @click="resetQuery"> 清空</el-button> + </el-form-item> + </el-popover> </el-form-item> </el-form> </ContentWrap> @@ -94,6 +135,8 @@ min-width="100" fixed="left" /> + <!-- TODO @芋艿:摘要 --> + <!-- TODO tuituiji:参考钉钉;1)审批中时,展示审批任务;2)非审批中,展示状态 --> <el-table-column label="流程状态" prop="status" width="120"> <template #default="scope"> <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" /> @@ -113,7 +156,7 @@ width="180" :formatter="dateFormatter" /> - <el-table-column align="center" label="耗时" prop="durationInMillis" width="160"> + <!--<el-table-column align="center" label="耗时" prop="durationInMillis" width="160"> <template #default="scope"> {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }} </template> @@ -125,7 +168,7 @@ </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 @@ -161,11 +204,12 @@ </ContentWrap> </template> <script lang="ts" setup> +// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。 RE:done & to check import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { dateFormatter, formatPast2 } from '@/utils/formatTime' +import { dateFormatter } from '@/utils/formatTime' import { ElMessageBox } from 'element-plus' import * as ProcessInstanceApi from '@/api/bpm/processInstance' -import { CategoryApi } from '@/api/bpm/category' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' import { ProcessInstanceVO } from '@/api/bpm/processInstance' import * as DefinitionApi from '@/api/bpm/definition' @@ -182,13 +226,13 @@ pageNo: 1, pageSize: 10, name: '', - processDefinitionId: undefined, + processDefinitionKey: undefined, category: undefined, status: undefined, createTime: [] }) const queryFormRef = ref() // 搜索的表单 -const categoryList = ref([]) // 流程分类列表 +const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 /** 查询列表 */ const getList = async () => { @@ -201,6 +245,8 @@ loading.value = false } } + +const showPopover = ref(false) /** 搜索按钮操作 */ const handleQuery = () => { @@ -234,7 +280,7 @@ } /** 查看详情 */ -const handleDetail = (row) => { +const handleDetail = (row: ProcessInstanceVO) => { router.push({ name: 'BpmProcessInstanceDetail', query: { @@ -244,7 +290,7 @@ } /** 取消按钮操作 */ -const handleCancel = async (row) => { +const handleCancel = async (row: ProcessInstanceVO) => { // 二次确认 const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', { confirmButtonText: t('common.ok'), @@ -270,3 +316,8 @@ categoryList.value = await CategoryApi.getCategorySimpleList() }) </script> +<style> +.bold-label .el-form-item__label { + font-weight: bold; /* 将字体加粗 */ +} +</style> diff --git a/src/views/bpm/processInstance/manager/index.vue b/src/views/bpm/processInstance/manager/index.vue index da79456..3b44b19 100644 --- a/src/views/bpm/processInstance/manager/index.vue +++ b/src/views/bpm/processInstance/manager/index.vue @@ -75,9 +75,13 @@ start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - class="!w-220px" + 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-form-item> </el-form> </ContentWrap> diff --git a/src/views/bpm/processListener/ProcessListenerForm.vue b/src/views/bpm/processListener/ProcessListenerForm.vue index 8d4e979..a9684df 100644 --- a/src/views/bpm/processListener/ProcessListenerForm.vue +++ b/src/views/bpm/processListener/ProcessListenerForm.vue @@ -15,7 +15,7 @@ <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" - :label="dict.value" + :value="dict.value" > {{ dict.label }} </el-radio> diff --git a/src/views/bpm/simple/SimpleModelDesign.vue b/src/views/bpm/simple/SimpleModelDesign.vue new file mode 100644 index 0000000..eed0099 --- /dev/null +++ b/src/views/bpm/simple/SimpleModelDesign.vue @@ -0,0 +1,155 @@ +<template> + <ContentWrap :bodyStyle="{ padding: '20px 16px' }"> + <SimpleProcessDesigner + :model-id="modelId" + :model-key="modelKey" + :model-name="modelName" + :value="currentValue" + @success="handleSuccess" + @init-finished="handleInit" + :start-user-ids="startUserIds" + ref="designerRef" + /> + </ContentWrap> +</template> +<script setup lang="ts"> +import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/' + +defineOptions({ + name: 'SimpleModelDesign' +}) + +const props = defineProps<{ + modelId?: string + modelKey?: string + modelName?: string + value?: string + startUserIds?: number[] +}>() + +const emit = defineEmits(['success', 'init-finished']) +const designerRef = ref() +const isInitialized = ref(false) +const currentValue = ref('') + +// 初始化或更新当前值 +const initOrUpdateValue = async () => { + console.log('initOrUpdateValue', props.value) + if (props.value) { + currentValue.value = props.value + // 如果设计器已经初始化,立即加载数据 + if (isInitialized.value && designerRef.value) { + try { + await designerRef.value.loadProcessData(props.value) + await nextTick() + if (designerRef.value.refresh) { + await designerRef.value.refresh() + } + } catch (error) { + console.error('加载流程数据失败:', error) + } + } + } +} + +// 监听属性变化 +watch( + [() => props.modelKey, () => props.modelName, () => props.value], + async ([newKey, newName, newValue], [oldKey, oldName, oldValue]) => { + if (designerRef.value && isInitialized.value) { + try { + if (newKey && newName && (newKey !== oldKey || newName !== oldName)) { + await designerRef.value.updateModel(newKey, newName) + } + if (newValue && newValue !== oldValue) { + currentValue.value = newValue + await designerRef.value.loadProcessData(newValue) + await nextTick() + if (designerRef.value.refresh) { + await designerRef.value.refresh() + } + } + } catch (error) { + console.error('更新流程数据失败:', error) + } + } + }, + { deep: true, immediate: true } +) + +// 初始化完成回调 +const handleInit = async () => { + try { + isInitialized.value = true + emit('init-finished') + + // 等待下一个tick,确保设计器已经准备好 + await nextTick() + + // 初始化完成后,设置初始值 + if (props.modelKey && props.modelName) { + await designerRef.value.updateModel(props.modelKey, props.modelName) + } + if (props.value) { + currentValue.value = props.value + await designerRef.value.loadProcessData(props.value) + // 再次刷新确保数据正确加载 + await nextTick() + if (designerRef.value.refresh) { + await designerRef.value.refresh() + } + } + } catch (error) { + console.error('初始化流程数据失败:', error) + } +} + +// 修改成功回调 +const handleSuccess = (data?: any) => { + console.warn('handleSuccess', data) + if (data && data !== currentValue.value) { + currentValue.value = data + emit('success', data) + } +} + +/** 获取当前流程数据 */ +const getCurrentFlowData = async () => { + try { + if (designerRef.value) { + const data = await designerRef.value.getCurrentFlowData() + if (data) { + currentValue.value = data + } + return data + } + return currentValue.value || undefined + } catch (error) { + console.error('获取流程数据失败:', error) + return currentValue.value || undefined + } +} + +// 组件创建时初始化数据 +onMounted(() => { + initOrUpdateValue() +}) + +// 组件卸载前保存数据 +onBeforeUnmount(async () => { + try { + const data = await getCurrentFlowData() + if (data) { + emit('success', data) + } + } catch (error) { + console.error('保存数据失败:', error) + } +}) + +defineExpose({ + getCurrentFlowData, + refresh: () => designerRef.value?.refresh?.() +}) +</script> +<style lang="scss" scoped></style> diff --git a/src/views/bpm/simpleWorkflow/index.vue b/src/views/bpm/simpleWorkflow/index.vue new file mode 100644 index 0000000..691cf2e --- /dev/null +++ b/src/views/bpm/simpleWorkflow/index.vue @@ -0,0 +1,13 @@ +<template> + <SimpleProcessDesigner :model-id="modelId" /> +</template> +<script setup lang="ts"> +import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/' + +defineOptions({ + name: 'SimpleWorkflowDesignEditor' +}) +const { query } = useRoute() // 路由的查询 +const modelId = query.modelId as string +</script> +<style lang="scss" scoped></style> diff --git a/src/views/bpm/task/copy/index.vue b/src/views/bpm/task/copy/index.vue index dd41b2e..b64521d 100644 --- a/src/views/bpm/task/copy/index.vue +++ b/src/views/bpm/task/copy/index.vue @@ -1,11 +1,13 @@ <!-- 工作流 - 抄送我的流程 --> <template> + <ContentWrap> <!-- 搜索工作栏 --> <el-form ref="queryFormRef" :inline="true" class="-mb-15px" label-width="68px"> <el-form-item label="流程名称" prop="name"> <el-input v-model="queryParams.processInstanceName" + @keyup.enter="handleQuery" class="!w-240px" clearable placeholder="请输入流程名称" @@ -39,7 +41,12 @@ <ContentWrap> <el-table v-loading="loading" :data="list"> <el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" /> - <el-table-column align="center" label="流程发起人" prop="startUserName" min-width="100" /> + <el-table-column + align="center" + label="流程发起人" + prop="startUser.nickname" + min-width="100" + /> <el-table-column :formatter="dateFormatter" align="center" @@ -47,8 +54,11 @@ 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="抄送节点" prop="activityName" min-width="180" /> + <el-table-column align="center" label="抄送人" min-width="100"> + <template #default="scope"> {{ scope.row.createUser?.nickname || '系统' }} </template> + </el-table-column> + <el-table-column align="center" label="抄送意见" prop="reason" width="150" /> <el-table-column align="center" label="抄送时间" @@ -105,11 +115,16 @@ /** 处理审批按钮 */ const handleAudit = (row: any) => { + const query = { + id: row.processInstanceId, + activityId: undefined + } + if (row.activityId) { + query.activityId = row.activityId + } push({ name: 'BpmProcessInstanceDetail', - query: { - id: row.processInstanceId - } + query: query }) } diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue index d54ad61..e83e9ed 100644 --- a/src/views/bpm/task/done/index.vue +++ b/src/views/bpm/task/done/index.vue @@ -8,7 +8,7 @@ class="-mb-15px" label-width="68px" > - <el-form-item label="任务名称" prop="name"> + <el-form-item label="" prop="name"> <el-input v-model="queryParams.name" class="!w-240px" @@ -17,27 +17,96 @@ @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-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程分类" + clearable + class="!w-155px" + @change="handleQuery" + > + <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" :style="{ position: 'absolute', right: '130px' }"> + <el-select + v-model="queryParams.status" + placeholder="请选择流程状态" + clearable + class="!w-155px" + @change="handleQuery" + > + <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 :style="{ position: 'absolute', right: '0px' }"> + <el-popover + :visible="showPopover" + persistent + :width="400" + :show-arrow="false" + placement="bottom-end" + > + <template #reference> + <el-button @click="showPopover = !showPopover" > + <Icon icon="ep:plus" class="mr-5px" />高级筛选 + </el-button> + + </template> + <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程发起人" + clearable + class="!w-390px" + > + <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="发起时间" class="bold-label" label-position="top" 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 class="bold-label" label-position="top"> + <el-button @click="handleQuery"> 确认</el-button> + <el-button @click="showPopover = false"> 取消</el-button> + <el-button @click="resetQuery"> 清空</el-button> + </el-form-item> + </el-popover> + </el-form-item> + </el-form> </ContentWrap> @@ -102,9 +171,10 @@ </ContentWrap> </template> <script lang="ts" setup> -import { DICT_TYPE } from '@/utils/dict' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter, formatPast2 } from '@/utils/formatTime' import * as TaskApi from '@/api/bpm/task' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' defineOptions({ name: 'BpmTodoTask' }) @@ -117,9 +187,13 @@ pageNo: 1, pageSize: 10, name: '', + category: undefined, + status: undefined, createTime: [] }) const queryFormRef = ref() // 搜索的表单 +const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 +const showPopover = ref(false) /** 查询任务列表 */ const getList = async () => { @@ -150,13 +224,15 @@ push({ name: 'BpmProcessInstanceDetail', query: { - id: row.processInstance.id + id: row.processInstance.id, + taskId: row.id } }) } /** 初始化 **/ -onMounted(() => { - getList() +onMounted(async () => { + await getList() + categoryList.value = await CategoryApi.getCategorySimpleList() }) </script> diff --git a/src/views/bpm/task/manager/index.vue b/src/views/bpm/task/manager/index.vue index fb903d7..d608869 100644 --- a/src/views/bpm/task/manager/index.vue +++ b/src/views/bpm/task/manager/index.vue @@ -1,4 +1,5 @@ <template> + <ContentWrap> <!-- 搜索工作栏 --> <el-form diff --git a/src/views/bpm/task/todo/index.vue b/src/views/bpm/task/todo/index.vue index 27aae87..e1449b1 100644 --- a/src/views/bpm/task/todo/index.vue +++ b/src/views/bpm/task/todo/index.vue @@ -8,7 +8,7 @@ class="-mb-15px" label-width="68px" > - <el-form-item label="任务名称" prop="name"> + <el-form-item label="" prop="name"> <el-input v-model="queryParams.name" class="!w-240px" @@ -17,27 +17,79 @@ @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-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程分类" + clearable + class="!w-155px" + @change="handleQuery" + > + <el-option + v-for="category in categoryList" + :key="category.code" + :label="category.name" + :value="category.code" + /> + </el-select> + </el-form-item> + + <!-- 高级筛选 --> + <el-form-item :style="{ position: 'absolute', right: '0px' }"> + <el-popover + :visible="showPopover" + persistent + :width="400" + :show-arrow="false" + placement="bottom-end" + > + <template #reference> + <el-button @click="showPopover = !showPopover" > + <Icon icon="ep:plus" class="mr-5px" />高级筛选 + </el-button> + + </template> + <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category"> + <el-select + v-model="queryParams.category" + placeholder="请选择流程发起人" + clearable + class="!w-390px" + > + <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="发起时间" class="bold-label" label-position="top" 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 class="bold-label" label-position="top"> + <el-button @click="handleQuery"> 确认</el-button> + <el-button @click="showPopover = false"> 取消</el-button> + <el-button @click="resetQuery"> 清空</el-button> + </el-form-item> + </el-popover> + </el-form-item> + </el-form> </ContentWrap> @@ -87,6 +139,7 @@ <script lang="ts" setup> import { dateFormatter } from '@/utils/formatTime' import * as TaskApi from '@/api/bpm/task' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' defineOptions({ name: 'BpmTodoTask' }) @@ -99,9 +152,11 @@ pageNo: 1, pageSize: 10, name: '', + category: undefined, createTime: [] }) const queryFormRef = ref() // 搜索的表单 +const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 /** 查询任务列表 */ const getList = async () => { @@ -114,6 +169,8 @@ loading.value = false } } + +const showPopover = ref(false) /** 搜索按钮操作 */ const handleQuery = () => { @@ -132,13 +189,15 @@ push({ name: 'BpmProcessInstanceDetail', query: { - id: row.processInstance.id + id: row.processInstance.id, + taskId: row.id } }) } /** 初始化 **/ -onMounted(() => { - getList() +onMounted(async () => { + await getList() + categoryList.value = await CategoryApi.getCategorySimpleList() }) </script> diff --git a/src/views/data/channel/http/api/tag/index.vue b/src/views/data/channel/http/api/tag/index.vue index 9a38a69..1976db9 100644 --- a/src/views/data/channel/http/api/tag/index.vue +++ b/src/views/data/channel/http/api/tag/index.vue @@ -122,7 +122,12 @@ label="数据质量" header-align="center" align="center" - /> + > + <template #default="scope"> + <el-tag v-if="scope.row.dataQuality === 'Good'" size="small" type="success">{{scope.row.dataQuality}}</el-tag> + <el-tag v-if="scope.row.dataQuality === 'Bad'" size="small" type="danger">{{scope.row.dataQuality}}</el-tag> + </template> + </el-table-column> <el-table-column label="操作" align="center" min-width="110" fixed="right"> <template #default="scope"> <el-button diff --git a/src/views/data/ind/category/CategoryForm.vue b/src/views/data/ind/category/CategoryForm.vue index d4bd86c..40cd2fb 100644 --- a/src/views/data/ind/category/CategoryForm.vue +++ b/src/views/data/ind/category/CategoryForm.vue @@ -33,13 +33,13 @@ <script lang="ts" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import * as CategoryApi from '@/api/data/ind/category' - import { CACHE_KEY, useCache } from '@/hooks/web/useCache' + import {CACHE_KEY, useSessionCache} from '@/hooks/web/useCache' import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants' import { defaultProps, handleTree } from '@/utils/tree' defineOptions({ name: 'IndItemCategoryForm' }) - const { wsCache } = useCache() + const { wsSessionCache } = useSessionCache() const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -114,7 +114,7 @@ } finally { formLoading.value = false // 清空,从而触发刷新 - wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) } } diff --git a/src/views/data/ind/data/DataSetForm.vue b/src/views/data/ind/data/DataSetForm.vue index 132e25c..4b03f66 100644 --- a/src/views/data/ind/data/DataSetForm.vue +++ b/src/views/data/ind/data/DataSetForm.vue @@ -21,7 +21,8 @@ </el-select> </el-form-item> <el-form-item label="查询语句" prop="querySql"> - <el-input v-model="formData.querySql" placeholder="请输入内容" type="textarea" maxlength="200" + <el-input v-model="formData.querySql" placeholder="请输入内容" type="textarea" maxlength="500" + :rows="6" show-word-limit spellcheck="false"/> </el-form-item> <el-form-item label="备注" prop="remark"> diff --git a/src/views/data/ind/item/AtomIndDefineForm.vue b/src/views/data/ind/item/AtomIndDefineForm.vue index 00ab2f5..225d5c1 100644 --- a/src/views/data/ind/item/AtomIndDefineForm.vue +++ b/src/views/data/ind/item/AtomIndDefineForm.vue @@ -20,19 +20,19 @@ <el-row> <el-col :span="12"> <el-form-item label="指标分类" prop="itemCategory"> - <el-select v-model="formData.itemCategory" clearable placeholder="请选择指标分类"> - <el-option - v-for="item in dataCategoryList" - :key="item.id" - :label="item.label" - :value="item.id + ''" - /> - </el-select> + <el-tree-select + v-model="formData.itemCategory" + :data="dataCategoryList" + :default-expanded-keys="[0]" + :props="defaultProps" + check-strictly + node-key="id" + /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="时间粒度" prop="timeGranularity"> - <el-select v-model="formData.timeGranularity" placeholder="请选择"> + <el-select v-model="formData.timeGranularity" clearable placeholder="请选择"> <el-option v-for="dict in getStrDictOptions(DICT_TYPE.TIME_GRANULARITY)" :key="dict.value" @@ -61,9 +61,10 @@ </el-col> </el-row> <el-row> - <el-col :span="6"> + <el-col :span="12"> <el-form-item label="数据集" prop="atomItem.dataSet"> - <el-select v-model="formData.atomItem.dataSet" clearable placeholder="请选择数据集" @change="handleDataSetChange($event)"> + <el-select v-model="formData.atomItem.dataSet" filterable + allow-create clearable placeholder="请选择数据集" @change="handleDataSetChange($event)"> <el-option v-for="item in dataSetList" :key="item.id" @@ -75,7 +76,8 @@ </el-col> <el-col :span="6"> <el-form-item label="使用字段" prop="atomItem.usingField"> - <el-select v-model="formData.atomItem.usingField" clearable placeholder="请选择字段"> + <el-select v-model="formData.atomItem.usingField" filterable + allow-create clearable placeholder="请选择字段"> <el-option v-for="item in dataSetFieldList" :key="item.id" @@ -87,7 +89,8 @@ </el-col> <el-col :span="6"> <el-form-item label="统计方式" prop="statFunc"> - <el-select v-model="formData.atomItem.statFunc" clearable placeholder="请选择"> + <el-select v-model="formData.atomItem.statFunc" filterable + allow-create clearable placeholder="请选择"> <el-option v-for="dict in getStrDictOptions(DICT_TYPE.DATA_STAT_FUNC)" :key="dict.value" @@ -118,7 +121,7 @@ import * as DataSetApi from '@/api/data/ind/data/data.set' import * as DataSetFieldApi from '@/api/data/ind/data/data.field' import * as CategoryApi from '@/api/data/ind/category/index' - + import {handleTree} from "@/utils/tree"; defineOptions({name: 'IndDataSetForm'}) @@ -167,7 +170,16 @@ const formRef = ref() // 表单 Ref const dataSetList = ref([] as DataSetApi.DataSetVO[]) const dataSetFieldList = ref([] as DataSetFieldApi.DataSetFieldVO[]) - const dataCategoryList = ref([]) + + const dataCategoryList = ref<Tree[]>([]) + + const getCategoryTree = async () => { + dataCategoryList.value = [] + const res = await CategoryApi.getCategoryListAllSimple() + let category: Tree = {id: 0, label: '主类目', children: []} + category.children = handleTree(res, 'id', 'pid') + dataCategoryList.value.push(category) + } /** 打开弹窗 */ const open = async (type: string, id?: string) => { dialogVisible.value = true @@ -176,7 +188,7 @@ resetForm() // 加载数据源列表 dataSetList.value = await DataSetApi.getDataSetList() - dataCategoryList.value = await CategoryApi.getCategoryListAllSimple() + await getCategoryTree() // 修改时,设置数据 if (id) { formLoading.value = true diff --git a/src/views/data/ind/item/CalIndDefineForm.vue b/src/views/data/ind/item/CalIndDefineForm.vue index 0d5fc81..52d78cc 100644 --- a/src/views/data/ind/item/CalIndDefineForm.vue +++ b/src/views/data/ind/item/CalIndDefineForm.vue @@ -20,14 +20,14 @@ <el-row> <el-col :span="12"> <el-form-item label="指标分类" prop="itemCategory"> - <el-select v-model="formData.itemCategory" clearable placeholder="请选择指标分类"> - <el-option - v-for="item in dataCategoryList" - :key="item.id" - :label="item.label" - :value="item.id + ''" - /> - </el-select> + <el-tree-select + v-model="formData.itemCategory" + :data="dataCategoryList" + :default-expanded-keys="[0]" + :props="defaultProps" + check-strictly + node-key="id" + /> </el-form-item> </el-col> <el-col :span="12"> @@ -139,6 +139,7 @@ import * as ItemApi from '@/api/data/ind/item/item' import { ElMessage } from 'element-plus' import * as CategoryApi from '@/api/data/ind/category/index' + import {handleTree} from "@/utils/tree"; defineOptions({name: 'IndDataSetForm'}) @@ -183,14 +184,22 @@ const operatorList = ref(['+', '-', '*', '/', '&', '|', '!', '>', '<']) const formRules = reactive({ itemName: [{required: true, message: '指标名称不能为空', trigger: 'blur'}], - itemCategory: [{required: true, message: '指标类型不能为空', trigger: 'blur'}], - precision: [{validator: validateAsNumber, trigger: 'blur' }], - coefficient: [{validator: validateAsNumber, trigger: 'blur' }], + itemCategory: [{required: true, message: '指标类型不能为空', trigger: 'blur'}] + // precision: [{validator: validateAsNumber, trigger: 'blur' }], + // coefficient: [{validator: validateAsNumber, trigger: 'blur' }], }) const formRef = ref() // 表单 Ref const dataSourceList = ref([] as DataSourceConfigApi.DataSourceConfigVO[]) const queryParams = reactive({}) - const dataCategoryList = ref([] as CategoryApi.IndItemCategoryVO[]) + + const dataCategoryList = ref<Tree[]>([]) + const getCategoryTree = async () => { + dataCategoryList.value = [] + const res = await CategoryApi.getCategoryListAllSimple() + let category: Tree = {id: 0, label: '主类目', children: []} + category.children = handleTree(res, 'id', 'pid') + dataCategoryList.value.push(category) + } /** 打开弹窗 */ const open = async (type: string, id?: number) => { dialogVisible.value = true @@ -199,7 +208,7 @@ resetForm() // 加载数据源列表 - dataCategoryList.value = await CategoryApi.getCategoryListAllSimple() + await getCategoryTree() itemList.value = await ItemApi.getItemList(queryParams) // 修改时,设置数据 if (id) { diff --git a/src/views/data/ind/item/DerIndDefineForm.vue b/src/views/data/ind/item/DerIndDefineForm.vue index 3822e86..dc35975 100644 --- a/src/views/data/ind/item/DerIndDefineForm.vue +++ b/src/views/data/ind/item/DerIndDefineForm.vue @@ -7,8 +7,21 @@ :rules="formRules" label-width="100px"> <el-row> <el-col :span="12"> + <el-form-item label="指标编码" prop="itemNo"> + <el-input v-model="formData.itemNo" disabled/> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="指标名称" prop="itemName"> + <el-input v-model="formData.itemName"/> + </el-form-item> + </el-col> + </el-row> + <el-row> + <el-col :span="12"> <el-form-item label="原子指标" prop="atomItem.itemId"> - <el-select v-model="formData.atomItem.itemId" clearable placeholder="请选择原子指标" + <el-select v-model="formData.atomItem.itemId" filterable + allow-create clearable placeholder="请选择原子指标" @change="handleChange($event)"> <el-option v-for="item in atomItemList" @@ -27,27 +40,15 @@ </el-row> <el-row> <el-col :span="12"> - <el-form-item label="指标编码" prop="itemNo"> - <el-input v-model="formData.itemNo" disabled/> - </el-form-item> - </el-col> - <el-col :span="12"> - <el-form-item label="指标名称" prop="itemName"> - <el-input v-model="formData.itemName"/> - </el-form-item> - </el-col> - </el-row> - <el-row> - <el-col :span="12"> <el-form-item label="指标分类" prop="itemCategory"> - <el-select v-model="formData.itemCategory" clearable placeholder="请选择指标分类"> - <el-option - v-for="item in dataCategoryList" - :key="item.id" - :label="item.label" - :value="item.id + ''" - /> - </el-select> + <el-tree-select + v-model="formData.itemCategory" + :data="dataCategoryList" + :default-expanded-keys="[0]" + :props="defaultProps" + check-strictly + node-key="id" + /> </el-form-item> </el-col> <el-col :span="12"> @@ -83,7 +84,7 @@ <el-row> <el-col :span="6"> <el-form-item label="时间标识" prop="timeLabel"> - <el-select v-model="formData.derItem.timeLabel" clearable placeholder="请选择时间标识"> + <el-select v-model="formData.derItem.timeLabel" allow-create filterable clearable placeholder="请选择时间标识"> <el-option v-for="item in dataSetFieldList" :key="item.id" @@ -130,7 +131,8 @@ <el-row> <el-col :span="24"> <el-form-item label="分析维度" prop="dimension"> - <el-select v-model="formData.derItem.dimension" clearable placeholder="请选择分析维度" multiple> + <el-select v-model="formData.derItem.dimension" filterable + allow-create clearable placeholder="请选择分析维度" multiple> <el-option v-for="item in dataSetFieldList" :key="item.id" @@ -164,6 +166,7 @@ import {PageParam} from "@/api/data/ind/item/item"; import * as CategoryApi from "@/api/data/ind/category"; import * as DataSetFieldApi from "@/api/data/ind/data/data.field"; + import {handleTree} from "@/utils/tree"; defineOptions({name: 'IndDataSetForm'}) @@ -218,9 +221,16 @@ const formRef = ref() // 表单 Ref const atomItemList = ref([] as ItemApi.ItemVO[]) const showTimeChange = ref(false) - const dataCategoryList = ref([] as CategoryApi.IndItemCategoryVO[]) const dataSetFieldList = ref([] as DataSetFieldApi.DataSetFieldVO[]) + const dataCategoryList = ref<Tree[]>([]) + const getCategoryTree = async () => { + dataCategoryList.value = [] + const res = await CategoryApi.getCategoryListAllSimple() + let category: Tree = {id: 0, label: '主类目', children: []} + category.children = handleTree(res, 'id', 'pid') + dataCategoryList.value.push(category) + } /** 打开弹窗 */ const open = async (type: string, id?: string) => { dialogVisible.value = true @@ -228,7 +238,7 @@ formType.value = type resetForm() // 加载数据源列表 - dataCategoryList.value = await CategoryApi.getCategoryListAllSimple() + await getCategoryTree() const queryParams = reactive({ itemType: 'ATOM' }) diff --git a/src/views/data/plan/category/CategoryForm.vue b/src/views/data/plan/category/CategoryForm.vue index 5bbbaad..8fa7de0 100644 --- a/src/views/data/plan/category/CategoryForm.vue +++ b/src/views/data/plan/category/CategoryForm.vue @@ -33,13 +33,13 @@ <script lang="ts" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import * as CategoryApi from '@/api/data/plan/category' -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import {CACHE_KEY, useCache, useSessionCache} from '@/hooks/web/useCache' import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants' import { defaultProps, handleTree } from '@/utils/tree' defineOptions({ name: 'PlanItemCategoryForm' }) -const { wsCache } = useCache() +const { wsSessionCache } = useSessionCache() const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -114,7 +114,7 @@ } finally { formLoading.value = false // 清空,从而触发刷新 - wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) } } diff --git a/src/views/data/point/DaPointChart.vue b/src/views/data/point/DaPointChart.vue index 8713861..caaa271 100644 --- a/src/views/data/point/DaPointChart.vue +++ b/src/views/data/point/DaPointChart.vue @@ -87,6 +87,7 @@ defineExpose({open}) // 提供 open 方法,用于打开弹窗 + const loading = ref(false) async function getDataList() { visible.value = true; loading.value = true diff --git a/src/views/data/point/DaPointForm.vue b/src/views/data/point/DaPointForm.vue index 0279054..5a1682b 100644 --- a/src/views/data/point/DaPointForm.vue +++ b/src/views/data/point/DaPointForm.vue @@ -283,6 +283,38 @@ </el-form-item> </el-col> </el-row> + <!--累计点--> + <el-row :gutter="20" v-if="formData.pointType === 'CUMULATE'"> + <el-col :span="24"> + <el-form-item label="瞬时测点" prop="cumulatePoint.momentPoint"> + <el-select + v-model="formData.cumulatePoint.momentPoint" + filterable + placeholder="请选择"> + <el-option + v-for="(item, index) in pointList2" + :key="index" + :label="item.pointName" + :value="item.pointNo"/> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="累计长度" prop="cumulatePoint.length"> + <el-input-number v-model="formData.cumulatePoint.length" style="width: 100%" + :min="1" :max="3000" + :controls="false"/> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="除数" prop="cumulatePoint.divisor"> + <el-input-number v-model="formData.cumulatePoint.divisor" style="width: 100%" + :min="1" :max="3000" + :controls="false"/> + </el-form-item> + </el-col> + + </el-row> </el-form> <template #footer> <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> @@ -294,6 +326,7 @@ import * as DaPoint from '@/api/data/da/point' import * as TagApi from '@/api/data/channel/tag' import {DICT_TYPE, getDictOptions, getIntDictOptions} from "@/utils/dict"; +import {getPointSimpleList} from "@/api/data/da/point"; defineOptions({name: 'DataDaPointForm'}) @@ -311,6 +344,13 @@ }]) const queryParams = reactive({ pointTypes: "MEASURE,CONSTANT", +}) +const pointList2 = ref([{ + pointName: '', + pointNo: '' +}]) +const queryParams2 = reactive({ + pointTypes: "MEASURE,CONSTANT,CALCULATE", }) const operatorList = ref(['+', '-', '*', '/', '&', '|', '!', '>', '<']) const formData = ref({ @@ -343,6 +383,13 @@ tagNo: '', dimension: '', valueType: '', + }, + cumulatePoint: { + id: '', + pointId: '', + momentPoint: '', + length: '', + divisor: '' } }) const formRules = reactive({ @@ -350,8 +397,11 @@ pointType: [{required: true, message: '测点类型不能为空', trigger: 'blur'}], dataType: [{required: true, message: '数据类型不能为空', trigger: 'blur'}], minfreqid: [{required: true, message: '采集频率不能为空', trigger: 'blur'}], - "measurePoint.valueType": [{required: true, message: '采集频率不能为空', trigger: 'blur'}], - "measurePoint.dimension": [{required: true, message: '采集频率不能为空', trigger: 'blur'}], + "measurePoint.valueType": [{required: true, message: '值类型不能为空', trigger: 'blur'}], + "measurePoint.dimension": [{required: true, message: '平滑尺度不能为空', trigger: 'blur'}], + "cumulatePoint.momentPoint": [{required: true, message: '累计测点不能为空', trigger: 'blur'}], + "cumulatePoint.length": [{required: true, message: '累计长度不能为空', trigger: 'blur'}], + "cumulatePoint.divisor": [{required: true, message: '除数不能为空', trigger: 'blur'}], }) const formRef = ref() // 表单 Ref @@ -363,6 +413,7 @@ resetForm() getSourceOption() getPointList() + getPointList2() // 修改时,设置数据 if (id) { formLoading.value = true @@ -465,6 +516,13 @@ tagNo: '', dimension: '1', valueType: 'SIMULATE', + }, + cumulatePoint: { + id: '', + pointId: '', + momentPoint: '', + length: 60, + divisor: 60 } } formRef.value?.resetFields() @@ -525,7 +583,11 @@ } const getPointList = async () => { - pointList.value = await DaPoint.getPointList(queryParams) + pointList.value = await DaPoint.getPointSimpleList(queryParams) +} + +const getPointList2 = async () => { + pointList2.value = await DaPoint.getPointSimpleList(queryParams2) } const getInfo = async (id) => { diff --git a/src/views/data/point/index.vue b/src/views/data/point/index.vue index 5d63b34..b11294c 100644 --- a/src/views/data/point/index.vue +++ b/src/views/data/point/index.vue @@ -35,6 +35,21 @@ class="!w-200px" /> </el-form-item> + <el-form-item label="采集质量" prop="collectQuality"> + <el-select + v-model="queryParams.collectQuality" + placeholder="请选择" + clearable + class="!w-240px" + > + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.DATA_QUALITY)" + :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" /> @@ -91,9 +106,9 @@ <!-- 列表 --> <ContentWrap> <el-table border stripe v-loading="loading" :data="list" @selection-change="selectionChangeHandle"> - <el-table-column type="selection" header-align="center" align="center" width="50"/> + <el-table-column type="selection" header-align="center" align="center" fixed="left" width="50"/> <el-table-column fixed label="测点编码" header-align="center" align="left" min-width="130" prop="pointNo" /> - <el-table-column label="测点名称" header-align="center" align="left" min-width="220" prop="pointName" /> + <el-table-column fixed label="测点名称" header-align="center" align="left" min-width="240" prop="pointName" /> <el-table-column label="测点类型" align="center" prop="pointType" width="100"> <template #default="scope"> <dict-tag :type="DICT_TYPE.DATA_POINT_TYPE" :value="scope.row.pointType" /> @@ -116,6 +131,13 @@ <el-table-column label="数据源类型" align="center" prop="sourceType" min-width="100"/> <el-table-column label="数据源名称" align="center" prop="sourceName" min-width="100"/> <el-table-column label="测点Tag" header-align="center" align="left" prop="tagNo" min-width="150"/> + <el-table-column label="采集质量" header-align="center" align="center" prop="collectQuality" width="100"> + <template #default="scope"> + <el-tag v-if="scope.row.collectQuality === 'Good'" size="small" type="success">{{scope.row.collectQuality}}</el-tag> + <el-tag v-if="scope.row.collectQuality === 'Bad'" size="small" type="danger">{{scope.row.collectQuality}}</el-tag> + </template> + </el-table-column> + <el-table-column label="采集时间" header-align="center" align="center" prop="collectTime" min-width="150"/> <el-table-column label="是否启用" align="center" prop="isEnable" width="85"> <template #default="scope"> <el-tag v-if="scope.row.isEnable === 1" size="small">是</el-tag> @@ -168,7 +190,7 @@ import * as DaPoint from '@/api/data/da/point' import {ref, reactive} from "vue"; import download from "@/utils/download"; -import {DICT_TYPE, getDictOptions} from "@/utils/dict"; +import {DICT_TYPE, getDictOptions, getIntDictOptions, getStrDictOptions} from "@/utils/dict"; import DaPointForm from './DaPointForm.vue' import DaPointChart from './DaPointChart.vue' import * as UserApi from "@/api/system/user"; @@ -188,6 +210,7 @@ pointNo: undefined, pointName: undefined, tagNo: undefined, + collectQuality: undefined, }) const queryFormRef = ref() // 搜索的表单 @@ -272,9 +295,8 @@ let ids = dataListSelections.map(item => { return item.id }) - // 启用的二次确认 - await message.enableConfirm(ids) - + // 二次确认 + await message.confirm('确认要开启所选测点?') await DaPoint.enable(ids) message.success(t('common.enableSuccess')) await getList() @@ -284,9 +306,8 @@ let ids = dataListSelections.map(item => { return item.id }) - // 启用的二次确认 - await message.disableConfirm(ids,) - + // 二次确认 + await message.confirm('确认要禁用所选测点?') await DaPoint.disable(ids) message.success(t('common.disableSuccess')) await getList() diff --git a/src/views/infra/apiAccessLog/index.vue b/src/views/infra/apiAccessLog/index.vue index e3a6a7c..7e3a070 100644 --- a/src/views/infra/apiAccessLog/index.vue +++ b/src/views/infra/apiAccessLog/index.vue @@ -90,31 +90,31 @@ <ContentWrap> <el-table v-loading="loading" :data="list"> <el-table-column label="日志编号" align="center" prop="id" width="100" fix="right" /> - <el-table-column label="用户编号" align="center" prop="userId" /> - <el-table-column label="用户类型" align="center" prop="userType"> + <el-table-column label="用户编号" align="center" prop="userId" width="80"/> + <el-table-column label="用户类型" align="center" prop="userType" width="100"> <template #default="scope"> <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" /> </template> </el-table-column> - <el-table-column label="应用名" align="center" prop="applicationName" width="150" /> + <el-table-column label="应用名" align="center" prop="applicationName" width="120" /> <el-table-column label="请求方法" align="center" prop="requestMethod" width="80" /> - <el-table-column label="请求地址" align="center" prop="requestUrl" width="500" /> + <el-table-column label="请求地址" align="center" prop="requestUrl" /> <el-table-column label="请求时间" align="center" prop="beginTime" width="180"> <template #default="scope"> <span>{{ formatDate(scope.row.beginTime) }}</span> </template> </el-table-column> - <el-table-column label="执行时长" align="center" prop="duration" width="180"> + <el-table-column label="执行时长" align="center" prop="duration" width="100"> <template #default="scope"> {{ scope.row.duration }} ms </template> </el-table-column> - <el-table-column label="操作结果" align="center" prop="status"> + <el-table-column label="操作结果" align="center" prop="status" width="120"> <template #default="scope"> {{ scope.row.resultCode === 0 ? '成功' : '失败(' + scope.row.resultMsg + ')' }} </template> </el-table-column> <el-table-column label="操作模块" align="center" prop="operateModule" width="180" /> <el-table-column label="操作名" align="center" prop="operateName" width="180" /> - <el-table-column label="操作类型" align="center" prop="operateType"> + <el-table-column label="操作类型" align="center" prop="operateType" width="80"> <template #default="scope"> <dict-tag :type="DICT_TYPE.INFRA_OPERATE_TYPE" :value="scope.row.operateType" /> </template> diff --git a/src/views/infra/apiErrorLog/index.vue b/src/views/infra/apiErrorLog/index.vue index 22f3116..c337684 100644 --- a/src/views/infra/apiErrorLog/index.vue +++ b/src/views/infra/apiErrorLog/index.vue @@ -86,16 +86,16 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="日志编号" align="center" prop="id" /> - <el-table-column label="用户编号" align="center" prop="userId" /> - <el-table-column label="用户类型" align="center" prop="userType"> + <el-table-column label="日志编号" align="center" prop="id" width="100"/> + <el-table-column label="用户编号" align="center" prop="userId" width="80"/> + <el-table-column label="用户类型" align="center" prop="userType" width="100"> <template #default="scope"> <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" /> </template> </el-table-column> - <el-table-column label="应用名" align="center" prop="applicationName" width="200" /> + <el-table-column label="应用名" align="center" prop="applicationName" width="120" /> <el-table-column label="请求方法" align="center" prop="requestMethod" width="80" /> - <el-table-column label="请求地址" align="center" prop="requestUrl" width="180" /> + <el-table-column label="请求地址" align="center" prop="requestUrl" /> <el-table-column label="异常发生时间" align="center" @@ -103,8 +103,8 @@ width="180" :formatter="dateFormatter" /> - <el-table-column label="异常名" align="center" prop="exceptionName" width="180" /> - <el-table-column label="处理状态" align="center" prop="processStatus"> + <el-table-column label="异常名" align="center" prop="exceptionName" /> + <el-table-column label="处理状态" align="center" prop="processStatus" width="100"> <template #default="scope"> <dict-tag :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS" diff --git a/src/views/infra/config/index.vue b/src/views/infra/config/index.vue index e12e86d..0f48ede 100644 --- a/src/views/infra/config/index.vue +++ b/src/views/infra/config/index.vue @@ -79,17 +79,17 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="参数主键" align="center" prop="id" /> - <el-table-column label="参数分类" align="center" prop="category" /> + <el-table-column label="参数主键" align="center" prop="id" width="80"/> + <el-table-column label="参数分类" align="center" prop="category" width="80"/> <el-table-column label="参数名称" align="center" prop="name" :show-overflow-tooltip="true" /> - <el-table-column label="参数键名" align="center" prop="key" :show-overflow-tooltip="true" /> - <el-table-column label="参数键值" align="center" prop="value" /> - <el-table-column label="是否可见" align="center" prop="visible"> + <el-table-column label="参数键名" align="center" prop="key" :show-overflow-tooltip="true"/> + <el-table-column label="参数键值" align="center" prop="value" width="350"/> + <el-table-column label="是否可见" align="center" prop="visible" width="80"> <template #default="scope"> <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.visible" /> </template> </el-table-column> - <el-table-column label="系统内置" align="center" prop="type"> + <el-table-column label="系统内置" align="center" prop="type" width="80"> <template #default="scope"> <dict-tag :type="DICT_TYPE.INFRA_CONFIG_TYPE" :value="scope.row.type" /> </template> @@ -102,7 +102,7 @@ width="180" :formatter="dateFormatter" /> - <el-table-column label="操作" align="center"> + <el-table-column label="操作" align="center" width="120"> <template #default="scope"> <el-button link diff --git a/src/views/infra/dataSourceConfig/index.vue b/src/views/infra/dataSourceConfig/index.vue index 92bd301..3c1cdf6 100644 --- a/src/views/infra/dataSourceConfig/index.vue +++ b/src/views/infra/dataSourceConfig/index.vue @@ -18,10 +18,10 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="主键编号" align="center" prop="id" /> - <el-table-column label="数据源名称" align="center" prop="name" /> + <el-table-column label="主键编号" align="center" prop="id" width="100"/> + <el-table-column label="数据源名称" align="center" prop="name" width="100"/> <el-table-column label="数据源连接" align="center" prop="url" :show-overflow-tooltip="true" /> - <el-table-column label="用户名" align="center" prop="username" /> + <el-table-column label="用户名" align="center" prop="username" width="100"/> <el-table-column label="创建时间" align="center" @@ -29,7 +29,7 @@ width="180" :formatter="dateFormatter" /> - <el-table-column label="操作" align="center"> + <el-table-column label="操作" align="center" width="120"> <template #default="scope"> <el-button link diff --git a/src/views/infra/file/index.vue b/src/views/infra/file/index.vue index e1d2ffd..3598bfa 100644 --- a/src/views/infra/file/index.vue +++ b/src/views/infra/file/index.vue @@ -91,6 +91,9 @@ /> <el-table-column label="操作" align="center"> <template #default="scope"> + <el-button link type="primary" @click="copyToClipboard(scope.row.url)"> + 复制链接 + </el-button> <el-button link type="danger" @@ -168,6 +171,13 @@ formRef.value.open() } +/** 复制到剪贴板方法 */ +const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text).then(() => { + message.success('复制成功') + }) +} + /** 删除按钮操作 */ const handleDelete = async (id: number) => { try { diff --git a/src/views/micro/index.vue b/src/views/micro/index.vue index 1a4669c..92b9626 100644 --- a/src/views/micro/index.vue +++ b/src/views/micro/index.vue @@ -1,22 +1,20 @@ <template> <div class="sub-app"> - <WujieVue width="100%" height="100%" :name="name" :url="url" :alive="true" sync /> + <WujieVue width="100%" height="100%" v-bind="computedOptions" :degrade="true" :alive="true" sync /> </div> </template> <script lang="ts" setup> -import hostMap from "@/utils/hostMap"; -import wujieVue from "wujie-vue3"; -const route = useRoute() -const url = hostMap("//localhost:90/") + route.params.path -const name = 'fast' -watch(() => "$route.params.path", - () => { - wujieVue.bus.$emit("vue3-router-change", `/${route.params.path}`); - }, - { - immediate: true - } -) + import WujieVue from "wujie-vue3"; + import { useRoute } from 'vue-router' + import { computed } from 'vue' + + const route = useRoute() + const computedOptions = computed(() => { + return { + name: route.query.key, + url: route.query.url, + } + }) </script> <style scoped lang="scss"> .sub-app { diff --git a/src/views/model/mpk/file/MpkForm.vue b/src/views/model/mpk/file/MpkForm.vue index da3c881..8dfbae9 100644 --- a/src/views/model/mpk/file/MpkForm.vue +++ b/src/views/model/mpk/file/MpkForm.vue @@ -333,6 +333,9 @@ menuAndGroup: [ {required: true, message: '所属目录不能为空', trigger: 'blur'} ], + icon: [ + {required: true, message: 'icon不能为空', trigger: 'blur'} + ], }) const formRef = ref() // 表单 Ref @@ -451,7 +454,9 @@ if (id) { formLoading.value = true try { + debugger formData.value = await MpkApi.getMpk(id) + debugger } finally { formLoading.value = false } diff --git a/src/views/model/mpk/file/MpkRun.vue b/src/views/model/mpk/file/MpkRun.vue index 104377d..2b9039c 100644 --- a/src/views/model/mpk/file/MpkRun.vue +++ b/src/views/model/mpk/file/MpkRun.vue @@ -34,28 +34,24 @@ </el-col> </el-row> <el-divider content-position="left">模型参数信息</el-divider> - <el-row :gutter="20"> - <el-col :span="2" style="margin-bottom: 10px;margin-left: 20px"> - <el-button tag="a" :href="staticDir + '/template/模型参数导入模板.xlsx'" download="模型参数导入模板.xlsx" style="text-decoration: none;" type="primary" size="small" link>模板下载</el-button> - </el-col> - <el-col :span="2" style="margin-bottom: 10px;"> - <el-upload - ref="uploadRef" - v-model:file-list="fileList" - :show-file-list="false" - :action="importUrl" - :auto-upload="true" - :disabled="formLoading" - :before-upload="beforeUpload" - :headers="uploadHeaders" - :on-error="submitFormError" - :on-success="submitFormSuccess" - accept=".xlsx" - > - <el-button type="primary" size="small" link>参数导入</el-button> - </el-upload> - </el-col> - </el-row> + <div style="display:flex;flex-direction: row;align-items: center;margin-bottom: 6px"> + <el-button tag="a" :href="staticDir + '/template/模型参数导入模板.xlsx'" download="模型参数导入模板.xlsx" style="text-decoration: none;" type="primary" size="small" link>模板下载</el-button> + <el-upload + ref="uploadRef" + v-model:file-list="fileList" + :show-file-list="false" + :action="importUrl" + :auto-upload="true" + :disabled="formLoading" + :before-upload="beforeUpload" + :headers="uploadHeaders" + :on-error="submitFormError" + :on-success="submitFormSuccess" + accept=".xlsx" + > + <el-button type="primary" size="small" link>参数导入</el-button> + </el-upload> + </div> <el-row v-for="(item,index) in datas" :key="index" :gutter="20"> <el-col :span="24"> <el-form-item :label="'参数_' + (index)" required style="width: 100%"> @@ -87,23 +83,31 @@ </el-table-column> <el-table-column prop="" + label="参数名称" + align="center"> + <template #default="scope"> + <el-input size="small" v-model="scope.row.name" :disabled="true" maxlength="50" clearable /> + </template> + </el-table-column> + <el-table-column + prop="" label="参数value" align="center"> <template #default="scope"> <el-input size="small" v-model="scope.row.settingValue" :disabled="scope.row.settingKey === 'pyFile'" maxlength="50" clearable /> </template> </el-table-column> - <el-table-column label="操作" fixed="right" header-align="center" align="center" width="100"> - <template #default="scope"> - <el-button - @click="deleteRow(scope.$index)" - key="danger" - type="danger" - :disabled="scope.row.settingKey === 'pyFile'" - link - >删除</el-button> - </template> - </el-table-column> +<!-- <el-table-column label="操作" fixed="right" header-align="center" align="center" width="100">--> +<!-- <template #default="scope">--> +<!-- <el-button--> +<!-- @click="deleteRow(scope.$index)"--> +<!-- key="danger"--> +<!-- type="danger"--> +<!-- :disabled="scope.row.settingKey === 'pyFile'"--> +<!-- link--> +<!-- >删除</el-button>--> +<!-- </template>--> +<!-- </el-table-column>--> </el-table> <el-divider content-position="left">模型运行结果</el-divider> <el-input v-model="modelRunResult" placeholder="" rows="4" type="textarea" /> diff --git a/src/views/model/mpk/file/index.vue b/src/views/model/mpk/file/index.vue index 8e98f7f..a3d2bde 100644 --- a/src/views/model/mpk/file/index.vue +++ b/src/views/model/mpk/file/index.vue @@ -22,11 +22,20 @@ ref="queryFormRef" :inline="true" label-width="68px" + @submit.prevent > - <el-form-item label="模型名称" prop="pyName"> + <el-form-item label="模型名称" prop="pyChineseName"> + <el-input + v-model="queryParams.pyChineseName" + placeholder="请输入模型名称" + clearable + class="!w-240px" + /> + </el-form-item> + <el-form-item label="模型文件" prop="pyName"> <el-input v-model="queryParams.pyName" - placeholder="请输入模型名称" + placeholder="请输入模型文件名称" clearable class="!w-240px" /> @@ -58,8 +67,8 @@ :data="list" row-key="id" > - <el-table-column prop="pyChineseName" label="模型名称" header-align="center" align="center" min-width="100" /> - <el-table-column prop="pyName" label="模型文件" header-align="center" align="center" min-width="300"/> + <el-table-column prop="pyChineseName" label="模型名称" header-align="center" align="left" min-width="100" /> + <el-table-column prop="pyName" label="模型文件" header-align="center" align="left" min-width="300"/> <el-table-column prop="pyType" label="模型类型" :formatter="(r,c,v) => getDictLabel(DICT_TYPE.MODEL_TYPE,v)"/> <el-table-column prop="menuName" label="所属菜单" min-width="120px"/> <el-table-column prop="groupName" label="所属组" min-width="120px"/> @@ -147,6 +156,7 @@ const queryParams = reactive({ page: 1, limit: 10, + pyChineseName: '', pyName: '', label: '' }) diff --git a/src/views/model/mpk/icon/index.vue b/src/views/model/mpk/icon/index.vue index 2f97330..e9c3654 100644 --- a/src/views/model/mpk/icon/index.vue +++ b/src/views/model/mpk/icon/index.vue @@ -7,6 +7,7 @@ ref="queryFormRef" :inline="true" label-width="68px" + @submit.prevent > <el-form-item label="模型名称" prop="iconName"> <el-input diff --git a/src/views/model/mpk/menu/index.vue b/src/views/model/mpk/menu/index.vue index 0578c7e..6ba4c18 100644 --- a/src/views/model/mpk/menu/index.vue +++ b/src/views/model/mpk/menu/index.vue @@ -7,6 +7,7 @@ ref="queryFormRef" :inline="true" label-width="68px" + @submit.prevent > <el-form-item label="菜单名称" prop="name"> <el-input diff --git a/src/views/model/mpk/pack/index.vue b/src/views/model/mpk/pack/index.vue index f3e4a4c..da368c7 100644 --- a/src/views/model/mpk/pack/index.vue +++ b/src/views/model/mpk/pack/index.vue @@ -7,6 +7,7 @@ ref="queryFormRef" :inline="true" label-width="68px" + @submit.prevent > <el-form-item label="名称" prop="packName"> <el-input diff --git a/src/views/model/mpk/project/ProjectForm.vue b/src/views/model/mpk/project/ProjectForm.vue index d6937f7..585271e 100644 --- a/src/views/model/mpk/project/ProjectForm.vue +++ b/src/views/model/mpk/project/ProjectForm.vue @@ -140,7 +140,7 @@ // 所有模型列表 const modelList = ref([]) const getModelList = async () => { - modelList.value = await MpkApi.list() + modelList.value = await MpkApi.list({}) } // 模型筛选 diff --git a/src/views/model/mpk/project/ProjectPackage.vue b/src/views/model/mpk/project/ProjectPackage.vue index 3034300..8ba4c25 100644 --- a/src/views/model/mpk/project/ProjectPackage.vue +++ b/src/views/model/mpk/project/ProjectPackage.vue @@ -3,6 +3,7 @@ <el-form ref="formRef" v-loading="formLoading" + element-loading-text="打包时间较长,请耐心等待" :model="formData" :rules="formRules" label-width="80px" @@ -57,17 +58,15 @@ projectId: undefined, projectName: undefined, projectCode: undefined, - ids: undefined, version: undefined, }) /** 打开弹窗 */ - const open = async (projectId,projectName,projectCode,ids) => { + const open = async (projectId,projectName,projectCode) => { dialogVisible.value = true formData.projectId = projectId formData.projectName = projectName formData.projectCode = projectCode - formData.ids = ids formData.log = undefined formData.version = 'V' } diff --git a/src/views/model/mpk/project/index.vue b/src/views/model/mpk/project/index.vue index e7d90ba..7bc116d 100644 --- a/src/views/model/mpk/project/index.vue +++ b/src/views/model/mpk/project/index.vue @@ -133,6 +133,7 @@ import ProjectForm from './ProjectForm.vue' import ProjectPackage from './ProjectPackage.vue' import RelevanceModel from './ProjectPackageModelDialog.vue' + import * as projectApi from "@/api/model/mpk/project"; defineOptions({name: 'MpkProject'}) @@ -165,7 +166,7 @@ const handleCommand = (command: string, row) => { switch (command) { case 'packageModel': - packageModel(row.id, row.projectName, row.projectCode, row.models) + packageModel(row.id, row.projectName, row.projectCode) break default: break @@ -174,13 +175,14 @@ //打包 const projectPackageRef = ref(); - const packageModel = (projectId, projectName, projectCode, models) => { - let ids = models.map(e => e.id); - if (ids && ids.length > 0) { - projectPackageRef.value.open(projectId, projectName, projectCode, ids.join(",")); - } else { + const packageModel = async (projectId, projectName, projectCode) => { + //校验是否关联模型 + const data = await projectApi.getProjectModel({page: 1, pageSize: 1, projectId: projectId}) + if (data.total === 0) { message.error("请先为项目添加模型!") + return } + projectPackageRef.value.open(projectId, projectName, projectCode); } /** 搜索按钮操作 */ diff --git a/src/views/model/pre/dm/index.vue b/src/views/model/pre/dm/index.vue index 77ebac9..27df07a 100644 --- a/src/views/model/pre/dm/index.vue +++ b/src/views/model/pre/dm/index.vue @@ -7,6 +7,7 @@ ref="queryFormRef" :inline="true" label-width="68px" + @submit.prevent > <el-form-item label="名称" prop="modulename"> <el-input diff --git a/src/views/model/pre/item/MmPredictItemChart.vue b/src/views/model/pre/item/MmPredictItemChart.vue index a0f867a..4d24c06 100644 --- a/src/views/model/pre/item/MmPredictItemChart.vue +++ b/src/views/model/pre/item/MmPredictItemChart.vue @@ -3,6 +3,7 @@ title="预测数据" :close-on-click-modal="false" width="50%" + @close="dialogClose" v-model="visible" > <el-form @@ -12,7 +13,6 @@ > <el-form-item label="开始时间"> <el-date-picker - size="mini" v-model="dataForm.startTime" format="YYYY-MM-DD HH:mm:00" value-format="YYYY-MM-DD HH:mm:00" @@ -22,7 +22,6 @@ </el-form-item> <el-form-item label="结束时间"> <el-date-picker - size="mini" v-model="dataForm.endTime" format="YYYY-MM-DD HH:mm:00" value-format="YYYY-MM-DD HH:mm:00" @@ -58,8 +57,8 @@ const message = useMessage() // 消息弹窗 const visible = ref(false); -const chartDomPre = ref(null); -let myChart = null; +const chartDomPre = ref(); +let myChart = undefined; const chartParams = reactive({ itemId: undefined, startTime: undefined, @@ -79,7 +78,16 @@ dataForm.value.id = row.id; dataForm.value.itemName = row.itemname; if (row.id) { + nextTick(() => { + myChart = echarts.init(chartDomPre.value); + }); getDataList(); + } +} + +const dialogClose = () => { + if (myChart) { + myChart.dispose(); // 组件卸载时销毁实例 } } @@ -94,12 +102,12 @@ chartParams.endTime = dataForm.value.endTime; const data = await McsApi.getPreDataItemChart(chartParams) let legendData = [] - if (data.legend && data.legend.length > 0) { - data.legend.forEach(item => { - legendData.push(item + ":" + '真实值') - legendData.push(item + ":" + '预测值') - }) - } + // if (data.legend && data.legend.length > 0) { + // data.legend.forEach(item => { + // legendData.push(item + ":" + '真实值') + // legendData.push(item + ":" + '预测值') + // }) + // } let seriesData = [] if (data.predictTime) { @@ -131,18 +139,22 @@ if (data.viewMap) { Object.keys(data.viewMap).forEach(key => { let viewData = data.viewMap[key] - seriesData.push({ - name: key + ":" + '真实值', - type: "line", - data: viewData.realData, - showSymbol: false, - smooth: false, - lineStyle: { - normal: { - width: 1, + if(viewData.realData) { + legendData.push(key + ":" + '真实值') + seriesData.push({ + name: key + ":" + '真实值', + type: "line", + data: viewData.realData, + showSymbol: false, + smooth: false, + lineStyle: { + normal: { + width: 1, + }, }, - }, - }) + }) + } + legendData.push(key + ":" + '预测值') seriesData.push({ name: key + ":" + '预测值', type: "line", @@ -158,7 +170,6 @@ }) } - myChart = echarts.init(chartDomPre.value); const option = { title: { text: dataForm.value.itemName, diff --git a/src/views/model/pre/item/MmPredictItemForm.vue b/src/views/model/pre/item/MmPredictItemForm.vue index 2ee0b2e..c8eee15 100644 --- a/src/views/model/pre/item/MmPredictItemForm.vue +++ b/src/views/model/pre/item/MmPredictItemForm.vue @@ -78,7 +78,7 @@ <el-row> <el-col :span="12"> <el-form-item label="管网" prop="dmModuleItem.moduleid"> - <el-select v-model="dataForm.dmModuleItem.moduleid" placeholder="请选择"> + <el-select v-model="dataForm.dmModuleItem.moduleid" placeholder="请选择" @change="clearExpressionList"> <el-option v-for="item in moduleList" :key="item.id" @@ -103,10 +103,26 @@ </el-row> <el-row v-if="dataForm.itemtypename === 'MergeItem'"> <el-col :span="12"> - <el-form-item label="预测长度"> + <el-form-item label="预测长度" prop="mmPredictItem.predictlength"> <el-input + @change="clearExpressionList" v-model="dataForm.mmPredictItem.predictlength" placeholder="预测长度" maxlength="5"/> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="真实数据点"> + <el-select + v-model="dataForm.pointId" + filterable + clearable + placeholder="请选择"> + <el-option + v-for="item in pointList" + :key="item.id" + :label="item.pointName" + :value="item.id"/> + </el-select> </el-form-item> </el-col> </el-row> @@ -135,9 +151,7 @@ <Icon icon="ep:upload"/> 上传模型 </el-button> - <el-button - size="small" type="primary" @click="setReplaceModelOnly(true)" - v-if="formType.value === 'update'"> + <el-button type="primary" plain @click="setReplaceModelOnly(true)"> <Icon icon="ep:upload"/> 更新模型 </el-button> @@ -146,7 +160,7 @@ </el-row> <el-row v-if="dataForm.itemtypename === 'NormalItem'"> <el-col :span="12"> - <el-form-item label="关联项目"> + <el-form-item label="关联项目" prop="mmPredictModel.mpkprojectid"> <el-select v-model="dataForm.mmPredictModel.mpkprojectid" placeholder="请选择"> <el-option v-for="item in mpkProjectList" @@ -201,7 +215,8 @@ <el-divider content-position="left" v-if="dataForm.itemtypename === 'NormalItem'">模型输出 </el-divider> <el-button - @click="addItemOutput(dataForm.mmItemOutputList)" + v-if="dataForm.itemtypename === 'NormalItem'" + @click="addItemOutput()" type="primary" size="small"> 添加 @@ -249,6 +264,7 @@ <el-select v-model="scope.row.pointid" filterable + clearable @change="(value) => changeOutputPoint(value,scope.row)" placeholder="请选择"> <el-option @@ -282,7 +298,8 @@ <el-table-column prop="valuetype" label="类型" align="center" min-width="150"/> <el-table-column prop="" label="值" align="center" min-width="200"> <template #default="scope"> - <el-input size="mini" v-model="scope.row.value" maxlength="256" + <el-input v-model="scope.row.value" maxlength="1000" + :disabled="scope.row.key === 'pyFile'" style="width:100%;height:100%"/> </template> </el-table-column> @@ -312,7 +329,26 @@ </el-table-column> <el-table-column prop="" label="参数名称" align="center"> <template #default="scope"> - <el-select + <el-select v-if="scope.row.modelparamtype === 'NormalItem'" + v-model="scope.row.modelparamid" + placeholder="请选择" + filterable + @change="changeModelparam(scope.row)" + style="width: 100%"> + <el-option-group + v-for="group in modelparamListMap['NormalItem']" + :key="group.value" + :label="group.label" + > + <el-option + v-for="item in group.children" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-option-group> + </el-select> + <el-select v-else v-model="scope.row.modelparamid" filterable @change="changeModelparam(scope.row)" @@ -336,14 +372,12 @@ <template #default="scope"> <el-button @click="addRow(scope.$index, dataForm.mmModelParamList)" - type="text" - size="mini"> + type="text"> 添加 </el-button> <el-button - @click="deleteRow(scope.$index, dataForm.mmModelParamList)" - type="text" - size="mini"> + @click="deleteRow(scope.$index, scope.row, dataForm.mmModelParamList)" + type="text"> 删除 </el-button> </template> @@ -358,18 +392,28 @@ style="width: 100%; margin-top: 5px;"> <el-table-column prop="" - label="预测项" + label="预测项(NormalItem)" align="center"> <template #default="scope"> <el-select v-model="scope.row.point" + placeholder="请选择" filterable - placeholder="请选择"> - <el-option - v-for="(item, index) in predictItemList" - :key="index" - :label="item.itemname" - :value="item.itemno"/> + :no-data-text="'无数据(预测长度:' + dataForm.mmPredictItem.predictlength + ';管网:' + moduleList.find(e => e.id === dataForm.dmModuleItem.moduleid)?.modulename + ')'" + @change="changeNormalItemSelect" + style="width: 100%"> + <el-option-group + v-for="group in modelparamListMap['NormalItem'].filter(e => e.predictlength == dataForm.mmPredictItem.predictlength && e.moduleid === dataForm.dmModuleItem.moduleid)" + :key="group.value" + :label="group.label" + > + <el-option + v-for="item in group.children" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-option-group> </el-select> </template> </el-table-column> @@ -446,7 +490,6 @@ const pointNoList = ref([]) const pointList = ref([]) const pointMap = ref({}) -const predictItemList = ref([]) const modelparamListMap = ref({}) const modelparamMap = ref({}) const expressionList = ref([]) @@ -514,7 +557,8 @@ num: undefined }, mmModelArithSettingsList: [], - mmModelParamList: [] + mmModelParamList: [], + pointId: undefined }) const formRules = reactive({ 'mmPredictItem.itemname': [{required: true, message: '预测项名不能为空', trigger: 'blur'}], @@ -532,6 +576,8 @@ 'mmPredictItem.status': [{required: true, message: '是否启用不能为空', trigger: 'blur'}], 'dmModuleItem.moduleid': [{required: true, message: '管网不能为空', trigger: 'blur'}], 'dmModuleItem.itemorder': [{required: true, message: '排序不能为空', trigger: 'blur'}], + 'mmPredictItem.predictlength': [{required: true, message: '预测长度不能为空', trigger: 'blur'}], + 'mmPredictModel.mpkprojectid': [{required: true, message: '关联项目不能为空', trigger: 'blur'}], }) const formRef = ref() // 表单 Ref @@ -547,7 +593,7 @@ setDefaultFields() // 加载参数列表 - modelparamListMap.value = await ScheduleModelApi.getModelParamList() + modelparamListMap.value = await ScheduleModelApi.getModelParamList(id) // 获取预测项类型列表 itemTypeList.value = await MmItemType.getItemTypeList() @@ -563,11 +609,6 @@ // 获取mpk项目列表 mpkProjectList.value = await ProjectApi.list() - - // 获取normal列表 - predictItemList.value = await MmPredictItem.getMmPredictItemList({ - itemtypename: 'NormalItem' - }) // 获取数据点列表 pointNoList.value = await DaPoint.getPointList(queryParams) @@ -599,21 +640,40 @@ if (!formRef) return const valid = await formRef.value.validate() if (!valid) return - //校验模型输出 - if (dataForm.value.mmItemOutputList == undefined || dataForm.value.mmItemOutputList.length <= 0) { - message.error("模型输出不为空") - return + if (dataForm.value.itemtypename === 'NormalItem') { + if (dataForm.value.mmItemOutputList == undefined || dataForm.value.mmItemOutputList.length <= 0) { + message.error("模型输出不为空") + return + } + + let flag = false + dataForm.value.mmItemOutputList.forEach(e => { + if (e.resultstr == undefined || e.resultstr === '' || e.resultType == undefined || e.resultType === '' || (e.resultType === 2 && (e.resultIndex == undefined || e.resultIndex === ''))) { + message.error("模型输出数据异常") + flag = true + return + } + }) + if (flag) return + } + if (dataForm.value.itemtypename === 'MergeItem') { + if (expressionList.value == undefined || expressionList.value.length <= 1) { + message.error("表达式长度低于2") + return + } + + let flag = false + expressionList.value.forEach((e,index) => { + if (e.point == undefined || e.point === '' || ((e.operator == undefined || e.operator === '') && index != expressionList.value.length - 1)) { + message.error("表达式数据异常") + flag = true + return + } + }) + if (flag) return } - let flag = false - dataForm.value.mmItemOutputList.forEach(e => { - if (e.resultstr == undefined || e.resultstr === '' || e.resultType == undefined || e.resultType === '' || e.pointid == undefined || e.pointid === '' || (e.resultType === 2 && (e.resultIndex == undefined || e.resultIndex === ''))) { - message.error("模型输出数据异常") - flag = true - } - }) - if (flag) return // 提交请求 formLoading.value = true @@ -631,7 +691,7 @@ } if (dataForm.value.mmModelArithSettingsList) { for (let item of dataForm.value.mmModelArithSettingsList) { - if (item.key === 'lenpredict') { + if (item.key === 'predictLength') { dataForm.value.mmPredictItem.predictlength = item.value } } @@ -679,7 +739,7 @@ let endIndex = (indexSub == -1 || (indexPlus < indexSub && indexPlus !== -1)) ? indexPlus : indexSub expressionList.value.push({ point: expression.substring(0, endIndex), - operator: expression.substring(endIndex, 1) + operator: expression.substring(endIndex, endIndex + 1) }) expression = expression.substring(endIndex + 1) } else { @@ -719,7 +779,8 @@ dataForm.value.mmPredictModel.modelparamstructure = '' if (response.data.loadFieldSetList && response.data.loadFieldSetList[0].propertyList) { response.data.loadFieldSetList[0].propertyList.forEach(function (value) { - if (value.key !== 'data1') { + //匹配 data数字 + if (!/^data\d+$/.test(value.key)) { dataForm.value.mmModelArithSettingsList.push({ key: value.key, name: value.name, @@ -788,9 +849,9 @@ rows.splice(index, 0, row) } -function deleteRow(index: string, rows) { - if (!rows || rows.length === 1) { - message.error('不能全部删除!') +function deleteRow(index, row, rows) { + if (!rows || rows.length === 1 || rows.filter(e => e.modelparamportorder === row.modelparamportorder).length === 1) { + message.error('不可删除!') return } rows.splice(index, 1) @@ -803,9 +864,12 @@ orderRow(rows) } -function addItemOutput(list) { - list.push({}) - orderItemOutput(list) +function addItemOutput() { + if (!dataForm.value.mmItemOutputList) { + dataForm.value.mmItemOutputList = [] + } + dataForm.value.mmItemOutputList.push({}) + orderItemOutput(dataForm.value.mmItemOutputList) } function deleteItemOutput(index: string, rows) { @@ -866,6 +930,13 @@ fileList.value = [] } +const clearExpressionList = (value) => { + expressionList.value = [{ + point: '', + operator: '' + }] +} + /** 重置表单 */ const resetForm = () => { dataForm.value = { diff --git a/src/views/model/pre/item/index.vue b/src/views/model/pre/item/index.vue index 2158644..8bf49b6 100644 --- a/src/views/model/pre/item/index.vue +++ b/src/views/model/pre/item/index.vue @@ -7,6 +7,7 @@ ref="queryFormRef" :inline="true" label-width="68px" + @submit.prevent > <el-form-item label="编号" prop="itemno"> <el-input @@ -25,6 +26,42 @@ @keyup.enter="handleQuery" class="!w-240px" /> + </el-form-item> + <el-form-item label="类型" prop="itemtypeid"> + <el-select + v-model="queryParams.itemtypeid" + placeholder="请选择" + clearable + class="!w-240px"> + <el-option + v-for="item in itemTypeList" + :key="item.id" + :label="item.itemtypename" + :value="item.id"/> + </el-select> + </el-form-item> + <el-form-item label="管网名称" prop="modulename"> + <el-input + v-model="queryParams.modulename" + placeholder="请输入管网名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="运行状态" prop="runStatus"> + <el-select + v-model="queryParams.runStatus" + placeholder="请选择" + clearable + class="!w-240px"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.ITEM_RUN_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> </el-form-item> <el-form-item> <el-button @click="handleQuery"> @@ -65,16 +102,7 @@ <dict-tag :type="DICT_TYPE.PRED_GRANULARITY" :value="scope.row.granularity" /> </template> </el-table-column> - <el-table-column label="是否融合" align="center" prop="isfuse"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.COM_IS_INT" :value="scope.row.isfuse" /> - </template> - </el-table-column> - <el-table-column label="是否检查" align="center" prop="workchecked"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.COM_IS_INT" :value="scope.row.workchecked" /> - </template> - </el-table-column> + <el-table-column label="管网名称" align="center" prop="modulename" /> <el-table-column label="是否启用" align="center" prop="status"> <template #default="scope"> <dict-tag :type="DICT_TYPE.COM_IS_INT" :value="scope.row.status" /> @@ -83,7 +111,7 @@ <el-table-column label="运行时间" min-width="150" align="center" prop="lastTime"/> <el-table-column label="运行状态" align="center" prop="runStatus"> <template #default="scope"> - <dict-tag :type="DICT_TYPE.ITEM_RUN_STATUS" :value="scope.row.runStatus" /> + <dict-tag :type="DICT_TYPE.ITEM_RUN_STATUS" :value="scope.row.runStatus || 200" /> </template> </el-table-column> <el-table-column label="运行耗时(ms)" align="center" prop="duration"/> @@ -92,17 +120,15 @@ <el-button link type="primary" - size="mini" @click="openForm('update', scope.row.id, scope.row.itemtypename)" v-hasPermi="['model:pre-item:update']" > 编辑 </el-button> - <el-button link size="mini" type="primary" @click="chartHandle(scope.row)">数据</el-button> + <el-button link type="primary" @click="chartHandle(scope.row)">数据</el-button> <el-button link type="danger" - size="mini" @click="handleDelete(scope.row.id)" v-hasPermi="['model:pre-item:delete']" > @@ -130,14 +156,16 @@ <script lang="ts" setup> import MmPredictItemForm from './MmPredictItemForm.vue' import MmPredictItemChart from './MmPredictItemChart.vue' +import * as MmItemType from '@/api/model/pre/type' import * as MmPredictItem from '@/api/model/pre/item' -import {DICT_TYPE} from "@/utils/dict"; +import {DICT_TYPE, getIntDictOptions} from "@/utils/dict"; defineOptions({name: 'DataMmPredictItem'}) const message = useMessage() // 消息弹窗 const {t} = useI18n() // 国际化 +const itemTypeList = ref([]) const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 const list = ref([]) // 列表的数据 @@ -146,6 +174,8 @@ pageSize: 10, itemno: undefined, itemname: undefined, + itemtypeid: undefined, + modulename: undefined, }) const isList = ref([ { @@ -212,5 +242,7 @@ /** 初始化 **/ onMounted(async () => { await getList() + // 获取预测项类型列表 + itemTypeList.value = await MmItemType.getItemTypeList() }) </script> diff --git a/src/views/model/pre/type/index.vue b/src/views/model/pre/type/index.vue index 601d7c1..bc1eab1 100644 --- a/src/views/model/pre/type/index.vue +++ b/src/views/model/pre/type/index.vue @@ -7,6 +7,7 @@ ref="queryFormRef" :inline="true" label-width="68px" + @submit.prevent > <el-form-item label="名称" prop="itemtypename"> <el-input diff --git a/src/views/model/sche/model/ScheduleModelForm.vue b/src/views/model/sche/model/ScheduleModelForm.vue index 84b1f3d..397123b 100644 --- a/src/views/model/sche/model/ScheduleModelForm.vue +++ b/src/views/model/sche/model/ScheduleModelForm.vue @@ -7,15 +7,11 @@ :rules="formRules" label-width="120px" > + <el-divider content-position="left">基本信息</el-divider> <el-row> <el-col :span="12"> <el-form-item label="模型编号" prop="modelCode"> <el-input v-model="formData.modelCode" placeholder="请输入模型编号" /> - </el-form-item> - </el-col> - <el-col :span="12"> - <el-form-item label="模型名称" prop="modelName"> - <el-input v-model="formData.modelName" placeholder="请输入模型名称" /> </el-form-item> </el-col> </el-row> @@ -45,36 +41,54 @@ </el-form-item> </el-col> </el-row> + <el-divider content-position="left">模型信息</el-divider> + <div style="width: 120px;text-align: right;margin-bottom: 8px"> + <el-popover placement="right" :width="300" trigger="click" ref="modelPopover" @before-enter="model = undefined"> + <template #reference> + <span style="color: #409eff;cursor: pointer">关联模型信息</span> + </template> + <template #default> + <div style="display:flex;flex-direction: row;align-items: center;"> + <el-cascader style="width: 100%" v-model="model" placeholder="选择模型" :teleported="false" @change="changeModel" :options="scheduleModelList"/> + </div> + </template> + </el-popover> + </div> <el-row> - <el-col :span="24"> + <el-col :span="12"> + <el-form-item label="模型名称" prop="modelName"> + <el-input v-model="formData.modelName" placeholder="请输入模型名称"/> + </el-form-item> + </el-col> + <el-col :span="12"> <el-form-item label="类名" prop="className"> - <el-input v-model="formData.className" placeholder="请输入类名 " /> + <el-input v-model="formData.className" placeholder="请输入类名" :disabled="true" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="方法名" prop="methodName"> - <el-input v-model="formData.methodName" placeholder="请输入方法名 " /> + <el-input v-model="formData.methodName" placeholder="请输入方法名" :disabled="true" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="参数数量" prop="portLength"> - <el-input-number v-model="formData.portLength" :min="0" controls-position="right" /> + <el-input-number v-model="formData.portLength" :min="0" controls-position="right" :disabled="true" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="参数构造" prop="paramStructure"> - <el-input v-model="formData.paramStructure" placeholder="请输入参数构造 " /> + <el-input v-model="formData.paramStructure" placeholder="请输入参数构造" :disabled="true" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="模型路径" prop="modelPath"> - <el-input v-model="formData.modelPath" placeholder="模型路径" /> + <el-input v-model="formData.modelPath" placeholder="模型路径" :disabled="true" /> </el-form-item> </el-col> </el-row> @@ -89,7 +103,7 @@ width="100" align="center"> <template #default="scope"> - <el-input size="mini" v-model="scope.row.modelparamportorder" maxlength="5" clearable + <el-input v-model="scope.row.modelparamportorder" maxlength="5" clearable :disabled="true" style="width:100%; hight:100%"/> </template> </el-table-column> @@ -99,7 +113,7 @@ width="100" align="center"> <template #default="scope"> - <el-input size="mini" v-model="scope.row.modelparamorder" maxlength="5" clearable + <el-input v-model="scope.row.modelparamorder" maxlength="5" clearable style="width:100%;hight:100%"/> </template> </el-table-column> @@ -109,7 +123,7 @@ width="150" align="center"> <template #default="scope"> - <el-select v-model="scope.row.modelparamtype" placeholder="请选择"> + <el-select v-model="scope.row.modelparamtype" placeholder="请选择" @change="changeModelparamtype(scope.row)"> <el-option v-for="dict in getStrDictOptions(DICT_TYPE.MODEL_PARAM_TYPE)" :key="dict.value" @@ -124,8 +138,25 @@ label="参数名称" align="center"> <template #default="scope"> - <el-select - size="mini" + <el-select v-if="scope.row.modelparamtype === 'NormalItem'" + v-model="scope.row.modelparamid" + placeholder="请选择" + filterable + style="width: 100%"> + <el-option-group + v-for="group in modelparamListMap['NormalItem']" + :key="group.value" + :label="group.label" + > + <el-option + v-for="item in group.children" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-option-group> + </el-select> + <el-select v-else v-model="scope.row.modelparamid" filterable placeholder="请选择"> @@ -162,7 +193,7 @@ </el-button> <el-button link - @click.prevent="deleteRow(scope.$index, formData.paramList)" + @click.prevent="deleteRow(scope.$index, scope.row, formData.paramList)" type="primary" size="small"> 删除 @@ -179,27 +210,30 @@ <el-table-column prop="" label="键" + min-width="150" align="center"> <template #default="scope"> - <el-input size="mini" v-model="scope.row.key" maxlength="20" clearable + <el-input v-model="scope.row.key" maxlength="20" clearable :disabled="true" style="width:100%;hight:100%"/> </template> </el-table-column> <el-table-column prop="" label="名称" + min-width="150" align="center"> <template #default="scope"> - <el-input size="mini" v-model="scope.row.name" maxlength="20" clearable + <el-input v-model="scope.row.name" maxlength="20" clearable :disabled="true" style="width:100%;hight:100%"/> </template> </el-table-column> <el-table-column prop="" label="类型" + min-width="100" align="center"> <template #default="scope"> - <el-select v-model="scope.row.valuetype" placeholder="请选择"> + <el-select v-model="scope.row.valuetype" placeholder="请选择" :disabled="true"> <el-option v-for="dict in getStrDictOptions(DICT_TYPE.MODEL_METHOD_SETTING_VALUE_TYPE)" :key="dict.value" @@ -212,31 +246,96 @@ <el-table-column prop="" label="值" + min-width="300" align="center"> <template #default="scope"> - <el-input size="mini" v-model="scope.row.value" maxlength="256" clearable + <el-input v-model="scope.row.value" maxlength="256" clearable + :disabled="scope.row.key === 'pyFile_BAK'" style="width:100%;hight:100%"/> + </template> + </el-table-column> + </el-table> + <el-divider content-position="left">模型下发配置</el-divider> + <el-row :gutter="20"> + <el-col :span="4"> + <el-button type="primary" size="small" @click="addRowOut()" >新增</el-button> + </el-col> + </el-row> + <el-table + :data="formData.modelOut" + border + style="width: 100%; margin-top: 5px;"> + <el-table-column prop="resultKey" label="输出key" align="center" min-width="100"> + <template #default="scope"> + <el-input size="mini" v-model="scope.row.resultKey" style="width:100%;height:100%"/> + </template> + </el-table-column> + <el-table-column prop="resultType" label="数据类型" align="center" min-width="150"> + <template #default="scope"> + <el-select v-model="scope.row.resultType" placeholder="请选择"> + <el-option + v-for="dict in getStrDictOptions(DICT_TYPE.RESULT_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </template> + </el-table-column> + <el-table-column prop="resultPort" label="角标1" align="center" min-width="100"> + <template #default="scope"> + <el-input-number :min="0" clearable controls-position="right" size="mini" v-model="scope.row.resultPort" style="width:100%;height:100%"/> + </template> + </el-table-column> + <el-table-column prop="resultIndex" label="角标2" align="center" min-width="100"> + <template #default="scope"> + <el-input-number :min="0" clearable controls-position="right" size="mini" v-model="scope.row.resultIndex" style="width:100%;height:100%"/> + </template> + </el-table-column> + <el-table-column prop="isWrite" label="是否下发" align="center" min-width="100"> + <template #default="scope"> + <el-switch size="small" v-model="scope.row.isWrite" :active-value="1" + :inactive-value="0"/> </template> </el-table-column> <el-table-column prop="" - label="操作" - width="100" - align="center"> + label="测点名称" + align="center" min-width="200"> + <template #default="scope"> + <el-select v-model="scope.row.pointNo" + filterable + placeholder="请选择"> + <el-option + v-for="(item, index) in modelparamListMap['DATAPOINT']" + :key="index" + :label="item.name" + :value="item.itemNo"/> + </el-select> + </template> + </el-table-column> + <el-table-column prop="disturbancePointNo’" label="无扰切换点位" align="center" min-width="200"> + <template #default="scope"> + <el-select v-model="scope.row.disturbancePointNo" + clearable + filterable + placeholder="请选择"> + <el-option + v-for="(item, index) in modelparamListMap['DATAPOINT']" + :key="index" + :label="item.name" + :value="item.itemNo"/> + </el-select> + </template> + </el-table-column> + <el-table-column label="操作" fixed="right" header-align="center" align="center" width="100"> <template #default="scope"> <el-button - @click.prevent="addRow(scope.$index, formData.settingList)" + @click="deleteModelOutRow(scope.$index)" + key="danger" + type="danger" link - type="primary" - size="small"> - 添加 - </el-button> - <el-button - @click.prevent="deleteRow(scope.$index, formData.settingList)" - link - type="primary" - size="small"> - 删除 + >删除 </el-button> </template> </el-table-column> @@ -252,6 +351,8 @@ import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' import * as ScheduleModelApi from '@/api/model/sche/model' import { CommonStatusEnum } from '@/utils/constants' + import * as MpkApi from "@/api/model/mpk/mpk"; + import {generateUUID} from "@/utils"; defineOptions({ name: 'ScheduleModelForm' }) @@ -274,19 +375,9 @@ resultStrId: undefined, invocation: undefined, status: CommonStatusEnum.ENABLE, - paramList: [{ - modelparamportorder: '1', - modelparamorder: '1', - modelparamtype: '', - modelparamid: '', - datalength: '' - }], - settingList: [{ - key: '', - value: '', - valuetype: '', - name: '' - }] + paramList: [], + settingList: [], + modelOut: [] }) const formRules = reactive({ modelCode: [{ required: true, message: '模型编号不能为空', trigger: 'blur' }], @@ -295,6 +386,10 @@ }) const formRef = ref() // 表单 Ref const modelparamListMap = ref({}) + // 调度模型列表 + const scheduleModelList = ref([]) + const model = ref() + const modelPopover = ref() const addRow = function (index, rows) { let row = JSON.parse(JSON.stringify(rows[index])) @@ -302,9 +397,9 @@ this.orderRow(rows) } - const deleteRow = function (index, rows) { - if (!rows || rows.length === 1) { - message.error('不能全部删除!') + const deleteRow = function (index, row, rows) { + if (!rows || rows.length === 1 || rows.filter(e => e.modelparamportorder === row.modelparamportorder).length === 1) { + message.error('不可删除!') return } rows.splice(index, 1) @@ -341,6 +436,8 @@ } // 加载参数列表 modelparamListMap.value = await ScheduleModelApi.getModelParamList() + // 加载调度模型列表 + getScheduleModelList() } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 @@ -385,20 +482,102 @@ resultStrId: undefined, invocation: undefined, status: CommonStatusEnum.ENABLE, - paramList: [{ - modelparamportorder: '1', - modelparamorder: '1', - modelparamtype: '', - modelparamid: '', - datalength: '' - }], - settingList: [{ - key: '', - value: '', - valuetype: '', - name: '' - }] + paramList: [], + settingList: [], + modelOut: [] } formRef.value?.resetFields() } + + const getScheduleModelList = async () => { + let list = await MpkApi.list({pyType: 'schedul'}) + if (list && list.length > 0) { + scheduleModelList.value = list.map(e => { + return { + label: e.pyChineseName, + value: e, + children: e.modelMethods.map(m => { + return { + label: m.methodName, + value: m + } + }) + } + }) + } + } + + // 选择调度模型 + const changeModel = async () => { + // 校验 + if (model.value && model.value.length > 0) { + const modelInfo = model.value[0] + const methodInfo = model.value[1] + formData.value.modelName = modelInfo.pyChineseName + formData.value.className = modelInfo.pkgName + '.impl.' + modelInfo.pyName + 'Impl'; + formData.value.methodName = methodInfo.methodName + formData.value.portLength = methodInfo.dataLength + // 参数构造 + let paramStructure = [] + for (let i = 0; i < methodInfo.dataLength; i++) { + paramStructure.push('[[D') + } + if (methodInfo.model === 1) { + paramStructure.push('java.util.HashMap') + } + paramStructure.push('java.util.HashMap') + formData.value.paramStructure = paramStructure.join(',') + formData.value.modelPath = modelInfo.pyModule + // 输入参数 + let paramList = [] + for (let i = 0; i < methodInfo.dataLength; i++) { + paramList.push({ + modelparamportorder: i+1 + '', + modelparamorder: '1', + modelparamtype: '', + modelparamid: '', + datalength: 0 + }) + } + + formData.value.paramList = paramList + // 设置参数 + let settingList = [] + methodInfo.methodSettings.forEach(e => { + settingList.push({ + key: e.settingKey, + value: e.value, + valuetype: e.valueType, + name: e.name + }) + }) + formData.value.settingList = settingList + modelPopover.value.hide() + }else { + message.error("请先选择模型") + } + } + + function changeModelparamtype(row) { + row.modelparamid = '' + } + const addRowOut= function () { + if(formData.value.modelOut===undefined) { + formData.value.modelOut = [] + } + formData.value.modelOut.push({ + id: generateUUID(), + resultKey: undefined, + resultType: "double[][]", + port: 0, + index: 0, + isWrite: 1, + pointNo:undefined, + sort:undefined, + disturbancePointNo:undefined, + }) + } + const deleteModelOutRow = function (index) { + formData.value.modelOut.splice(index, 1) + } </script> diff --git a/src/views/model/sche/model/index.vue b/src/views/model/sche/model/index.vue index 87b3cd1..ae84a63 100644 --- a/src/views/model/sche/model/index.vue +++ b/src/views/model/sche/model/index.vue @@ -52,8 +52,12 @@ <ContentWrap> <el-table v-loading="loading" :data="list"> <el-table-column label="模型编号" align="center" prop="modelCode" min-width="100"/> - <el-table-column label="模型名称" align="center" prop="modelName" min-width="100"/> - <el-table-column label="模型类型" align="center" prop="modelType" min-width="100"/> + <el-table-column label="模型名称" header-align="center" align="left" prop="modelName" min-width="100"/> + <el-table-column label="模型类型" align="center" prop="modelType" min-width="100"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.SCHE_MODEL_TYPE" :value="scope.row.modelType" /> + </template> + </el-table-column> <el-table-column label="类名" header-align="center" align="left" prop="className" min-width="200"/> <el-table-column label="方法名" align="center" prop="methodName" min-width="100"/> <el-table-column label="参数数量" align="center" prop="portLength" min-width="100"/> diff --git a/src/views/model/sche/scheme/ScheduleSchemeForm.vue b/src/views/model/sche/scheme/ScheduleSchemeForm.vue index f7c0b44..9ee1e84 100644 --- a/src/views/model/sche/scheme/ScheduleSchemeForm.vue +++ b/src/views/model/sche/scheme/ScheduleSchemeForm.vue @@ -10,12 +10,12 @@ <el-row> <el-col :span="12"> <el-form-item label="方案编号" prop="code"> - <el-input v-model="formData.code" placeholder="请输入方案编号" /> + <el-input v-model="formData.code" placeholder="请输入方案编号"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="方案名称" prop="name"> - <el-input v-model="formData.name" placeholder="请输入方案名称" /> + <el-input v-model="formData.name" placeholder="请输入方案名称"/> </el-form-item> </el-col> </el-row> @@ -24,7 +24,7 @@ <el-form-item label="触发方式" prop="triggerMethod"> <el-select v-model="formData.triggerMethod" placeholder="请选择"> <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.SCHE_TRIGGER_METHOD)" + v-for="dict in getDictOptions(DICT_TYPE.SCHE_TRIGGER_METHOD)" :key="dict.value" :label="dict.label" :value="dict.value" @@ -34,26 +34,26 @@ </el-col> <el-col :span="12"> <el-form-item label="触发条件" prop="triggerCondition"> - <el-input v-model="formData.triggerCondition" placeholder="请输入触发条件" /> + <el-input v-model="formData.triggerCondition" placeholder="请输入触发条件"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="调整对象" prop="scheduleObj"> - <el-input v-model="formData.scheduleObj" placeholder="请输入调整对象" /> + <el-input v-model="formData.scheduleObj" placeholder="请输入调整对象"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="调整类型" prop="scheduleType"> - <el-input v-model="formData.scheduleType" placeholder="请输入调整类型" /> + <el-input v-model="formData.scheduleType" placeholder="请输入调整类型"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="调整策略" prop="scheduleStrategy"> - <el-input v-model="formData.scheduleStrategy" placeholder="请输入调整策略 " /> + <el-input v-model="formData.scheduleStrategy" placeholder="请输入调整策略 "/> </el-form-item> </el-col> <el-col :span="12"> @@ -71,9 +71,23 @@ </el-col> </el-row> <el-row> + <el-col :span="12"> + <el-form-item label="关联项目" prop="mpkprojectid"> + <el-select v-model="formData.mpkprojectid" placeholder="请选择"> + <el-option + v-for="item in mpkProjectList" + :key="item.id" + :label="item.projectName" + :value="item.id"/> + </el-select> + </el-form-item> + </el-col> + </el-row> + <el-row> <el-col :span="24"> <el-form-item label="备注" prop="remark"> - <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" maxlength="100" + <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" + maxlength="100" show-word-limit/> </el-form-item> </el-col> @@ -86,20 +100,127 @@ </Dialog> </template> <script lang="ts" setup> - import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' - import * as ScheduleSchemeApi from '@/api/model/sche/scheme' - import { CommonStatusEnum } from '@/utils/constants' - import * as ScheduleModelApi from "@/api/model/sche/model"; +import {DICT_TYPE, getDictOptions} from '@/utils/dict' +import * as ScheduleSchemeApi from '@/api/model/sche/scheme' +import {CommonStatusEnum} from '@/utils/constants' +import * as ScheduleModelApi from "@/api/model/sche/model"; +import * as ProjectApi from '@/api/model/mpk/project' - defineOptions({ name: 'ScheduleSchemeForm' }) +defineOptions({name: 'ScheduleSchemeForm'}) - 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({ +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, + code: undefined, + name: undefined, + triggerMethod: undefined, + triggerCondition: undefined, + scheduleObj: undefined, + scheduleType: undefined, + scheduleStrategy: undefined, + modelId: undefined, + scheduleTime: undefined, + remark: undefined, + status: 0, + mpkprojectid: undefined, +}) +const formRules = reactive({ + code: [{required: true, message: '编号不能为空', trigger: 'blur'}], + name: [{required: true, message: '名称不能为空', trigger: 'blur'}], + triggerMethod: [{required: true, message: '触发方式不能为空', trigger: 'blur'}], + triggerCondition: [{required: true, message: '触发条件不能为空', trigger: 'blur'}], + modelId: [{required: true, message: '调度模型不能为空', trigger: 'blur'}], + triggerCondition: [{required: true, message: '触发条件不能为空', trigger: 'blur'}], + mpkprojectid: [{required: true, message: '关联项目不能为空', trigger: 'blur'}], +}) +const formRef = ref() // 表单 Ref +const scheduleModelList = ref([] as ScheduleModelApi.ScheduleModelVO[]) +const mpkProjectList = ref([]) +const addRow = function (index, rows) { + let row = JSON.parse(JSON.stringify(rows[index])) + rows.splice(index, 0, row) + this.orderRow(rows) +} + +const deleteRow = function (index, rows) { + if (!rows || rows.length === 1) { + message.error('不能全部删除!') + return + } + rows.splice(index, 1) + this.orderRow(rows) +} + +const orderRow = function (rows) { + let modelparamorder = 0 + let modelparamportorder = 0 + rows.forEach(function (value) { + if (value.modelparamportorder !== modelparamportorder) { + modelparamportorder = value.modelparamportorder + modelparamorder = 1 + } + value.modelparamorder = modelparamorder + modelparamorder++ + }) +} + +/** 打开弹窗 */ +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 ScheduleSchemeApi.getScheduleScheme(id) + } finally { + formLoading.value = false + } + } + // 加载调度模型列表 + scheduleModelList.value = await ScheduleModelApi.getScheduleModelList() + + // 获取mpk项目列表 + mpkProjectList.value = await ProjectApi.list() +} +defineExpose({open}) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as ScheduleSchemeApi.ScheduleSchemeVO + if (formType.value === 'create') { + await ScheduleSchemeApi.createScheduleScheme(data) + message.success(t('common.createSuccess')) + } else { + await ScheduleSchemeApi.updateScheduleScheme(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { id: undefined, code: undefined, name: undefined, @@ -111,105 +232,9 @@ modelId: undefined, scheduleTime: undefined, remark: undefined, - status: 0 - }) - const formRules = reactive({ - code: [{ required: true, message: '编号不能为空', trigger: 'blur' }], - name: [{ required: true, message: '名称不能为空', trigger: 'blur' }] - }) - const formRef = ref() // 表单 Ref - const scheduleModelList = ref([] as ScheduleModelApi.ScheduleModelVO[]) - - const addRow = function (index, rows) { - let row = JSON.parse(JSON.stringify(rows[index])) - rows.splice(index, 0, row) - this.orderRow(rows) + status: CommonStatusEnum.ENABLE, + mpkprojectid: undefined } - - const deleteRow = function (index, rows) { - if (!rows || rows.length === 1) { - message.error('不能全部删除!') - return - } - rows.splice(index, 1) - this.orderRow(rows) - } - - const orderRow = function (rows) { - let modelparamorder = 0 - let modelparamportorder = 0 - rows.forEach(function (value) { - if (value.modelparamportorder !== modelparamportorder) { - modelparamportorder = value.modelparamportorder - modelparamorder = 1 - } - value.modelparamorder = modelparamorder - modelparamorder++ - }) - } - - /** 打开弹窗 */ - 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 ScheduleSchemeApi.getScheduleScheme(id) - } finally { - formLoading.value = false - } - } - // 加载调度模型列表 - scheduleModelList.value = await ScheduleModelApi.getScheduleModelList() - } - defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - - /** 提交表单 */ - const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 - const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - const data = formData.value as unknown as ScheduleSchemeApi.ScheduleSchemeVO - if (formType.value === 'create') { - await ScheduleSchemeApi.createScheduleScheme(data) - message.success(t('common.createSuccess')) - } else { - await ScheduleSchemeApi.updateScheduleScheme(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } - } - - /** 重置表单 */ - const resetForm = () => { - formData.value = { - id: undefined, - code: undefined, - name: undefined, - triggerMethod: undefined, - triggerCondition: undefined, - scheduleObj: undefined, - scheduleType: undefined, - scheduleStrategy: undefined, - modelId: undefined, - scheduleTime: undefined, - remark: undefined, - status: CommonStatusEnum.ENABLE - } - formRef.value?.resetFields() - } + formRef.value?.resetFields() +} </script> diff --git a/src/views/model/sche/scheme/index.vue b/src/views/model/sche/scheme/index.vue index 6212a90..1c94533 100644 --- a/src/views/model/sche/scheme/index.vue +++ b/src/views/model/sche/scheme/index.vue @@ -36,6 +36,20 @@ 重置 </el-button> <el-button + type="success" + plain + @click="enable" + v-hasPermi="['sche:scheme:update']" + >启用 + </el-button> + <el-button + type="danger" + plain + @click="disable" + v-hasPermi="['sche:scheme:update']" + >禁用 + </el-button> + <el-button type="primary" plain @click="openForm('create')" @@ -50,9 +64,10 @@ <!-- 列表 --> <ContentWrap> - <el-table v-loading="loading" :data="list"> + <el-table v-loading="loading" :data="list" @selection-change="selectionChangeHandle"> + <el-table-column type="selection" header-align="center" align="center" fixed="left" width="50"/> <el-table-column label="方案编号" align="center" prop="code" min-width="100"/> - <el-table-column label="方案名称" align="center" prop="name" min-width="100"/> + <el-table-column label="方案名称" header-align="center" align="left" prop="name" min-width="100"/> <el-table-column label="触发方式" align="center" prop="triggerMethod" min-width="100"> <template #default="scope"> <dict-tag :type="DICT_TYPE.SCHE_TRIGGER_METHOD" :value="scope.row.triggerMethod" /> @@ -63,13 +78,19 @@ <el-table-column label="调整类型" align="center" prop="scheduleType" min-width="100"/> <el-table-column label=" 调整策略" align="center" prop="scheduleStrategy" min-width="100"/> <el-table-column label="调度时间" align="center" prop="scheduleTime" min-width="160" /> - <el-table-column label="备注" align="center" prop="remark" min-width="100" /> - <el-table-column label="状态" align="center" prop="status" min-width="100"> + <el-table-column label="运行状态" align="center" prop="runStatus"> + <template #default="scope"> + <el-tag v-if="scope.row.runStatus + '' === '100'" size="small" type="success">{{scope.row.runStatus}}</el-tag> + <el-tag v-else size="small" type="danger">{{scope.row.runStatus}}</el-tag> + </template> + </el-table-column> + <el-table-column label="备注" header-align="center" align="left" prop="remark" min-width="160" /> + <el-table-column label="是否启用" align="center" prop="status" min-width="100"> <template #default="scope"> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> </template> </el-table-column> - <el-table-column label="操作" align="center" min-width="110" fixed="right"> + <el-table-column label="操作" align="center" min-width="100" fixed="right"> <template #default="scope"> <el-button link @@ -78,6 +99,14 @@ v-hasPermi="['sche:scheme:update']" > 编辑 + </el-button> + <el-button + link + type="primary" + @click="openRecordList(scope.row.id)" + v-hasPermi="['sche:record:query']" + > + 日志 </el-button> <el-button link @@ -102,13 +131,17 @@ <!-- 表单弹窗:添加/修改 --> <ScheduleSchemeForm ref="formRef" @success="getList" /> + <!-- 表单弹窗:添加/修改 --> + <RecordList ref="recordRef" /> </template> <script lang="ts" setup> import {DICT_TYPE, getIntDictOptions} from '@/utils/dict' - import {dateFormatter} from '@/utils/formatTime' - import download from '@/utils/download' import * as ScheduleSchemeApi from '@/api/model/sche/scheme' import ScheduleSchemeForm from './ScheduleSchemeForm.vue' + import RecordList from './record/index.vue' + import * as DaPoint from "@/api/data/da/point"; + import {reactive} from "vue"; + import {InfraJobStatusEnum} from "@/utils/constants"; defineOptions({name: 'ScheduleScheme'}) @@ -171,6 +204,40 @@ } } + /** 调用日志查看 */ + const recordRef = ref() + const openRecordList = (id?: string) => { + recordRef.value.open(id) + } + + let dataListSelections = reactive([]) + // 多选 + function selectionChangeHandle (val) { + dataListSelections = val + } + // 启用 + async function enable() { + let ids = dataListSelections.map(item => { + return item.id + }) + // 二次确认 + await message.confirm('是否确认要启用所选调度方案?') + await ScheduleSchemeApi.enable(ids) + message.success(t('common.enableSuccess')) + await getList() + } + // 禁用 + async function disable(){ + let ids = dataListSelections.map(item => { + return item.id + }) + // 二次确认 + await message.confirm('确认要禁用所选调度方案?') + await ScheduleSchemeApi.disable(ids) + message.success(t('common.disableSuccess')) + await getList() + } + /** 初始化 **/ onMounted(async () => { await getList() diff --git a/src/views/model/sche/scheme/record/index.vue b/src/views/model/sche/scheme/record/index.vue new file mode 100644 index 0000000..4d74cda --- /dev/null +++ b/src/views/model/sche/scheme/record/index.vue @@ -0,0 +1,153 @@ +<template> + <el-drawer + v-model="drawer" + size="50%" + title="调度日志" + :direction="direction" + :before-close="handleClose" + > + <!-- 搜索 --> + <ContentWrap> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item> + <el-date-picker + v-model="queryParams.startTime" + format="YYYY-MM-DD HH:mm:00" + value-format="YYYY-MM-DD HH:mm:00" + type="datetime" + placeholder="选择日期时间" + /> + </el-form-item> + <el-form-item> + <el-date-picker + v-model="queryParams.endTime" + format="YYYY-MM-DD HH:mm:00" + value-format="YYYY-MM-DD HH:mm:00" + type="datetime" + placeholder="选择日期时间" + /> + </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-form-item> + </el-form> + </ContentWrap> + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list"> + <el-table-column + prop="scheduleTime" + label="调度时间" + header-align="center" + align="left" + min-width="150" + /> + <el-table-column + prop="resultCode" + label="结果状态" + header-align="center" + align="center" + /> + <el-table-column + prop="resultData" + label="结果数据" + header-align="center" + align="center" + min-width="400" + /> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + </el-drawer> +</template> +<script lang="ts" setup> +import type { DrawerProps } from 'element-plus' +import * as ScheduleRecordApi from "@/api/model/sche/record"; +import {reactive, ref} from "vue"; + +defineOptions({name: 'RecordList'}) + +const message = useMessage() // 消息弹窗 +const {t} = useI18n() // 国际化 + +const drawer = ref(false) +const direction = ref<DrawerProps['direction']>('rtl') +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + schemeId: undefined, + startTime: undefined, + endTime: undefined, +}) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const page = await ScheduleRecordApi.getScheduleRecordPage(queryParams) + list.value = page.list + total.value = page.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 打开弹窗 */ +const open = async (id?: string) => { + resetForm() + drawer.value = true + queryParams.schemeId = id + if (id) { + getList() + } +} +defineExpose({open}) // 提供 open 方法,用于打开弹窗 + +/** 重置表单 */ +const resetForm = () => { + queryParams.pageNo = 1 + queryParams.pageSize = 10 + queryParams.schemeId = '' + queryParams.startTime = undefined + queryParams.endTime = undefined +} +const handleClose = (done: () => void) => { + drawer.value = false +} +</script> diff --git a/src/views/report/drag/index.vue b/src/views/report/drag/index.vue new file mode 100644 index 0000000..82c1117 --- /dev/null +++ b/src/views/report/drag/index.vue @@ -0,0 +1,13 @@ +<template> + <ContentWrap> + <IFrame :src="src" /> + </ContentWrap> +</template> +<script lang="ts" setup> +import {getAccessToken, getTenantId} from '@/utils/auth' + +defineOptions({ name: 'Drag' }) + +const BASE_URL = import.meta.env.VITE_BASE_URL +const src = ref(BASE_URL + '/drag/list?token=' + getAccessToken() + "&tenantId=" + getTenantId()) +</script> diff --git a/src/views/report/goview/index.vue b/src/views/report/goview/index.vue index 1bac286..8da8e2f 100644 --- a/src/views/report/goview/index.vue +++ b/src/views/report/goview/index.vue @@ -4,7 +4,11 @@ </ContentWrap> </template> <script lang="ts" setup> +import {getAccessToken, getTenantId} from "@/utils/auth"; + defineOptions({ name: 'GoView' }) -const src = 'http://127.0.0.1:3000' +const BASE_URL = 'http://172.16.8.100' +const src = ref(BASE_URL + '/bigscreen/project/items?token=' + getAccessToken() + "&tenantId=" + getTenantId()) + </script> diff --git a/src/views/report/jmreport/index.vue b/src/views/report/jmreport/index.vue index 0bac351..55338e7 100644 --- a/src/views/report/jmreport/index.vue +++ b/src/views/report/jmreport/index.vue @@ -4,10 +4,10 @@ </ContentWrap> </template> <script lang="ts" setup> -import { getAccessToken } from '@/utils/auth' +import {getAccessToken, getTenantId} from '@/utils/auth' defineOptions({ name: 'JimuReport' }) const BASE_URL = import.meta.env.VITE_BASE_URL -const src = ref(BASE_URL + '/jmreport/list?token=' + getAccessToken()) +const src = ref(BASE_URL + '/jmreport/list?token=' + getAccessToken() + "&tenantId=" + getTenantId()) </script> diff --git a/src/views/system/app/AppForm.vue b/src/views/system/app/AppForm.vue index 4dd599f..151620e 100644 --- a/src/views/system/app/AppForm.vue +++ b/src/views/system/app/AppForm.vue @@ -51,24 +51,24 @@ <el-input v-model="formData.appDomain" placeholder="请输入应用域名" /> </el-form-item> </el-col> - <el-col :span="12"> - <el-form-item label="接口域名" prop="apiDomain"> - <el-input v-model="formData.apiDomain" placeholder="请输入接口域名" /> - </el-form-item> - </el-col> +<!-- <el-col :span="12">--> +<!-- <el-form-item label="接口域名" prop="apiDomain">--> +<!-- <el-input v-model="formData.apiDomain" placeholder="请输入接口域名" />--> +<!-- </el-form-item>--> +<!-- </el-col>--> </el-row> - <el-row> - <el-col :span="12"> - <el-form-item label="应用账号" prop="appKey"> - <el-input v-model="formData.appKey" placeholder="请输入应用账号" /> - </el-form-item> - </el-col> - <el-col :span="12"> - <el-form-item label="应用密码" prop="appSecret"> - <el-input v-model="formData.appSecret" placeholder="请输入应用密码" /> - </el-form-item> - </el-col> - </el-row> +<!-- <el-row>--> +<!-- <el-col :span="12">--> +<!-- <el-form-item label="应用账号" prop="appKey">--> +<!-- <el-input v-model="formData.appKey" placeholder="请输入应用账号" />--> +<!-- </el-form-item>--> +<!-- </el-col>--> +<!-- <el-col :span="12">--> +<!-- <el-form-item label="应用密码" prop="appSecret">--> +<!-- <el-input v-model="formData.appSecret" placeholder="请输入应用密码" />--> +<!-- </el-form-item>--> +<!-- </el-col>--> +<!-- </el-row>--> <el-row> <el-col :span="24"> <el-form-item label="应用图标" prop="icon"> diff --git a/src/views/system/app/index.vue b/src/views/system/app/index.vue index 8e67647..9b29a84 100644 --- a/src/views/system/app/index.vue +++ b/src/views/system/app/index.vue @@ -93,8 +93,8 @@ <el-table-column label="应用编号" align="center" prop="appCode" /> <el-table-column label="应用名称" align="center" prop="appName" /> <el-table-column label="应用域名" align="center" prop="appDomain" /> - <el-table-column label="接口域名" align="center" prop="apiDomain" /> - <el-table-column label="应用账号" align="center" prop="appKey" /> +<!-- <el-table-column label="接口域名" align="center" prop="apiDomain" />--> +<!-- <el-table-column label="应用账号" align="center" prop="appKey" />--> <el-table-column label="应用图标" align="center" prop="logo"> <template #default="scope"> <img width="40px" height="40px" :src="scope.row.icon" /> diff --git a/src/views/system/appmenu/AppMenuForm.vue b/src/views/system/appmenu/AppMenuForm.vue index 3a7ad72..f8545cf 100644 --- a/src/views/system/appmenu/AppMenuForm.vue +++ b/src/views/system/appmenu/AppMenuForm.vue @@ -115,13 +115,13 @@ <script lang="ts" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import * as MenuApi from '@/api/system/menu' -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import {CACHE_KEY, useCache, useSessionCache} from '@/hooks/web/useCache' import { CommonStatusEnum, SystemAppMenuTypeEnum } from '@/utils/constants' import { defaultProps, handleTree } from '@/utils/tree' defineOptions({ name: 'SystemAppMenuForm' }) -const { wsCache } = useCache() +const { wsSessionCache } = useSessionCache() const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -214,7 +214,7 @@ } finally { formLoading.value = false // 清空,从而触发刷新 - wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) } } diff --git a/src/views/system/appmenu/index.vue b/src/views/system/appmenu/index.vue index 66b4f35..ea3ef37 100644 --- a/src/views/system/appmenu/index.vue +++ b/src/views/system/appmenu/index.vue @@ -116,11 +116,12 @@ import { handleTree } from '@/utils/tree' import * as MenuApi from '@/api/system/menu' import AppMenuForm from './AppMenuForm.vue' -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import {CACHE_KEY, useCache, useSessionCache} from '@/hooks/web/useCache' defineOptions({ name: 'SystemAppMenu' }) const { wsCache } = useCache() +const { wsSessionCache } = useSessionCache() const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -190,7 +191,8 @@ await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存') // 清空,从而触发刷新 wsCache.delete(CACHE_KEY.USER) - wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + // wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) // 刷新浏览器 location.reload() } catch {} diff --git a/src/views/system/area/index.vue b/src/views/system/area/index.vue index f3289a0..339ecef 100644 --- a/src/views/system/area/index.vue +++ b/src/views/system/area/index.vue @@ -1,4 +1,6 @@ <template> + <doc-alert title="地区 & IP" url="https://doc.iocoder.cn/area-and-ip/" /> + <!-- 操作栏 --> <ContentWrap> <el-button type="primary" plain @click="openForm()"> @@ -14,6 +16,7 @@ <template #default="{ height, width }"> <!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 --> <el-table-v2 + v-loading="loading" :columns="columns" :data="list" :width="width" @@ -29,7 +32,7 @@ <AreaForm ref="formRef" /> </template> <script setup lang="tsx"> -import type { Column } from 'element-plus' +import { Column } from 'element-plus' import AreaForm from './AreaForm.vue' import * as AreaApi from '@/api/system/area' @@ -38,7 +41,7 @@ // 表格的 column 字段 const columns: Column[] = [ { - dataKey: 'id', // 需要渲染当前列的数据字段。例如说:{id:9527, name:'Mike'},则填 id + dataKey: 'id', // 需要渲染当前列的数据字段 title: '编号', // 显示在单元格表头的文本 width: 400, // 当前列的宽度,必须设置 fixed: true, // 是否固定列 @@ -50,14 +53,17 @@ width: 200 } ] -// 表格的数据 -const list = ref([]) +const loading = ref(true) // 列表的加载中 +const list = ref([]) // 表格的数据 -/** - * 获得数据列表 - */ +/** 获得数据列表 */ const getList = async () => { - list.value = await AreaApi.getAreaTree() + loading.value = true + try { + list.value = await AreaApi.getAreaTree() + } finally { + loading.value = false + } } /** 添加/修改操作 */ diff --git a/src/views/system/loginlog/index.vue b/src/views/system/loginlog/index.vue index 011992b..4c442b8 100644 --- a/src/views/system/loginlog/index.vue +++ b/src/views/system/loginlog/index.vue @@ -45,7 +45,7 @@ plain @click="handleExport" :loading="exportLoading" - v-hasPermi="['infra:login-log:export']" + v-hasPermi="['system:login-log:export']" > <Icon icon="ep:download" class="mr-5px" /> 导出 </el-button> @@ -56,16 +56,16 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="日志编号" align="center" prop="id" /> - <el-table-column label="操作类型" align="center" prop="logType"> + <el-table-column label="日志编号" align="center" prop="id" width="100" /> + <el-table-column label="操作类型" align="center" prop="logType" width="100"> <template #default="scope"> <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="scope.row.logType" /> </template> </el-table-column> <el-table-column label="用户名称" align="center" prop="username" width="180" /> <el-table-column label="登录地址" align="center" prop="userIp" width="180" /> - <el-table-column label="浏览器" align="center" prop="userAgent" /> - <el-table-column label="登陆结果" align="center" prop="result"> + <el-table-column label="浏览器" align="center" prop="userAgent" :show-overflow-tooltip="true"/> + <el-table-column label="登录结果" align="center" prop="result" width="100"> <template #default="scope"> <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="scope.row.result" /> </template> @@ -77,13 +77,13 @@ width="180" :formatter="dateFormatter" /> - <el-table-column label="操作" align="center"> + <el-table-column label="操作" align="center" width="80"> <template #default="scope"> <el-button link type="primary" @click="openDetail(scope.row)" - v-hasPermi="['infra:login-log:query']" + v-hasPermi="['system:login-log:query']" > 详情 </el-button> diff --git a/src/views/system/menu/MenuForm.vue b/src/views/system/menu/MenuForm.vue index 2b4a90d..a5004d0 100644 --- a/src/views/system/menu/MenuForm.vue +++ b/src/views/system/menu/MenuForm.vue @@ -115,13 +115,13 @@ <script lang="ts" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import * as MenuApi from '@/api/system/menu' -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import {CACHE_KEY, useCache, useSessionCache} from '@/hooks/web/useCache' import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants' import { defaultProps, handleTree } from '@/utils/tree' defineOptions({ name: 'SystemMenuForm' }) -const { wsCache } = useCache() +const { wsSessionCache } = useSessionCache() const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -214,7 +214,7 @@ } finally { formLoading.value = false // 清空,从而触发刷新 - wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) } } diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index 4f280b5..03d352f 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -50,10 +50,6 @@ <Icon class="mr-5px" icon="ep:plus" /> 新增 </el-button> - <el-button plain type="danger" @click="toggleExpandAll"> - <Icon class="mr-5px" icon="ep:sort" /> - 展开/折叠 - </el-button> <el-button plain @click="refreshMenu"> <Icon class="mr-5px" icon="ep:refresh" /> 刷新菜单缓存 @@ -64,57 +60,22 @@ <!-- 列表 --> <ContentWrap> - <el-table - v-if="refreshTable" - v-loading="loading" - :data="list" - :default-expand-all="isExpandAll" - row-key="id" - > - <el-table-column :show-overflow-tooltip="true" label="菜单名称" prop="name" width="250" /> - <el-table-column align="center" label="图标" prop="icon" width="100"> - <template #default="scope"> - <Icon :icon="scope.row.icon" /> + <div style="height: 700px"> + <!-- AutoResizer 自动调节大小 --> + <el-auto-resizer> + <template #default="{ height, width }"> + <!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 --> + <el-table-v2 + v-loading="loading" + :columns="columns" + :data="list" + :width="width" + :height="height" + expand-column-key="name" + /> </template> - </el-table-column> - <el-table-column label="排序" prop="sort" width="60" /> - <el-table-column :show-overflow-tooltip="true" label="权限标识" prop="permission" /> - <el-table-column :show-overflow-tooltip="true" label="组件路径" prop="component" /> - <el-table-column :show-overflow-tooltip="true" label="组件名称" prop="componentName" /> - <el-table-column label="状态" prop="status" width="80"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column align="center" label="操作"> - <template #default="scope"> - <el-button - v-hasPermi="['system:menu:update']" - link - type="primary" - @click="openForm('update', scope.row.id)" - > - 修改 - </el-button> - <el-button - v-hasPermi="['system:menu:create']" - link - type="primary" - @click="openForm('create', undefined, scope.row.id)" - > - 新增 - </el-button> - <el-button - v-hasPermi="['system:menu:delete']" - link - type="danger" - @click="handleDelete(scope.row.id)" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> + </el-auto-resizer> + </div> </ContentWrap> <!-- 表单弹窗:添加/修改 --> @@ -124,15 +85,117 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { handleTree } from '@/utils/tree' import * as MenuApi from '@/api/system/menu' +import { MenuVO } from '@/api/system/menu' import MenuForm from './MenuForm.vue' -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' +import { CACHE_KEY, useCache, useSessionCache } from '@/hooks/web/useCache' +import { h } from 'vue' +import { Column, ElButton } from 'element-plus' +import { Icon } from '@/components/Icon' +import { hasPermission } from '@/directives/permission/hasPermi' +import { CommonStatusEnum } from '@/utils/constants' defineOptions({ name: 'SystemMenu' }) const { wsCache } = useCache() +const { wsSessionCache } = useSessionCache() const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 +// 表格的 column 字段 +const columns: Column[] = [ + { + dataKey: 'name', + title: '菜单名称', + width: 250 + }, + { + dataKey: 'icon', + title: '图标', + width: 150, + cellRenderer: ({ rowData }) => { + return h(Icon, { + icon: rowData.icon + }) + } + }, + { + dataKey: 'sort', + title: '排序', + width: 100 + }, + { + dataKey: 'permission', + title: '权限标识', + width: 240 + }, + { + dataKey: 'component', + title: '组件路径', + width: 240 + }, + { + dataKey: 'componentName', + title: '组件名称', + width: 240 + }, + { + dataKey: 'status', + title: '状态', + width: 160, + cellRenderer: ({ rowData }) => { + return h(ElSwitch, { + modelValue: rowData.status, + activeValue: CommonStatusEnum.ENABLE, + inactiveValue: CommonStatusEnum.DISABLE, + loading: menuStatusUpdating.value[rowData.id], + disabled: !hasPermission(['system:menu:update']), + onChange: (val) => handleStatusChanged(rowData, val as number) + }) + } + }, + { + dataKey: 'operation', + title: '操作', + width: 200, + cellRenderer: ({ rowData }) => { + return h( + 'div', + [ + hasPermission(['system:menu:update']) && + h( + ElButton, + { + link: true, + type: 'primary', + onClick: () => openForm('update', rowData.id) + }, + '修改' + ), + hasPermission(['system:menu:create']) && + h( + ElButton, + { + link: true, + type: 'primary', + onClick: () => openForm('create', undefined, rowData.id) + }, + '新增' + ), + hasPermission(['system:menu:delete']) && + h( + ElButton, + { + link: true, + type: 'danger', + onClick: () => handleDelete(rowData.id) + }, + '删除' + ) + ].filter(Boolean) + ) + } + } +] const loading = ref(true) // 列表的加载中 const list = ref<any>([]) // 列表的数据 const queryParams = reactive({ @@ -140,8 +203,6 @@ status: undefined }) const queryFormRef = ref() // 搜索的表单 -const isExpandAll = ref(false) // 是否展开,默认全部折叠 -const refreshTable = ref(true) // 重新渲染表格状态 /** 查询列表 */ const getList = async () => { @@ -171,15 +232,6 @@ formRef.value.open(type, id, parentId) } -/** 展开/折叠操作 */ -const toggleExpandAll = () => { - refreshTable.value = false - isExpandAll.value = !isExpandAll.value - nextTick(() => { - refreshTable.value = true - }) -} - /** 刷新菜单缓存按钮操作 */ const refreshMenu = async () => { try { @@ -187,6 +239,8 @@ // 清空,从而触发刷新 wsCache.delete(CACHE_KEY.USER) wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + wsSessionCache.delete(CACHE_KEY.USER) + wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) // 刷新浏览器 location.reload() } catch {} @@ -205,6 +259,21 @@ } catch {} } +/** 开启/关闭菜单的状态 */ +const menuStatusUpdating = ref({}) // 菜单状态更新中的 menu 映射。key:菜单编号,value:是否更新中 +const handleStatusChanged = async (menu: MenuVO, val: number) => { + // 1. 标记 menu.id 更新中 + menuStatusUpdating.value[menu.id] = true + try { + // 2. 发起更新状态 + menu.status = val + await MenuApi.updateMenu(menu) + } finally { + // 3. 标记 menu.id 更新完成 + menuStatusUpdating.value[menu.id] = false + } +} + /** 初始化 **/ onMounted(() => { getList() diff --git a/src/views/system/operatelog/index.vue b/src/views/system/operatelog/index.vue index bd67305..dc19764 100644 --- a/src/views/system/operatelog/index.vue +++ b/src/views/system/operatelog/index.vue @@ -79,7 +79,7 @@ plain @click="handleExport" :loading="exportLoading" - v-hasPermi="['infra:operate-log:export']" + v-hasPermi="['system:operate-log:export']" > <Icon icon="ep:download" class="mr-5px" /> 导出 </el-button> @@ -110,7 +110,7 @@ link type="primary" @click="openDetail(scope.row)" - v-hasPermi="['infra:operate-log:query']" + v-hasPermi="['system:operate-log:query']" > 详情 </el-button> diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue index 5a8cdb3..f945726 100644 --- a/src/views/system/role/index.vue +++ b/src/views/system/role/index.vue @@ -125,6 +125,16 @@ 菜单权限 </el-button> <el-button + v-hasPermi="['system:permission:assign-role-data-scope']" + link + preIcon="ep:coin" + title="数据权限" + type="primary" + @click="openDataPermissionForm(scope.row)" + > + 数据权限 + </el-button> + <el-button v-hasPermi="['system:role:delete']" link type="danger" diff --git a/src/views/system/tenant/index.vue b/src/views/system/tenant/index.vue index be849d7..f81c5d3 100644 --- a/src/views/system/tenant/index.vue +++ b/src/views/system/tenant/index.vue @@ -99,7 +99,7 @@ <el-table v-loading="loading" :data="list"> <el-table-column label="租户编号" align="center" prop="id" /> <el-table-column label="租户名" align="center" prop="name" /> - <el-table-column label="租户套餐" align="center" prop="packageId"> + <el-table-column label="租户套餐" align="center" prop="packageId" width="180" > <template #default="scope"> <el-tag v-if="scope.row.packageId === 0" type="danger">系统租户</el-tag> <template v-else v-for="item in packageList"> diff --git a/src/views/system/tenantPackage/TenantPackageForm.vue b/src/views/system/tenantPackage/TenantPackageForm.vue index 7492889..476a1df 100644 --- a/src/views/system/tenantPackage/TenantPackageForm.vue +++ b/src/views/system/tenantPackage/TenantPackageForm.vue @@ -10,6 +10,12 @@ <el-form-item label="套餐名" prop="name"> <el-input v-model="formData.name" placeholder="请输入套餐名" /> </el-form-item> + <el-form-item label="套餐介绍" prop="description"> + <el-input type="textarea" v-model="formData.description" placeholder="请输入套餐介绍" /> + </el-form-item> + <el-form-item label="套餐图标"> + <UploadImg v-model="formData.icon" :limit="1" /> + </el-form-item> <el-form-item label="菜单权限"> <el-card class="cardHeight"> <template #header> @@ -39,6 +45,18 @@ show-checkbox /> </el-card> + </el-form-item> + <el-form-item label="套餐标签" prop="labels"> + <el-select + v-model="formData.labels" + filterable + multiple + allow-create + placeholder="请输入套餐标签" + style="width: 500px" + > + <el-option v-for="label in formData.labels" :key="label" :label="label" :value="label" /> + </el-select> </el-form-item> <el-form-item label="状态" prop="status"> <el-radio-group v-model="formData.status"> @@ -81,6 +99,9 @@ const formData = ref({ id: null, name: null, + icon: undefined, + labels: [], + description: null, remark: null, menuIds: [], status: CommonStatusEnum.ENABLE @@ -161,6 +182,9 @@ formData.value = { id: null, name: null, + icon: undefined, + labels: [], + description: null, remark: null, menuIds: [], status: CommonStatusEnum.ENABLE diff --git a/src/views/system/tenantPackage/index.vue b/src/views/system/tenantPackage/index.vue index fc68a4d..56f8f3d 100644 --- a/src/views/system/tenantPackage/index.vue +++ b/src/views/system/tenantPackage/index.vue @@ -27,73 +27,83 @@ /> </el-select> </el-form-item> - <el-form-item label="创建时间" prop="createTime"> - <el-date-picker - v-model="queryParams.createTime" - type="daterange" - value-format="YYYY-MM-DD HH:mm:ss" - start-placeholder="开始日期" - end-placeholder="结束日期" - 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 @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="['system:tenant-package:create']" > - <Icon icon="ep:plus" class="mr-5px" /> + <Icon icon="ep:plus" class="mr-5px"/> 新增 </el-button> </el-form-item> </el-form> </ContentWrap> - <!-- 列表 --> <ContentWrap> - <el-table v-loading="loading" :data="list"> - <el-table-column label="套餐编号" align="center" prop="id" width="120" /> - <el-table-column label="套餐名" align="center" prop="name" /> - <el-table-column label="状态" align="center" prop="status" width="100"> - <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="remark" /> - <el-table-column - label="创建时间" - align="center" - prop="createTime" - width="180" - :formatter="dateFormatter" - /> - <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> - <template #default="scope"> - <el-button - link - type="primary" - @click="openForm('update', scope.row.id)" - v-hasPermi="['system:tenant-package:update']" - > - 修改 - </el-button> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['system:tenant-package:delete']" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> + <el-skeleton :loading="loading"> + <div class="package-card" v-for="(item, index) in packages" :key="`dynamics-${index}`"> + <div class="card-content"> + <img class="card-icon" :src="item.icon"/> + <div class="card-middle"> + <div class="tenant-title">{{ item.name }}</div> + <div class="tenant-operation"> + <el-dropdown @command="(command) => handleCommand(command, item)" + v-hasPermi="[ + 'system:tenant-package:update', + 'system:tenant-package:delete' + ]"> + <el-button type="primary" link> + <Icon icon="ep:more-filled"/> + </el-button> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item + command="handleUpdate" + v-if="checkPermi(['system:tenant-package:update'])" + > + <Icon icon="ep:edit"/> + 修改 + </el-dropdown-item> + <el-dropdown-item + command="handleDelete" + v-if="checkPermi(['system:tenant-package:delete'])" + > + <Icon icon="ep:delete"/> + 删除 + </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </div> + </div> + <div class="description">{{ item.description }}</div> + <div class="label-areas"> + <el-tag + :disable-transitions="true" + :key="i" + v-for="(label, i) in item.labels" + :index="i" + class="label" + > + {{ label }} + </el-tag> + </div> + </div> + </div> + </el-skeleton> <!-- 分页 --> <Pagination + class="pagination" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @@ -102,22 +112,24 @@ </ContentWrap> <!-- 表单弹窗:添加/修改 --> - <TenantPackageForm ref="formRef" @success="getList" /> + <TenantPackageForm ref="formRef" @success="getList"/> </template> <script lang="ts" setup> -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' +import {DICT_TYPE, getIntDictOptions} from '@/utils/dict' import * as TenantPackageApi from '@/api/system/tenantPackage' import TenantPackageForm from './TenantPackageForm.vue' +import {TenantPackageVO} from "@/api/system/tenantPackage"; +import {checkPermi} from "@/utils/permission"; -defineOptions({ name: 'SystemTenantPackage' }) +defineOptions({name: 'SystemTenantPackage'}) const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 +const {t} = useI18n() // 国际化 const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 +const packages = ref([]) + const queryParams = reactive({ pageNo: 1, pageSize: 10, @@ -130,10 +142,9 @@ /** 查询列表 */ const getList = async () => { - loading.value = true try { const data = await TenantPackageApi.getTenantPackagePage(queryParams) - list.value = data.list + packages.value = data.list total.value = data.total } finally { loading.value = false @@ -158,6 +169,20 @@ formRef.value.open(type, id) } +/** 操作分发 */ +const handleCommand = (command: string, row: TenantPackageVO) => { + switch (command) { + case 'handleUpdate': + openForm('update', row.id) + break + case 'handleDelete': + handleDelete(row.id) + break + default: + break + } +} + /** 删除按钮操作 */ const handleDelete = async (id: number) => { try { @@ -168,11 +193,95 @@ message.success(t('common.delSuccess')) // 刷新列表 await getList() - } catch {} + } catch { + } +} + +const initData = async () => { + await Promise.all([ + getList() + ]) } /** 初始化 **/ -onMounted(() => { - getList() -}) +initData() + </script> +<style lang="scss" scoped> +.package-card { + display: inline-block; + width: 396px; + height: 379px; + background: #FFFFFF; + border-radius: 4px 4px 4px 4px; + border: 1px solid #EBEDF0; + margin: 0px 8px 8px 0; +} + +.card-content { + margin-left: 10px; +} + +.card-icon { + width: 372px; + height: 200px; + margin: 10px 0 10px 2px; +} + +.card-middle { + width: 396px; + height: 25px; + margin: 0 12px; + display: flex; +} + +.tenant-title { + width: 340px; + height: 25px; + font-family: Microsoft YaHei UI, Microsoft YaHei UI; + font-weight: bold; + font-size: 20px; + color: #282F3D; +} + +.tenant-operation { + margin-right: 12px; +} + +.description { + margin: 8px 12px; + width: 372px; + height: 36px; + font-family: Microsoft YaHei, Microsoft YaHei; + font-weight: 400; + font-size: 14px; + color: #6B7785; + line-height: 18px; + word-wrap: break-word; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.label-areas { + display: flex; + flex-wrap: wrap; + height: 54px; + width: 396px; + margin: 0 8px; +} + +.label { + width: 80px; + height: 20px; + margin: 4px; + border-radius: 80px 80px 80px 80px; + border: 1px solid #417CFF; +} +.pagination { + margin-right: 30px; + margin-top: 400px; +} +</style> diff --git a/stylelint.config.js b/stylelint.config.js index 890b45b..b336785 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -13,19 +13,19 @@ 'at-rule-no-unknown': [ true, { - ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin'] + ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend'] } ], 'media-query-no-invalid': null, 'function-no-unknown': null, 'no-empty-source': null, 'named-grid-areas-no-invalid': null, - 'unicode-bom': 'never', + // 'unicode-bom': 'never', 'no-descending-specificity': null, 'font-family-no-missing-generic-family-keyword': null, - 'declaration-colon-space-after': 'always-single-line', - 'declaration-colon-space-before': 'never', - 'declaration-block-trailing-semicolon': null, + // 'declaration-colon-space-after': 'always-single-line', + // 'declaration-colon-space-before': 'never', + // 'declaration-block-trailing-semicolon': null, 'rule-empty-line-before': [ 'always', { diff --git a/tsconfig.json b/tsconfig.json index 182852a..38376ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,11 +24,11 @@ "@/*": ["src/*"] }, "types": [ - "@intlify/unplugin-vue-i18n/types", - "vite/client", - "element-plus/global", - "@types/qrcode", - "vite-plugin-svg-icons/client" + // "@intlify/unplugin-vue-i18n/types", + "vite/client" + // "element-plus/global", + // "@types/qrcode", + // "vite-plugin-svg-icons/client" ], "outDir": "target", // 请保留这个属性,防止tsconfig.json文件报错 "typeRoots": ["./node_modules/@types/", "./types"] diff --git a/types/env.d.ts b/types/env.d.ts index 25cef34..76f4669 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -22,9 +22,9 @@ readonly VITE_APP_DEFAULT_LOGIN_PASSWORD: string readonly VITE_APP_DOCALERT_ENABLE: string readonly VITE_BASE_URL: string - readonly VITE_UPLOAD_URL: string readonly VITE_API_URL: string readonly VITE_BASE_PATH: string + readonly VITE_UPLOAD_TYPE: string readonly VITE_VIDEO_CAMERA_DOMAIN: string readonly VITE_DROP_DEBUGGER: string readonly VITE_DROP_CONSOLE: string diff --git a/types/router.d.ts b/types/router.d.ts index 9b08b80..03e91f1 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -15,6 +15,8 @@ title: 'title' 设置该路由在侧边栏和面包屑中展示的名字 + titleSuffix: '2' 当 path 和 title 重复时的后缀或备注 + icon: 'svg-name' 设置该路由的图标 noCache: true 如果设置为true,则不会被 <keep-alive> 缓存(默认 false) @@ -37,6 +39,7 @@ hidden?: boolean alwaysShow?: boolean title?: string + titleSuffix?: string icon?: string noCache?: boolean breadcrumb?: boolean diff --git a/uno.config.ts b/uno.config.ts index d146731..e52457a 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -12,7 +12,7 @@ ${selector} { display: flex; height: 100%; - padding: 1px 10px 0; + padding: 0 10px; cursor: pointer; align-items: center; transition: background var(--transition-time-02); diff --git a/vite.config.ts b/vite.config.ts index 8cba915..891e5f9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,8 @@ -import { resolve } from 'path' -import { loadEnv } from 'vite' -import type { UserConfig, ConfigEnv } from 'vite' -import { createVitePlugins } from './build/vite' -import { include, exclude } from "./build/vite/optimize" +import {resolve} from 'path' +import type {ConfigEnv, UserConfig} from 'vite' +import {loadEnv} from 'vite' +import {createVitePlugins} from './build/vite' +import {exclude, include} from "./build/vite/optimize" // 当前执行node命令时文件夹的地址(工作目录) const root = process.cwd() @@ -12,7 +12,7 @@ } // https://vitejs.dev/config/ -export default ({ command, mode }: ConfigEnv): UserConfig => { +export default ({command, mode}: ConfigEnv): UserConfig => { let env = {} as any const isBuild = command === 'build' if (!isBuild) { @@ -43,8 +43,9 @@ css: { preprocessorOptions: { scss: { - additionalData: '@import "./src/styles/variables.scss";', - javascriptEnabled: true + additionalData: '@use "@/styles/variables.scss" as *;', + javascriptEnabled: true, + silenceDeprecations: ["legacy-js-api"], } } }, @@ -71,8 +72,15 @@ drop_debugger: env.VITE_DROP_DEBUGGER === 'true', drop_console: env.VITE_DROP_CONSOLE === 'true' } - } + }, + rollupOptions: { + output: { + manualChunks: { + echarts: ['echarts'] // 将 echarts 单独打包,参考 https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/IAB1SX 讨论 + } + }, + }, }, - optimizeDeps: { include, exclude } + optimizeDeps: {include, exclude} } } diff --git a/web-types.json b/web-types.json new file mode 100644 index 0000000..602f212 --- /dev/null +++ b/web-types.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/web-types", + "framework": "vue", + "name": "name written in package.json", + "version": "version written in package.json", + "contributions": { + "html": { + "types-syntax": "typescript", + "attributes": [ + { + "name": "v-hasPermi" + }, + { + "name": "v-hasRole" + } + ] + } + } +} -- Gitblit v1.9.3