liriming
2 天以前 1220f5ca98b10b735a47c37a81fbfc554b01e2fe
Merge remote-tracking branch 'origin/master'
已删除24个文件
已修改196个文件
已添加69个文件
24647 ■■■■ 文件已修改
.env.dev 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.local 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.prod 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.test 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
public/logo.gif 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/category/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/model/index.ts 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/processInstance/index.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/simple/index.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/task/index.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/data/da/point/index.ts 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login/index.ts 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/model/mpk/mpk.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/model/mpk/project.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/model/pre/item/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/model/sche/model/index.ts 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/model/sche/record/index.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/model/sche/scheme/index.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenantPackage/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/imgs/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/add-user.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/approve.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/auditor.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/cancel.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/condition.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/copy.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/delay.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/finish.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/parallel.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/reject.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/running.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/simple-process-bg.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/starter.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/alipay_app.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/alipay_bar.svg 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/alipay_pc.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/alipay_qr.svg 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/alipay_wap.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/mock.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/wx_app.svg 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/wx_bar.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/wx_lite.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/wx_native.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/pay/icon/wx_pub.svg 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AppLinkInput/data.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ContentWrap/src/ContentWrap.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Crontab/src/Crontab.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DictTag/src/DictTag.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/ComponentContainer.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/ComponentContainerProperty.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/Carousel/config.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/Carousel/property.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/CouponCard/property.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/Divider/property.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/MenuGrid/property.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/MenuSwiper/property.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/NavigationBar/property.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/NoticeBar/config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/Popover/property.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/ProductCard/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/ProductCard/property.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/ProductList/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/ProductList/property.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionCombination/config.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionCombination/index.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionCombination/property.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionPoint/config.ts 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionPoint/index.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionPoint/property.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/SearchBar/property.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/TabBar/config.ts 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/TabBar/property.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/TitleBar/property.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DiyEditor/components/mobile/UserCard/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Draggable/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Echart/src/Echart.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Editor/src/Editor.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FormCreate/src/components/useApiSelect.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FormCreate/src/config/useDictSelectRule.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FormCreate/src/utils/index.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/IFrame/src/IFrame.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Icon/src/IconSelect.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RouterSearch/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ShortcutDateRangePicker/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesigner/src/addNode.vue 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesigner/src/nodeWrap.vue 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesigner/src/util.ts 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesigner/theme/workflow.css 1292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/consts.ts 606 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/node.ts 510 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue 429 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue 374 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue 913 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/utils.ts 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/iconfont.ttf 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/iconfont.woff 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/iconfont.woff2 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss 755 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UploadFile/src/UploadFile.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UploadFile/src/UploadImgs.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UploadFile/src/useUpload.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UserSelectForm/index.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue 959 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue 623 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/data.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue 280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/theme/element-variables.scss 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/theme/index.scss 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/theme/process-designer.scss 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/utils.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config/axios/service.ts 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directives/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directives/permission/hasPermi.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directives/permission/hasRole.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useCache.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useMessage.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppView.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Breadcrumb/src/Breadcrumb.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Footer/src/Footer.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Logo/src/Logo.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Menu/src/Menu.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Message/src/Message.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Setting/src/Setting.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TabMenu/src/TabMenu.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/src/TagsView.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/UserInfo/src/UserInfo.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/UserInfo/src/components/LockDialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/UserInfo/src/components/LockPage.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/useRenderLayout.tsx 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/formCreate/index.ts 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/modules/remaining.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/app.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/bpm/simpleWorkflow.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/tagsView.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.ts 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/global.module.scss 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/index.scss 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/var.css 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/constants.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dict.ts 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/formCreate.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/permission.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/routerHelper.ts 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/tree.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Home/Index.vue 113 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/category/CategoryForm.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/definition/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/form/editor/index.vue 63 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/form/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/group/UserGroupForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/CategoryDraggableModel.vue 511 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/ModelForm.vue 381 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/ModelImportForm.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/editor/index.vue 262 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/form/BasicInfo.vue 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/form/FormDesign.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/form/ProcessDesign.vue 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/form/index.vue 439 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/index.vue 515 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/index_old.vue 404 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processExpression/ProcessExpressionForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue 298 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/create/index.vue 423 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/create/index_old.vue 266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue 989 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskSignList.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/index.vue 597 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/index.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/manager/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processListener/ProcessListenerForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/simple/SimpleModelDesign.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/simpleWorkflow/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/copy/index.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/done/index.vue 116 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/manager/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/todo/index.vue 97 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/channel/http/api/tag/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/ind/category/CategoryForm.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/ind/data/DataSetForm.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/ind/item/AtomIndDefineForm.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/ind/item/CalIndDefineForm.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/ind/item/DerIndDefineForm.vue 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/plan/category/CategoryForm.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/point/DaPointChart.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/point/DaPointForm.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/point/index.vue 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/apiAccessLog/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/apiErrorLog/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/config/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/dataSourceConfig/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/file/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/micro/index.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/file/MpkForm.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/file/MpkRun.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/file/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/icon/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/menu/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/pack/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/project/ProjectForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/project/ProjectPackage.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/mpk/project/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/pre/dm/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/pre/item/MmPredictItemChart.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/pre/item/MmPredictItemForm.vue 171 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/pre/item/index.vue 62 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/pre/type/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/model/ScheduleModelForm.vue 307 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/model/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/scheme/ScheduleSchemeForm.vue 265 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/scheme/index.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/scheme/record/index.vue 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/drag/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/goview/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/jmreport/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/app/AppForm.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/app/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appmenu/AppMenuForm.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appmenu/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/area/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/loginlog/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/MenuForm.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/index.vue 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/operatelog/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/tenant/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/tenantPackage/TenantPackageForm.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/tenantPackage/index.vue 235 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
stylelint.config.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.json 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
types/env.d.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
types/router.d.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uno.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web-types.json 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.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'
.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'
.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
.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
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">
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"
public/favicon.ico
Binary files differ
public/logo.gif
Binary files differ
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 })
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 = {
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 })
}
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
  })
}
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 })
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}`})
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) => {
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) => {
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 })
}
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列表
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,
  }
}
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})
}
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 })
}
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
src/assets/imgs/logo.png

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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
src/assets/svgs/pay/icon/alipay_app.svg
文件已删除
src/assets/svgs/pay/icon/alipay_bar.svg
文件已删除
src/assets/svgs/pay/icon/alipay_pc.svg
文件已删除
src/assets/svgs/pay/icon/alipay_qr.svg
文件已删除
src/assets/svgs/pay/icon/alipay_wap.svg
文件已删除
src/assets/svgs/pay/icon/mock.svg
文件已删除
src/assets/svgs/pay/icon/wx_app.svg
文件已删除
src/assets/svgs/pay/icon/wx_bar.svg
文件已删除
src/assets/svgs/pay/icon/wx_lite.svg
文件已删除
src/assets/svgs/pay/icon/wx_native.svg
文件已删除
src/assets/svgs/pay/icon/wx_pub.svg
文件已删除
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'
      },
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>
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="范围">
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>
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;
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'">
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',
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
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>
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>
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
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">
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>
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>
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. 文字 -->
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'">
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: '',
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>
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' })
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'">
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' })
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>
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,
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>
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
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>
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>
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>
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,
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>
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
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>
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'
      }
    ]
  }
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)
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'">
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>
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
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(() => {
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
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"
src/components/FormCreate/src/config/useDictSelectRule.ts
@@ -48,7 +48,7 @@
        },
        {
          type: 'select',
          field: 'dictValueType',
          field: 'valueType',
          title: '字典值类型',
          value: 'str',
          options: [
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)
    })
  }
}
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>
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>
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
    // 这里可以执行相应的操作(例如打开搜索框等)
  }
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"
src/components/SimpleProcessDesigner/src/addNode.vue
文件已删除
src/components/SimpleProcessDesigner/src/nodeWrap.vue
文件已删除
src/components/SimpleProcessDesigner/src/util.ts
文件已删除
src/components/SimpleProcessDesigner/theme/workflow.css
文件已删除
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>
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>
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>
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>
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
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 }
]
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}
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 ''
}
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>
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>
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>
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>
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 }} &nbsp;<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>
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>
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>
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>
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>
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>
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>
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>
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>
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
}
src/components/SimpleProcessDesignerV2/theme/iconfont.ttf
Binary files differ
src/components/SimpleProcessDesignerV2/theme/iconfont.woff
Binary files differ
src/components/SimpleProcessDesignerV2/theme/iconfont.woff2
Binary files differ
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';
}
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>
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>
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'
}
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>
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 || '&nbsp;'
// }
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
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>
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": []
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',
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',
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': '创建数据存储',
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(
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>
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>
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 }} &nbsp;<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>
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
  }
}
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') ||
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: '取 消'
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)
  )
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'
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 }
)
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(
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 }
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]
}
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>
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>
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,
  () => {
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 {
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;
}
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;
    // }
  }
}
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 字段
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> &nbsp; </div>' +
          '<div>参考 https://xxxx/ 教程</div>' +
          '<div>参考 https://doc.iailab.cn/ 教程</div>' +
          '<div> &nbsp; </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(() => {
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()
    }
  })
}
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)
  })
}
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
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 登录表单
}
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'
        }
      )
    }
  }
}
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
      }
    ]"
  >
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;
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>
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
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;
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
  )
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>
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)
            }
          ]}
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>
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>
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'])
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()
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
                    }
                  ]}
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)
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 文档
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 })
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'
        }
      }
    ]
  },
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
    },
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)
}
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,
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
        }
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() {
src/styles/global.module.scss
@@ -1,4 +1,4 @@
@import './variables.scss';
@use './variables.scss' as *;
// 导出变量
:export {
  namespace: $namespace;
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;
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;
}
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 // 已取消
}
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'
}
src/utils/formCreate.ts
@@ -44,6 +44,7 @@
  value?: object
) => {
  if (isRef(detailPreview)) {
    // @ts-ignore
    detailPreview = detailPreview.value
  }
  // @ts-ignore
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)
    })
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)
        // 外链
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}`
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>
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()
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
}
/** 初始化 **/
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>
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
    }
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>
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>
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>
src/views/bpm/model/ModelImportForm.vue
文件已删除
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>
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>
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>
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>
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>
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>
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>
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>
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" />&nbsp; 发起
            </el-button>
            <el-button plain type="danger" @click="handleCancel">
              <Icon icon="ep:close" />&nbsp; 取消
            </el-button>
          </div>
        </div>
      </el-scrollbar>
    </div>
  </ContentWrap>
</template>
<script lang="ts" setup>
import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
import { BpmModelType } from '@/utils/constants'
import {
  CandidateStrategy,
  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>
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>
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>
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>
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" />&nbsp; {{ 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" />&nbsp; {{ 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" />&nbsp;
          {{ getButtonDisplayName(OperationButtonType.COPY) }}
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="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" />&nbsp;
          {{ 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" />&nbsp;
          {{ 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" />&nbsp;
          {{ 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" />&nbsp; 减签
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="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" />&nbsp;
          {{ 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" />&nbsp; 取消
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="cancelFormRef"
          :model="cancelForm"
          :rules="cancelFormRule"
          label-width="100px"
        >
          <el-form-item label="取消理由" prop="cancelReason">
            <span class="text-#878c93 text-12px">&nbsp; 取消后,该审批流程将自动结束</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" />&nbsp; 再次提交
    </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>
在上述文件截断后对比
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue (已删除) src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue (已删除) src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue (已删除) src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue (已删除) src/views/bpm/processInstance/detail/dialog/TaskSignList.vue (已删除) src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue (已删除) src/views/bpm/processInstance/detail/index.vue src/views/bpm/processInstance/index.vue src/views/bpm/processInstance/manager/index.vue src/views/bpm/processListener/ProcessListenerForm.vue src/views/bpm/simple/SimpleModelDesign.vue src/views/bpm/simpleWorkflow/index.vue src/views/bpm/task/copy/index.vue src/views/bpm/task/done/index.vue src/views/bpm/task/manager/index.vue src/views/bpm/task/todo/index.vue src/views/data/channel/http/api/tag/index.vue src/views/data/ind/category/CategoryForm.vue src/views/data/ind/data/DataSetForm.vue src/views/data/ind/item/AtomIndDefineForm.vue src/views/data/ind/item/CalIndDefineForm.vue src/views/data/ind/item/DerIndDefineForm.vue src/views/data/plan/category/CategoryForm.vue src/views/data/point/DaPointChart.vue src/views/data/point/DaPointForm.vue src/views/data/point/index.vue src/views/infra/apiAccessLog/index.vue src/views/infra/apiErrorLog/index.vue src/views/infra/config/index.vue src/views/infra/dataSourceConfig/index.vue src/views/infra/file/index.vue src/views/micro/index.vue src/views/model/mpk/file/MpkForm.vue src/views/model/mpk/file/MpkRun.vue src/views/model/mpk/file/index.vue src/views/model/mpk/icon/index.vue src/views/model/mpk/menu/index.vue src/views/model/mpk/pack/index.vue src/views/model/mpk/project/ProjectForm.vue src/views/model/mpk/project/ProjectPackage.vue src/views/model/mpk/project/index.vue src/views/model/pre/dm/index.vue src/views/model/pre/item/MmPredictItemChart.vue src/views/model/pre/item/MmPredictItemForm.vue src/views/model/pre/item/index.vue src/views/model/pre/type/index.vue src/views/model/sche/model/ScheduleModelForm.vue src/views/model/sche/model/index.vue src/views/model/sche/scheme/ScheduleSchemeForm.vue src/views/model/sche/scheme/index.vue src/views/model/sche/scheme/record/index.vue src/views/report/drag/index.vue src/views/report/goview/index.vue src/views/report/jmreport/index.vue src/views/system/app/AppForm.vue src/views/system/app/index.vue src/views/system/appmenu/AppMenuForm.vue src/views/system/appmenu/index.vue src/views/system/area/index.vue src/views/system/loginlog/index.vue src/views/system/menu/MenuForm.vue src/views/system/menu/index.vue src/views/system/operatelog/index.vue src/views/system/role/index.vue src/views/system/tenant/index.vue src/views/system/tenantPackage/TenantPackageForm.vue src/views/system/tenantPackage/index.vue stylelint.config.js tsconfig.json types/env.d.ts types/router.d.ts uno.config.ts vite.config.ts web-types.json