兰宝车间质量管理系统-前端
疯狂的狮子Li
2024-05-20 a63543a5c793c8954fa2f9da0ee4fb215c62d8c2
!118 ♥️发布 5.2.0-BETA 公测版本
Merge pull request !118 from 疯狂的狮子Li/dev
已修改133个文件
已添加90个文件
已重命名2个文件
已删除4个文件
20386 ■■■■ 文件已修改
.env.development 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.eslintignore 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.eslintrc.cjs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.prettierrc 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.prettierrc.cjs 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README.md 101 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
commitlint.config.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
html/ie.html 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/online/index.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/client/index.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/client/types.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/config/index.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dept/types.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/post/index.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/post/types.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/role/index.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenant/index.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user/index.ts 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user/types.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tool/gen/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/category/index.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/category/types.ts 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/definitionConfig/index.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/definitionConfig/types.ts 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/formManage/index.ts 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/formManage/types.ts 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/leave/index.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/leave/types.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/model/index.ts 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/model/types.ts 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/nodeConfig/index.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/nodeConfig/types.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/processDefinition/index.ts 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/processDefinition/types.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/processInstance/index.ts 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/processInstance/types.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/task/index.ts 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/task/types.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/workflowCommon/index.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/workflow/workflowCommon/types.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/caret-back.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/caret-forward.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/category.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/finish.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/model.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/my-copy.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/my-task.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/process-definition.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/topiam.svg 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/waiting.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/workflow.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/element-ui.scss 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/index.scss 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/variables.module.scss 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/defaultXML.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/lang/zh.ts 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/moddle/flowable.ts 1250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/ContextPad/CustomContextPadProvider.ts 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/Palette/CustomPaletteProvider.ts 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/Renderer/CustomRenderer.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/Translate/index.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/index.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/showConfig.ts 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/style/index.scss 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/hooks/usePanel.ts 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/hooks/useParseElement.ts 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/index.vue 496 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/GatewayPanel.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/ParticipantPanel.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/ProcessPanel.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/SequenceFlowPanel.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/StartEndPanel.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/SubProcessPanel.vue 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/TaskPanel.vue 492 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/index.vue 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/property/DueDate.vue 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/property/ExecutionListener.vue 308 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/property/ListenerParam.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/property/TaskListener.vue 310 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/BpmnDesign/index.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/BpmnView/index.vue 410 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Breadcrumb/index.vue 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/BuildCode/index.vue 73 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/BuildCode/render.vue 69 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DictTag/index.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Editor/index.vue 187 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FileUpload/index.vue 242 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Hamburger/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/HeaderSearch/index.vue 100 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/IconSelect/index.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImagePreview/index.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImageUpload/index.vue 256 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/LangSelect/index.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Pagination/index.vue 94 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Process/approvalRecord.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Process/multiInstanceUser.vue 368 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Process/submitVerify.vue 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RightToolbar/index.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RoleSelect/index.vue 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RuoYiDoc/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RuoYiGit/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SizeSelect/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SvgIcon/index.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/TopNav/index.vue 105 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/TreeSelect/index.vue 123 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UserSelect/index.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/iFrame/index.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/common/copyText.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/permission/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enums/LanguageEnum.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enums/bpmn/IndexEnums.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/useDialog.ts 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lang/en_US.json 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lang/index.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lang/zh_CN.json 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppMain.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/IframeToggle/index.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/InnerLink/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 94 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Settings/index.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Link.vue 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Logo.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/SidebarItem.vue 121 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/SocialCallback/index.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/ScrollPane.vue 119 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/index.vue 364 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TopBar/search.vue 188 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/notice/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/permission.ts 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/tab.ts 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.ts 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/settings.ts 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/app.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/modeler.ts 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/notice.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.ts 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/settings.ts 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/tagsView.ts 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/axios.d.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/bpmn/editor/global.d.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/bpmn/index.d.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/bpmn/moddle.d.ts 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/bpmn/panel.d.ts 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/element.d.ts 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/env.d.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/global.d.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/module.d.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/router.d.ts 54 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/setting.d.ts 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/vform3-builds.d.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/index.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/propTypes.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.ts 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/ruoyi.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/websocket.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/demo/index.vue 114 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/tree/index.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/error/401.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/error/404.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/cache/index.vue 77 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/logininfor/index.vue 144 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/online/index.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/operlog/index.vue 79 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/snailjob/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/redirect/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/register.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/client/index.vue 134 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/config/index.vue 102 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/index.vue 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dict/data.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dict/index.vue 95 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/index.vue 144 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/notice/index.vue 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/oss/config.vue 179 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/oss/index.vue 147 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/post/index.vue 371 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/authUser.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/index.vue 260 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/selectUser.vue 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/tenant/index.vue 136 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/tenantPackage/index.vue 95 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/authRole.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 337 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/index.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/onlineDevice.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/resetPwd.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/thirdParty.vue 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/userAvatar.vue 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/userInfo.vue 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/basicInfoForm.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/editTable.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/genInfoForm.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/importTable.vue 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/index.vue 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/category/index.vue 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/formManage/index.vue 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/leave/index.vue 251 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/leave/leaveEdit.vue 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/model/index.vue 383 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/processDefinition/components/processPreview.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/processDefinition/index.vue 517 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/processInstance/index.vue 361 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/task/allTaskWaiting.vue 286 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/task/myDocument.vue 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/task/taskCopyList.vue 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/task/taskFinish.vue 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/task/taskWaiting.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.json 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
uno.config.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/compression.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/i18n.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/index.ts 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/setup-extend.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/unocss.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -13,11 +13,13 @@
# ç›‘控地址
VITE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications'
# powerjob æŽ§åˆ¶å°åœ°å€
VITE_APP_POWERJOB_ADMIN = 'http://localhost:7700/'
# SnailJob æŽ§åˆ¶å°åœ°å€
VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
VITE_APP_PORT = 80
# æŽ¥å£åŠ å¯†åŠŸèƒ½å¼€å…³(如需关闭 åŽç«¯ä¹Ÿå¿…须对应关闭)
VITE_APP_ENCRYPT = true
# æŽ¥å£åŠ å¯†ä¼ è¾“ RSA å…¬é’¥ä¸ŽåŽç«¯è§£å¯†ç§é’¥å¯¹åº” å¦‚更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# æŽ¥å£å“åº”解密 RSA ç§é’¥ä¸ŽåŽç«¯åŠ å¯†å…¬é’¥å¯¹åº” å¦‚更换需前后端一同更换
@@ -26,5 +28,5 @@
# å®¢æˆ·ç«¯id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
# websocket å¼€å…³(开发环境默认关闭ws å› vite的bug导致如ws无法连接则会崩溃)
VITE_APP_WEBSOCKET = false
# websocket å¼€å…³
VITE_APP_WEBSOCKET = true
.env.production
@@ -10,8 +10,8 @@
# ç›‘控地址
VITE_APP_MONITRO_ADMIN = '/admin/applications'
# powerjob æŽ§åˆ¶å°åœ°å€
VITE_APP_POWERJOB_ADMIN = '/powerjob'
# SnailJob æŽ§åˆ¶å°åœ°å€
VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
# ç”Ÿäº§çŽ¯å¢ƒ
VITE_APP_BASE_API = '/prod-api'
@@ -21,6 +21,8 @@
VITE_APP_PORT = 80
# æŽ¥å£åŠ å¯†åŠŸèƒ½å¼€å…³(如需关闭 åŽç«¯ä¹Ÿå¿…须对应关闭)
VITE_APP_ENCRYPT = true
# æŽ¥å£åŠ å¯†ä¼ è¾“ RSA å…¬é’¥ä¸ŽåŽç«¯è§£å¯†ç§é’¥å¯¹åº” å¦‚更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# æŽ¥å£å“åº”解密 RSA ç§é’¥ä¸ŽåŽç«¯åŠ å¯†å…¬é’¥å¯¹åº” å¦‚更换需前后端一同更换
.eslintignore
@@ -11,7 +11,7 @@
.husky
.local
/bin
.eslintrc.js
.eslintrc.cjs
prettier.config.js
src/assets
tailwind.config.js
tailwind.config.js
.eslintrc.cjs
ÎļþÃû´Ó .eslintrc.js ÐÞ¸Ä
@@ -1,28 +1,37 @@
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
    node: true,
    es6: true
  },
  parser: 'vue-eslint-parser',
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-essential',
    'plugin:@typescript-eslint/recommended',
    'plugin:vue/vue3-recommended',
    './.eslintrc-auto-import.json',
    'plugin:@typescript-eslint/recommended',
    'prettier',
    'plugin:prettier/recommended'
  ],
  parserOptions: {
    ecmaVersion: '2020',
    sourceType: 'module',
    project: './tsconfig.*?.json',
    parser: '@typescript-eslint/parser'
  },
  plugins: ['vue', '@typescript-eslint'],
  plugins: ['vue', '@typescript-eslint', 'import', 'promise', 'node', 'prettier'],
  rules: {
    'vue/multi-word-component-names': 'off',
    '@typescript-eslint/no-empty-function': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-unused-vars': 'off',
    '@typescript-eslint/no-this-alias': 'off',
    // vue
    'vue/multi-word-component-names': 'off',
    'vue/valid-define-props': 'off',
    'vue/no-v-model-argument': 'off',
    'prefer-rest-params': 'off',
    // prettier
    'prettier/prettier': 'error',
    '@typescript-eslint/ban-types': [
      'error',
      {
.gitignore
@@ -22,6 +22,7 @@
package-lock.json
yarn.lock
pnpm-lock.yaml
# ç¼–译生成的文件
auto-imports.d.ts
.prettierrc
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
{
  "printWidth": 150,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "quoteProps": "preserve",
  "jsxSingleQuote": false,
  "bracketSameLine": false,
  "trailingComma": "none",
  "bracketSpacing": true,
  "embeddedLanguageFormatting": "auto",
  "arrowParens": "always",
  "requirePragma": false,
  "insertPragma": false,
  "proseWrap": "preserve",
  "htmlWhitespaceSensitivity": "css",
  "vueIndentScriptAndStyle": false,
  "endOfLine": "auto"
}
.prettierrc.cjs
ÎļþÒÑɾ³ý
README.md
@@ -1,9 +1,10 @@
## å¹³å°ç®€ä»‹
* æœ¬ä»“库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) ç‰ˆæœ¬ã€‚
* é…å¥—后端代码仓库地址
* [RuoYi-Vue-Plus 5.X(注意版本号)](https://gitee.com/dromara/RuoYi-Vue-Plus)
* [RuoYi-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
- æœ¬ä»“库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) ç‰ˆæœ¬ã€‚
- æˆå‘˜é¡¹ç›®: åŸºäºŽ vben(ant-design-vue) çš„前端项目 [ruoyi-plus-vben](https://gitee.com/dapppp/ruoyi-plus-vben)
- é…å¥—后端代码仓库地址
- [RuoYi-Vue-Plus 5.X(注意版本号)](https://gitee.com/dromara/RuoYi-Vue-Plus)
- [RuoYi-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
## å‰ç«¯è¿è¡Œ
@@ -17,7 +18,7 @@
# å¯åŠ¨æœåŠ¡
npm run dev
# æž„建生产环境
# æž„建生产环境
npm run build:prod
# å‰ç«¯è®¿é—®åœ°å€ http://localhost:80
@@ -25,51 +26,51 @@
## æœ¬æ¡†æž¶ä¸ŽRuoYi的业务差异
| ä¸šåŠ¡     | åŠŸèƒ½è¯´æ˜Ž                                    | æœ¬æ¡†æž¶ | RuoYi            |
|--------|-----------------------------------------|-----|------------------|
| ç§Ÿæˆ·ç®¡ç†   | ç³»ç»Ÿå†…租户的管理 å¦‚:租户套餐、过期时间、用户数量、企业信息等         | æ”¯æŒ  | æ—                 |
| ç§Ÿæˆ·å¥—餐管理 | ç³»ç»Ÿå†…租户所能使用的套餐管理 å¦‚:套餐内所包含的菜单等             | æ”¯æŒ  | æ—                 |
| ç”¨æˆ·ç®¡ç†   | ç”¨æˆ·çš„管理配置 å¦‚:新增用户、分配用户所属部门、角色、岗位等          | æ”¯æŒ  | æ”¯æŒ               |
| éƒ¨é—¨ç®¡ç†   | é…ç½®ç³»ç»Ÿç»„织机构(公司、部门、小组) æ ‘结构展现支持数据权限          | æ”¯æŒ  | æ”¯æŒ               |
| å²—位管理   | é…ç½®ç³»ç»Ÿç”¨æˆ·æ‰€å±žæ‹…任职务                            | æ”¯æŒ  | æ”¯æŒ               |
| èœå•管理   | é…ç½®ç³»ç»Ÿèœå•、操作权限、按钮权限标识等                     | æ”¯æŒ  | æ”¯æŒ               |
| è§’色管理   | è§’色菜单权限分配、设置角色按机构进行数据范围权限划分              | æ”¯æŒ  | æ”¯æŒ               |
| å­—典管理   | å¯¹ç³»ç»Ÿä¸­ç»å¸¸ä½¿ç”¨çš„一些较为固定的数据进行维护                  | æ”¯æŒ  | æ”¯æŒ               |
| å‚数管理   | å¯¹ç³»ç»ŸåŠ¨æ€é…ç½®å¸¸ç”¨å‚æ•°                             | æ”¯æŒ  | æ”¯æŒ               |
| é€šçŸ¥å…¬å‘Š   | ç³»ç»Ÿé€šçŸ¥å…¬å‘Šä¿¡æ¯å‘布维护                            | æ”¯æŒ  | æ”¯æŒ               |
| æ“ä½œæ—¥å¿—   | ç³»ç»Ÿæ­£å¸¸æ“ä½œæ—¥å¿—记录和查询 ç³»ç»Ÿå¼‚常信息日志记录和查询             | æ”¯æŒ  | æ”¯æŒ               |
| ç™»å½•日志   | ç³»ç»Ÿç™»å½•日志记录查询包含登录异常                        | æ”¯æŒ  | æ”¯æŒ               |
| æ–‡ä»¶ç®¡ç†   | ç³»ç»Ÿæ–‡ä»¶å±•示、上传、下载、删除等管理                      | æ”¯æŒ  | æ—                 |
| æ–‡ä»¶é…ç½®ç®¡ç† | ç³»ç»Ÿæ–‡ä»¶ä¸Šä¼ ã€ä¸‹è½½æ‰€éœ€è¦çš„配置信息动态添加、修改、删除等管理          | æ”¯æŒ  | æ—                 |
| åœ¨çº¿ç”¨æˆ·ç®¡ç† | å·²ç™»å½•系统的在线用户信息监控与强制踢出操作                   | æ”¯æŒ  | æ”¯æŒ               |
| å®šæ—¶ä»»åŠ¡   | è¿è¡ŒæŠ¥è¡¨ã€ä»»åŠ¡ç®¡ç†(添加、修改、删除)、日志管理、执行器管理等         | æ”¯æŒ  | ä»…支持任务与日志管理       |
| ä»£ç ç”Ÿæˆ   | å¤šæ•°æ®æºå‰åŽç«¯ä»£ç çš„生成(java、html、xml、sql)支持CRUD下载 | æ”¯æŒ  | ä»…支持单数据源          |
| ç³»ç»ŸæŽ¥å£   | æ ¹æ®ä¸šåŠ¡ä»£ç è‡ªåŠ¨ç”Ÿæˆç›¸å…³çš„api接口文档                    | æ”¯æŒ  | æ”¯æŒ               |
| æœåŠ¡ç›‘æŽ§   | ç›‘视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等     | æ”¯æŒ  | ä»…支持单机CPU、内存、磁盘监控 |
| ç¼“存监控   | å¯¹ç³»ç»Ÿçš„缓存信息查询,命令统计等。                       | æ”¯æŒ  | æ”¯æŒ               |
| åœ¨çº¿æž„建器  | æ‹–动表单元素生成相应的HTML代码。                      | æ”¯æŒ  | æ”¯æŒ               |
| ä½¿ç”¨æ¡ˆä¾‹   | ç³»ç»Ÿçš„一些功能案例                               | æ”¯æŒ  | ä¸æ”¯æŒ              |
| ä¸šåŠ¡         | åŠŸèƒ½è¯´æ˜Ž                                                      | æœ¬æ¡†æž¶ | RuoYi                         |
| ------------ | ------------------------------------------------------------- | ------ | ----------------------------- |
| ç§Ÿæˆ·ç®¡ç†     | ç³»ç»Ÿå†…租户的管理 å¦‚:租户套餐、过期时间、用户数量、企业信息等  | æ”¯æŒ   | æ—                             |
| ç§Ÿæˆ·å¥—餐管理 | ç³»ç»Ÿå†…租户所能使用的套餐管理 å¦‚:套餐内所包含的菜单等          | æ”¯æŒ   | æ—                             |
| ç”¨æˆ·ç®¡ç†     | ç”¨æˆ·çš„管理配置 å¦‚:新增用户、分配用户所属部门、角色、岗位等    | æ”¯æŒ   | æ”¯æŒ                          |
| éƒ¨é—¨ç®¡ç†     | é…ç½®ç³»ç»Ÿç»„织机构(公司、部门、小组) æ ‘结构展现支持数据权限   | æ”¯æŒ   | æ”¯æŒ                          |
| å²—位管理     | é…ç½®ç³»ç»Ÿç”¨æˆ·æ‰€å±žæ‹…任职务                                      | æ”¯æŒ   | æ”¯æŒ                          |
| èœå•管理     | é…ç½®ç³»ç»Ÿèœå•、操作权限、按钮权限标识等                        | æ”¯æŒ   | æ”¯æŒ                          |
| è§’色管理     | è§’色菜单权限分配、设置角色按机构进行数据范围权限划分          | æ”¯æŒ   | æ”¯æŒ                          |
| å­—典管理     | å¯¹ç³»ç»Ÿä¸­ç»å¸¸ä½¿ç”¨çš„一些较为固定的数据进行维护                  | æ”¯æŒ   | æ”¯æŒ                          |
| å‚数管理     | å¯¹ç³»ç»ŸåŠ¨æ€é…ç½®å¸¸ç”¨å‚æ•°                                        | æ”¯æŒ   | æ”¯æŒ                          |
| é€šçŸ¥å…¬å‘Š     | ç³»ç»Ÿé€šçŸ¥å…¬å‘Šä¿¡æ¯å‘布维护                                      | æ”¯æŒ   | æ”¯æŒ                          |
| æ“ä½œæ—¥å¿—     | ç³»ç»Ÿæ­£å¸¸æ“ä½œæ—¥å¿—记录和查询 ç³»ç»Ÿå¼‚常信息日志记录和查询         | æ”¯æŒ   | æ”¯æŒ                          |
| ç™»å½•日志     | ç³»ç»Ÿç™»å½•日志记录查询包含登录异常                              | æ”¯æŒ   | æ”¯æŒ                          |
| æ–‡ä»¶ç®¡ç†     | ç³»ç»Ÿæ–‡ä»¶å±•示、上传、下载、删除等管理                          | æ”¯æŒ   | æ—                             |
| æ–‡ä»¶é…ç½®ç®¡ç† | ç³»ç»Ÿæ–‡ä»¶ä¸Šä¼ ã€ä¸‹è½½æ‰€éœ€è¦çš„配置信息动态添加、修改、删除等管理  | æ”¯æŒ   | æ—                             |
| åœ¨çº¿ç”¨æˆ·ç®¡ç† | å·²ç™»å½•系统的在线用户信息监控与强制踢出操作                    | æ”¯æŒ   | æ”¯æŒ                          |
| å®šæ—¶ä»»åŠ¡     | è¿è¡ŒæŠ¥è¡¨ã€ä»»åŠ¡ç®¡ç†(添加、修改、删除)、日志管理、执行器管理等  | æ”¯æŒ   | ä»…支持任务与日志管理          |
| ä»£ç ç”Ÿæˆ     | å¤šæ•°æ®æºå‰åŽç«¯ä»£ç çš„生成(java、html、xml、sql)支持CRUD下载  | æ”¯æŒ   | ä»…支持单数据源                |
| ç³»ç»ŸæŽ¥å£     | æ ¹æ®ä¸šåŠ¡ä»£ç è‡ªåŠ¨ç”Ÿæˆç›¸å…³çš„api接口文档                         | æ”¯æŒ   | æ”¯æŒ                          |
| æœåŠ¡ç›‘æŽ§     | ç›‘视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等 | æ”¯æŒ   | ä»…支持单机CPU、内存、磁盘监控 |
| ç¼“存监控     | å¯¹ç³»ç»Ÿçš„缓存信息查询,命令统计等。                            | æ”¯æŒ   | æ”¯æŒ                          |
| åœ¨çº¿æž„建器   | æ‹–动表单元素生成相应的HTML代码。                              | æ”¯æŒ   | æ”¯æŒ                          |
| ä½¿ç”¨æ¡ˆä¾‹     | ç³»ç»Ÿçš„一些功能案例                                            | æ”¯æŒ   | ä¸æ”¯æŒ                        |
## æ¼”示图例
|                                                                                            |                                                                                            |
|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| ![输入图片说明](https://foruda.gitee.com/images/1680077524361362822/270bb429_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680077619939771291/989bf9b6_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680077681751513929/1c27c5bd_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680077721559267315/74d63e23_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680077765638904515/1b75d4a6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078026375951297/eded7a4b_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078237104531207/0eb1b6a7_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078254306078709/5931e22f_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078287971528493/0b9af60a_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078308138770249/8d3b6696_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078352553634393/db5ef880_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078378238393374/601e4357_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078414983206024/2aae27c1_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078446738419874/ecce7d59_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078475971341775/149e8634_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078491666717143/3fadece7_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078558863188826/fb8ced2a_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078574561685461/ae68a0b2_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078594932772013/9d8bfec6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078626493093532/fcfe4ff6_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078643608812515/0295bd4f_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078685196286463/d7612c81_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078703877318597/56fce0bc_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078716586545643/b6dbd68f_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078734103217688/eb1e6aa6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078759131415480/73c525d8_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078779416197879/75e3ed02_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078802329118061/77e10915_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078893627848351/34a1c342_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078928175016986/f126ec4a_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078941718318363/b68a0f72_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078963175518631/3bb769a1_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680078982294090567/b31c343d_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079000642440444/77ca82a9_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680079020995074177/03b7d52e_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079039367822173/76811806_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png "屏幕截图") |
|                                                                                                      |                                                                                                      |
| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| ![输入图片说明](https://foruda.gitee.com/images/1680077524361362822/270bb429_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680077619939771291/989bf9b6_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680077681751513929/1c27c5bd_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680077721559267315/74d63e23_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680077765638904515/1b75d4a6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078026375951297/eded7a4b_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078237104531207/0eb1b6a7_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078254306078709/5931e22f_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078287971528493/0b9af60a_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078308138770249/8d3b6696_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078352553634393/db5ef880_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078378238393374/601e4357_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078414983206024/2aae27c1_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078446738419874/ecce7d59_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078475971341775/149e8634_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078491666717143/3fadece7_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078558863188826/fb8ced2a_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078574561685461/ae68a0b2_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078594932772013/9d8bfec6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078626493093532/fcfe4ff6_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078643608812515/0295bd4f_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078685196286463/d7612c81_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078703877318597/56fce0bc_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078716586545643/b6dbd68f_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078734103217688/eb1e6aa6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078759131415480/73c525d8_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078779416197879/75e3ed02_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078802329118061/77e10915_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078893627848351/34a1c342_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078928175016986/f126ec4a_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078941718318363/b68a0f72_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078963175518631/3bb769a1_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680078982294090567/b31c343d_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079000642440444/77ca82a9_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680079020995074177/03b7d52e_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079039367822173/76811806_1766278.png '屏幕截图') |
| ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png '屏幕截图') |
commitlint.config.js
ÎļþÒÑɾ³ý
html/ie.html
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
@@ -7,24 +7,206 @@
    <meta name="renderer" content="webkit" />
    <base target="_blank" />
    <style type="text/css">
      html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
      a{text-decoration:none;color:#0072c6;}a:hover{text-decoration:none;color:#004d8c;}
      body{width:960px;margin:0 auto;padding:10px;font-size:14px;line-height:24px;color:#454545;font-family:'Microsoft YaHei UI','Microsoft YaHei',DengXian,SimSun,'Segoe UI',Tahoma,Helvetica,sans-serif;overflow-y:scroll}
      h1{font-size:40px;line-height:80px;font-weight:100;margin-bottom:10px;}
      h2{font-size:20px;line-height:25px;font-weight:100;margin:10px 0;}
      em{color:red}
      p{margin-bottom:10px;}
      hr{margin:20px 0;border:0;border-top:1px solid #dadada}
      span{display:block;font-size:12px;line-height:12px;}
      .clean{clear:both;}
      .browser{padding:10px 10px;}
      .browser li{width:auto;padding:0 80px;margin-top:30px;height:34px;line-height:22px;float:left;list-style:none;background:url(data:image/png;base64,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) no-repeat;padding-left:40px}
      .browser .browser-firefox{background-position:0 -34px}
      .browser .browser-ie{background-position:0 -68px;margin-left:0px}
      .browser .browser-360{background-position:0 -170px;margin-left: -27px}
      html,
      body,
      div,
      span,
      applet,
      object,
      iframe,
      h1,
      h2,
      h3,
      h4,
      h5,
      h6,
      p,
      blockquote,
      pre,
      a,
      abbr,
      acronym,
      address,
      big,
      cite,
      code,
      del,
      dfn,
      em,
      img,
      ins,
      kbd,
      q,
      s,
      samp,
      small,
      strike,
      strong,
      sub,
      sup,
      tt,
      var,
      b,
      u,
      i,
      center,
      dl,
      dt,
      dd,
      ol,
      ul,
      li,
      fieldset,
      form,
      label,
      legend,
      table,
      caption,
      tbody,
      tfoot,
      thead,
      tr,
      th,
      td,
      article,
      aside,
      canvas,
      details,
      embed,
      figure,
      figcaption,
      footer,
      header,
      hgroup,
      menu,
      nav,
      output,
      ruby,
      section,
      summary,
      time,
      mark,
      audio,
      video {
        border: 0;
        font-size: 100%;
        font: inherit;
        vertical-align: baseline;
        margin: 0;
        padding: 0;
      }
      article,
      aside,
      details,
      figcaption,
      figure,
      footer,
      header,
      hgroup,
      menu,
      nav,
      section {
        display: block;
      }
      body {
        line-height: 1;
      }
      ol,
      ul {
        list-style: none;
      }
      blockquote,
      q {
        quotes: none;
      }
      blockquote:before,
      blockquote:after,
      q:before,
      q:after {
        content: none;
      }
      table {
        border-collapse: collapse;
        border-spacing: 0;
      }
      a {
        text-decoration: none;
        color: #0072c6;
      }
      a:hover {
        text-decoration: none;
        color: #004d8c;
      }
      body {
        width: 960px;
        margin: 0 auto;
        padding: 10px;
        font-size: 14px;
        line-height: 24px;
        color: #454545;
        font-family: 'Microsoft YaHei UI', 'Microsoft YaHei', DengXian, SimSun, 'Segoe UI', Tahoma, Helvetica, sans-serif;
        overflow-y: scroll;
      }
      h1 {
        font-size: 40px;
        line-height: 80px;
        font-weight: 100;
        margin-bottom: 10px;
      }
      h2 {
        font-size: 20px;
        line-height: 25px;
        font-weight: 100;
        margin: 10px 0;
      }
      em {
        color: red;
      }
      p {
        margin-bottom: 10px;
      }
      hr {
        margin: 20px 0;
        border: 0;
        border-top: 1px solid #dadada;
      }
      span {
        display: block;
        font-size: 12px;
        line-height: 12px;
      }
      .clean {
        clear: both;
      }
      .browser {
        padding: 10px 10px;
      }
      .browser li {
        width: auto;
        padding: 0 80px;
        margin-top: 30px;
        height: 34px;
        line-height: 22px;
        float: left;
        list-style: none;
        background: url(data:image/png;base64,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)
          no-repeat;
        padding-left: 40px;
      }
      .browser .browser-firefox {
        background-position: 0 -34px;
      }
      .browser .browser-ie {
        background-position: 0 -68px;
        margin-left: 0px;
      }
      .browser .browser-360 {
        background-position: 0 -170px;
        margin-left: -27px;
      }
    </style>
  </head>
  <body style="margin-top:50px">
  <body style="margin-top: 50px">
    <h1>请升级您的浏览器,以便我们更好的为您提供服务!</h1>
    <p>您正在使用 Internet Explorer çš„æ—©æœŸç‰ˆæœ¬ï¼ˆIE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。</p>
    <hr />
index.html
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
@@ -9,7 +9,7 @@
    <title>RuoYi-Vue-Plus多租户管理系统</title>
    <!--[if lt IE 11
      ]><script>
        window.location.href='/html/ie.html';
        window.location.href = '/html/ie.html';
      </script><!
    [endif]-->
    <style>
@@ -47,7 +47,7 @@
        margin: -75px 0 0 -75px;
        border-radius: 50%;
        border: 3px solid transparent;
        border-top-color: #FFF;
        border-top-color: #fff;
        -webkit-animation: spin 2s linear infinite;
        -ms-animation: spin 2s linear infinite;
        -moz-animation: spin 2s linear infinite;
@@ -57,7 +57,7 @@
      }
      #loader:before {
        content: "";
        content: '';
        position: absolute;
        top: 5px;
        left: 5px;
@@ -65,7 +65,7 @@
        bottom: 5px;
        border-radius: 50%;
        border: 3px solid transparent;
        border-top-color: #FFF;
        border-top-color: #fff;
        -webkit-animation: spin 3s linear infinite;
        -moz-animation: spin 3s linear infinite;
        -o-animation: spin 3s linear infinite;
@@ -74,7 +74,7 @@
      }
      #loader:after {
        content: "";
        content: '';
        position: absolute;
        top: 15px;
        left: 15px;
@@ -82,14 +82,13 @@
        bottom: 15px;
        border-radius: 50%;
        border: 3px solid transparent;
        border-top-color: #FFF;
        border-top-color: #fff;
        -moz-animation: spin 1.5s linear infinite;
        -o-animation: spin 1.5s linear infinite;
        -ms-animation: spin 1.5s linear infinite;
        -webkit-animation: spin 1.5s linear infinite;
        animation: spin 1.5s linear infinite;
      }
      @-webkit-keyframes spin {
        0% {
@@ -119,13 +118,12 @@
        }
      }
      #loader-wrapper .loader-section {
        position: fixed;
        top: 0;
        width: 51%;
        height: 100%;
        background: #7171C6;
        background: #7171c6;
        z-index: 1000;
        -webkit-transform: translateX(0);
        -ms-transform: translateX(0);
@@ -140,21 +138,20 @@
        right: 0;
      }
      .loaded #loader-wrapper .loader-section.section-left {
        -webkit-transform: translateX(-100%);
        -ms-transform: translateX(-100%);
        transform: translateX(-100%);
        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      }
      .loaded #loader-wrapper .loader-section.section-right {
        -webkit-transform: translateX(100%);
        -ms-transform: translateX(100%);
        transform: translateX(100%);
        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      }
      .loaded #loader {
@@ -182,7 +179,7 @@
      #loader-wrapper .load_title {
        font-family: 'Open Sans';
        color: #FFF;
        color: #fff;
        font-size: 19px;
        width: 100%;
        text-align: center;
@@ -197,7 +194,7 @@
        font-weight: normal;
        font-style: italic;
        font-size: 13px;
        color: #FFF;
        color: #fff;
        opacity: 0.5;
      }
    </style>
package.json
@@ -1,15 +1,16 @@
{
  "name": "ruoyi-vue-plus",
  "version": "5.1.2",
  "version": "5.2.0-BETA",
  "description": "RuoYi-Vue-Plus多租户管理系统",
  "author": "LionLi",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "dev": "vite serve --mode development",
    "build:prod": "vite build --mode production &&vue-tsc --noEmit",
    "build:prod": "vite build --mode production",
    "build:dev": "vite build --mode development",
    "preview": "vite preview",
    "lint": "eslint src/**/*.{ts,js,vue} --fix",
    "prepare": "husky install",
    "lint:eslint": "eslint  --fix --ext .ts,.js,.vue ./src ",
    "prettier": "prettier --write ."
  },
  "repository": {
@@ -17,68 +18,84 @@
    "url": "https://gitee.com/JavaLionLi/plus-ui.git"
  },
  "dependencies": {
    "@element-plus/icons-vue": "2.1.0",
    "@element-plus/icons-vue": "2.3.1",
    "@highlightjs/vue-plugin": "2.1.0",
    "@lezer/common": "1.2.1",
    "@vueup/vue-quill": "1.2.0",
    "@vueuse/core": "9.5.0",
    "@vueuse/core": "10.9.0",
    "animate.css": "4.1.1",
    "await-to-js": "^3.0.0",
    "axios": "^1.3.4",
    "echarts": "5.4.0",
    "element-plus": "2.2.27",
    "await-to-js": "3.0.0",
    "axios": "1.6.8",
    "bpmn-js": "16.4.0",
    "camunda-bpmn-js-behaviors": "1.2.2",
    "camunda-bpmn-moddle": "7.0.1",
    "crypto-js": "4.2.0",
    "diagram-js": "12.3.0",
    "didi": "9.0.2",
    "echarts": "5.5.0",
    "element-plus": "2.7.2",
    "file-saver": "2.0.5",
    "fuse.js": "6.6.2",
    "js-cookie": "3.0.1",
    "jsencrypt": "3.3.1",
    "crypto-js": "^4.1.1",
    "fuse.js": "7.0.0",
    "highlight.js": "11.9.0",
    "image-conversion": "^2.1.1",
    "js-cookie": "3.0.5",
    "jsencrypt": "3.3.2",
    "moddle": "6.2.3",
    "nprogress": "0.2.0",
    "path-browserify": "1.0.1",
    "path-to-regexp": "6.2.0",
    "pinia": "2.0.22",
    "screenfull": "6.0.0",
    "vform3-builds": "3.0.8",
    "vue": "3.2.45",
    "vue-cropper": "1.0.3",
    "vue-i18n": "9.2.2",
    "vue-router": "4.1.4",
    "vue-types": "^5.0.3"
    "path-to-regexp": "6.2.1",
    "pinia": "2.1.7",
    "preact": "10.19.7",
    "screenfull": "6.0.2",
    "vform3-builds": "3.0.10",
    "vue": "3.4.25",
    "vue-cropper": "1.1.1",
    "vue-i18n": "9.10.2",
    "vue-router": "4.3.2",
    "vue-types": "5.1.1",
    "vxe-table": "4.5.22",
    "zeebe-bpmn-moddle": "1.0.0"
  },
  "devDependencies": {
    "@iconify/json": "^2.2.40",
    "@intlify/unplugin-vue-i18n": "0.8.2",
    "@types/crypto-js": "^4.1.1",
    "@types/file-saver": "2.0.5",
    "@types/js-cookie": "3.0.3",
    "@types/node": "18.14.2",
    "@types/nprogress": "0.2.0",
    "@types/path-browserify": "^1.0.0",
    "@typescript-eslint/eslint-plugin": "5.56.0",
    "@typescript-eslint/parser": "5.56.0",
    "@unocss/preset-attributify": "^0.50.6",
    "@unocss/preset-icons": "^0.50.6",
    "@unocss/preset-uno": "^0.50.6",
    "@vitejs/plugin-vue": "4.0.0",
    "@vue/compiler-sfc": "3.2.45",
    "autoprefixer": "10.4.14",
    "eslint": "8.36.0",
    "eslint-config-prettier": "8.8.0",
    "eslint-plugin-prettier": "4.2.1",
    "eslint-plugin-vue": "9.9.0",
    "fast-glob": "^3.2.11",
    "husky": "7.0.4",
    "postcss": "^8.4.21",
    "prettier": "2.8.6",
    "sass": "1.56.1",
    "typescript": "4.9.5",
    "unocss": "^0.50.6",
    "unplugin-auto-import": "0.13.0",
    "unplugin-icons": "0.15.1",
    "unplugin-vue-components": "0.23.0",
    "vite": "4.3.1",
    "@iconify/json": "2.2.201",
    "@intlify/unplugin-vue-i18n": "3.0.1",
    "@types/crypto-js": "4.2.2",
    "@types/file-saver": "2.0.7",
    "@types/js-cookie": "3.0.6",
    "@types/node": "18.18.2",
    "@types/nprogress": "0.2.3",
    "@types/path-browserify": "1.0.2",
    "@typescript-eslint/eslint-plugin": "7.3.1",
    "@typescript-eslint/parser": "7.3.1",
    "@unocss/preset-attributify": "0.58.6",
    "@unocss/preset-icons": "0.58.6",
    "@unocss/preset-uno": "0.58.6",
    "@vitejs/plugin-vue": "5.0.4",
    "@vue/compiler-sfc": "3.4.23",
    "autoprefixer": "10.4.18",
    "eslint": "8.57.0",
    "eslint-config-prettier": "9.1.0",
    "eslint-define-config": "2.1.0",
    "eslint-plugin-prettier": "5.1.3",
    "eslint-plugin-promise": "6.1.1",
    "eslint-plugin-node": "11.1.0",
    "eslint-plugin-import": "2.29.1",
    "eslint-plugin-vue": "9.23.0",
    "fast-glob": "3.3.2",
    "postcss": "8.4.36",
    "prettier": "3.2.5",
    "sass": "1.72.0",
    "typescript": "5.4.5",
    "unocss": "0.58.6",
    "unplugin-auto-import": "0.17.5",
    "unplugin-icons": "0.18.5",
    "unplugin-vue-components": "0.26.0",
    "unplugin-vue-setup-extend-plus": "1.0.1",
    "vite": "5.2.10",
    "vite-plugin-compression": "0.5.1",
    "vite-plugin-svg-icons": "2.0.1",
    "unplugin-vue-setup-extend-plus": "0.4.9",
    "vitest": "^0.29.7",
    "vue-eslint-parser": "9.1.0",
    "vue-tsc": "0.35.0"
    "vitest": "1.5.0",
    "vue-eslint-parser": "9.4.2",
    "vue-tsc": "2.0.13"
  }
}
src/App.vue
@@ -1,21 +1,20 @@
<template>
  <el-config-provider :locale="appStore.locale" :size="size">
  <el-config-provider :locale="appStore.locale" :size="appStore.size">
    <router-view />
  </el-config-provider>
</template>
<script setup lang="ts">
import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme'
import useSettingsStore from '@/store/modules/settings';
import { handleThemeStyle } from '@/utils/theme';
import useAppStore from '@/store/modules/app';
const appStore = useAppStore();
const size = computed(() => appStore.size as any);
onMounted(() => {
  nextTick(() => {
    // åˆå§‹åŒ–主题样式
    handleThemeStyle(useSettingsStore().theme)
  })
})
    handleThemeStyle(useSettingsStore().theme);
  });
});
</script>
src/api/login.ts
@@ -20,7 +20,8 @@
    url: '/auth/login',
    headers: {
      isToken: false,
      isEncrypt: true
      isEncrypt: true,
      repeatSubmit: false
    },
    method: 'post',
    data: params
@@ -38,7 +39,8 @@
    url: '/auth/register',
    headers: {
      isToken: false,
      isEncrypt: true
      isEncrypt: true,
      repeatSubmit: false
    },
    method: 'post',
    data: params
src/api/monitor/online/index.ts
@@ -18,3 +18,19 @@
    method: 'delete'
  });
}
// èŽ·å–å½“å‰ç”¨æˆ·ç™»å½•åœ¨çº¿è®¾å¤‡
export function getOnline() {
  return request({
    url: '/monitor/online',
    method: 'get'
  });
}
// åˆ é™¤å½“前在线设备
export function delOnline(tokenId: string) {
  return request({
    url: '/monitor/online/' + tokenId,
    method: 'post'
  });
}
src/api/system/client/index.ts
@@ -64,12 +64,12 @@
/**
 * çŠ¶æ€ä¿®æ”¹
 * @param id ID
 * @param clientId å®¢æˆ·ç«¯id
 * @param status çŠ¶æ€
 */
export function changeStatus(id: number | string, status: string) {
export function changeStatus(clientId: string, status: string) {
  const data = {
    id,
    clientId,
    status
  };
  return request({
src/api/system/client/types.ts
@@ -7,7 +7,7 @@
  /**
   * å®¢æˆ·ç«¯id
   */
  clientId: string | number;
  clientId: string;
  /**
   * å®¢æˆ·ç«¯key
src/api/system/config/index.ts
@@ -20,7 +20,7 @@
}
// æ ¹æ®å‚数键名查询参数值
export function getConfigKey(configKey: string): AxiosPromise<String> {
export function getConfigKey(configKey: string): AxiosPromise<string> {
  return request({
    url: '/system/config/configKey/' + configKey,
    method: 'get'
src/api/system/dept/types.ts
@@ -3,6 +3,7 @@
 */
export interface DeptQuery extends PageQuery {
  deptName?: string;
  deptCategory?: string;
  status?: number;
}
@@ -16,6 +17,7 @@
  children: DeptVO[];
  deptId: number | string;
  deptName: string;
  deptCategory: string;
  orderNum: number;
  leader: string;
  phone: string;
@@ -35,6 +37,7 @@
  children?: DeptForm[];
  deptId?: number | string;
  deptName?: string;
  deptCategory?: string;
  orderNum?: number;
  leader?: string;
  phone?: string;
src/api/system/post/index.ts
@@ -19,6 +19,18 @@
  });
}
// èŽ·å–å²—ä½é€‰æ‹©æ¡†åˆ—è¡¨
export function optionselect(deptId?: number | string, postIds?: (number | string)[]): AxiosPromise<PostVO[]> {
  return request({
    url: '/system/post/optionselect',
    method: 'get',
    params: {
      postIds: postIds,
      deptId: deptId
    }
  });
}
// æ–°å¢žå²—位
export function addPost(data: PostForm) {
  return request({
src/api/system/post/types.ts
@@ -1,7 +1,10 @@
export interface PostVO extends BaseEntity {
  postId: number | string;
  deptId: number | string;
  postCode: string;
  postName: string;
  postCategory: string;
  deptName: string;
  postSort: number;
  status: string;
  remark: string;
@@ -9,15 +12,20 @@
export interface PostForm {
  postId: number | string | undefined;
  deptId: number | string | undefined;
  postCode: string;
  postName: string;
  postCategory: string;
  postSort: number;
  status: string;
  remark: string;
}
export interface PostQuery extends PageQuery {
  deptId: number | string;
  belongDeptId: number | string;
  postCode: string;
  postName: string;
  postCategory: string;
  status: string;
}
src/api/system/role/index.ts
@@ -13,6 +13,17 @@
};
/**
 * é€šè¿‡roleIds查询角色
 * @param roleIds
 */
export const optionSelect = (roleIds: (number | string)[]): AxiosPromise<RoleVO[]> => {
  return request({
    url: '/system/role/optionselect?roleIds=' + roleIds,
    method: 'get'
  });
};
/**
 * æŸ¥è¯¢è§’色详细
 */
export const getRole = (roleId: string | number): AxiosPromise<RoleVO> => {
@@ -142,3 +153,8 @@
    method: 'get'
  });
};
export default {
  optionSelect,
  listRole
};
src/api/system/tenant/index.ts
@@ -25,7 +25,8 @@
    url: '/system/tenant',
    method: 'post',
    headers: {
      isEncrypt: true
      isEncrypt: true,
      repeatSubmit: false
    },
    data: data
  });
src/api/system/user/index.ts
@@ -18,6 +18,17 @@
};
/**
 * é€šè¿‡ç”¨æˆ·ids查询用户
 * @param userIds
 */
export const optionSelect = (userIds: (number | string)[]): AxiosPromise<UserVO[]> => {
  return request({
    url: '/system/user/optionselect?userIds=' + userIds,
    method: 'get'
  });
};
/**
 * èŽ·å–ç”¨æˆ·è¯¦æƒ…
 * @param userId
 */
@@ -75,7 +86,8 @@
    url: '/system/user/resetPwd',
    method: 'put',
    headers: {
      isEncrypt: true
      isEncrypt: true,
      repeatSubmit: false
    },
    data: data
  });
@@ -134,7 +146,8 @@
    url: '/system/user/profile/updatePwd',
    method: 'put',
    headers: {
      isEncrypt: true
      isEncrypt: true,
      repeatSubmit: false
    },
    data: data
  });
@@ -199,6 +212,7 @@
export default {
  listUser,
  getUser,
  optionSelect,
  addUser,
  updateUser,
  delUser,
src/api/system/user/types.ts
@@ -1,4 +1,3 @@
import { DeptVO } from './../dept/types';
import { RoleVO } from '@/api/system/role/types';
import { PostVO } from '@/api/system/post/types';
@@ -40,7 +39,7 @@
  loginIp: string;
  loginDate: string;
  remark: string;
  dept: DeptVO;
  deptName: string;
  roles: RoleVO[];
  roleIds: any;
  postIds: any;
src/api/tool/gen/index.ts
@@ -28,7 +28,7 @@
};
// ä¿®æ”¹ä»£ç ç”Ÿæˆä¿¡æ¯
export const updateGenTable = (data: DbTableForm) => {
export const updateGenTable = (data: DbTableForm): AxiosPromise<GenTableVO> => {
  return request({
    url: '/tool/gen',
    method: 'put',
@@ -37,7 +37,7 @@
};
// å¯¼å…¥è¡¨
export const importTable = (data: { tables: string; dataName: string }) => {
export const importTable = (data: { tables: string; dataName: string }): AxiosPromise<GenTableVO> => {
  return request({
    url: '/tool/gen/importTable',
    method: 'post',
src/api/workflow/category/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { CategoryVO, CategoryForm, CategoryQuery } from '@/api/workflow/category/types';
/**
 * æŸ¥è¯¢æµç¨‹åˆ†ç±»åˆ—表
 * @param query
 * @returns {*}
 */
export const listCategory = (query?: CategoryQuery): AxiosPromise<CategoryVO[]> => {
  return request({
    url: '/workflow/category/list',
    method: 'get',
    params: query
  });
};
/**
 * æŸ¥è¯¢æµç¨‹åˆ†ç±»è¯¦ç»†
 * @param id
 */
export const getCategory = (id: string | number): AxiosPromise<CategoryVO> => {
  return request({
    url: '/workflow/category/' + id,
    method: 'get'
  });
};
/**
 * æ–°å¢žæµç¨‹åˆ†ç±»
 * @param data
 */
export const addCategory = (data: CategoryForm) => {
  return request({
    url: '/workflow/category',
    method: 'post',
    data: data
  });
};
/**
 * ä¿®æ”¹æµç¨‹åˆ†ç±»
 * @param data
 */
export const updateCategory = (data: CategoryForm) => {
  return request({
    url: '/workflow/category',
    method: 'put',
    data: data
  });
};
/**
 * åˆ é™¤æµç¨‹åˆ†ç±»
 * @param id
 */
export const delCategory = (id: string | number | Array<string | number>) => {
  return request({
    url: '/workflow/category/' + id,
    method: 'delete'
  });
};
src/api/workflow/category/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
export interface CategoryVO {
  /**
   * ä¸»é”®
   */
  id: string;
  /**
   * åˆ†ç±»åç§°
   */
  categoryName: string;
  /**
   * åˆ†ç±»ç¼–码
   */
  categoryCode: string;
  /**
   * çˆ¶çº§id
   */
  parentId: string | number;
  /**
   * æŽ’序
   */
  sortNum: number;
  children?: CategoryVO[];
}
export interface CategoryForm extends BaseEntity {
  /**
   * ä¸»é”®
   */
  id?: string | number;
  /**
   * åˆ†ç±»åç§°
   */
  categoryName?: string;
  /**
   * åˆ†ç±»ç¼–码
   */
  categoryCode?: string;
  /**
   * çˆ¶çº§id
   */
  parentId?: string | number;
  /**
   * æŽ’序
   */
  sortNum?: number;
}
export interface CategoryQuery extends PageQuery {
  /**
   * åˆ†ç±»åç§°
   */
  categoryName?: string;
  /**
   * åˆ†ç±»ç¼–码
   */
  categoryCode?: string;
}
src/api/workflow/definitionConfig/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DefinitionConfigVO, DefinitionConfigForm } from '@/api/workflow/definitionConfig/types';
/**
 * æŸ¥è¯¢è¡¨å•配置详细
 * @param definitionId
 */
export const getByDefId = (definitionId: string | number): AxiosPromise<DefinitionConfigVO> => {
  return request({
    url: '/workflow/definitionConfig/getByDefId/' + definitionId,
    method: 'get'
  });
};
/**
 * æ–°å¢žè¡¨å•配置
 * @param data
 */
export const saveOrUpdate = (data: DefinitionConfigForm) => {
  return request({
    url: '/workflow/definitionConfig/saveOrUpdate',
    method: 'post',
    data: data
  });
};
/**
 * åˆ é™¤è¡¨å•配置
 * @param id
 */
export const deldefinitionConfig = (id: string | number | Array<string | number>) => {
  return request({
    url: '/workflow/definitionConfig/' + id,
    method: 'delete'
  });
};
/**
 * æŸ¥è¯¢æµç¨‹å®šä¹‰é…ç½®æŽ’除当前查询的流程定义
 * @param tableName
 * @param definitionId
 */
export const getByTableNameNotDefId = (tableName: string, definitionId: string | number) => {
  return request({
    url: `/workflow/definitionConfig/getByTableNameNotDefId/${tableName}/${definitionId}`,
    method: 'get'
  });
};
src/api/workflow/definitionConfig/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
import { FormManageVO } from '@/api/workflow/formManage/types';
export interface DefinitionConfigVO {
  /**
   * ä¸»é”®
   */
  id: string | number;
  /**
   * è¡¨å
   */
  tableName?: string;
  /**
   * æµç¨‹å®šä¹‰ID
   */
  definitionId: string | number;
  /**
   * æµç¨‹KEY
   */
  processKey: string;
  /**
   * æµç¨‹ç‰ˆæœ¬
   */
  version?: string | number;
  /**
   * å¤‡æ³¨
   */
  remark: string;
  /**
   * è¡¨å•管理
   */
  wfFormManageVo: FormManageVO;
}
export interface DefinitionConfigForm extends BaseEntity {
  /**
   * ä¸»é”®
   */
  id?: string | number;
  /**
   * è¡¨å
   */
  tableName?: string;
  /**
   * æµç¨‹å®šä¹‰ID
   */
  definitionId?: string | number;
  /**
   * æµç¨‹KEY
   */
  processKey?: string;
  /**
   * æµç¨‹ç‰ˆæœ¬
   */
  version?: string | number;
  /**
   * å¤‡æ³¨
   */
  remark?: string;
  /**
   * è¡¨å•管理
   */
  wfFormManageVo?: FormManageVO;
}
export interface DefinitionConfigQuery extends PageQuery {
  /**
   * è¡¨å
   */
  tableName?: string;
  /**
   * æµç¨‹å®šä¹‰ID
   */
  definitionId?: string | number;
  /**
   * æµç¨‹KEY
   */
  processKey?: string;
  /**
   * æµç¨‹ç‰ˆæœ¬
   */
  version?: string | number;
  /**
   * è¡¨å•管理
   */
  wfFormManageVo: FormManageVO;
}
src/api/workflow/formManage/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { FormManageVO, FormManageForm, FormManageQuery } from '@/api/workflow/formManage/types';
/**
 * æŸ¥è¯¢è¡¨å•管理列表
 * @param query
 * @returns {*}
 */
export const listFormManage = (query?: FormManageQuery): AxiosPromise<FormManageVO[]> => {
  return request({
    url: '/workflow/formManage/list',
    method: 'get',
    params: query
  });
};
/**
 * æŸ¥è¯¢è¡¨å•管理列表
 * @param query
 * @returns {*}
 */
export const selectListFormManage = (): AxiosPromise<FormManageVO[]> => {
  return request({
    url: '/workflow/formManage/list/selectList',
    method: 'get',
  });
};
/**
 * æŸ¥è¯¢è¡¨å•管理详细
 * @param id
 */
export const getFormManage = (id: string | number): AxiosPromise<FormManageVO> => {
  return request({
    url: '/workflow/formManage/' + id,
    method: 'get'
  });
};
/**
 * æ–°å¢žè¡¨å•管理
 * @param data
 */
export const addFormManage = (data: FormManageForm) => {
  return request({
    url: '/workflow/formManage',
    method: 'post',
    data: data
  });
};
/**
 * ä¿®æ”¹è¡¨å•管理
 * @param data
 */
export const updateFormManage = (data: FormManageForm) => {
  return request({
    url: '/workflow/formManage',
    method: 'put',
    data: data
  });
};
/**
 * åˆ é™¤è¡¨å•管理
 * @param id
 */
export const delFormManage = (id: string | number | Array<string | number>) => {
  return request({
    url: '/workflow/formManage/' + id,
    method: 'delete'
  });
};
src/api/workflow/formManage/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
export interface FormManageVO {
  /**
   * ä¸»é”®
   */
  id: string | number;
  /**
   * è¡¨å•名称
   */
  formName: string;
  /**
   * è¡¨å•类型
   */
  formType: string;
  /**
   * è¡¨å•类型名称
   */
  formTypeName: string;
  /**
   * è·¯ç”±åœ°å€/表单ID
   */
  router: string;
  /**
   * å¤‡æ³¨
   */
  remork: string;
}
export interface FormManageForm extends BaseEntity {
  /**
   * ä¸»é”®
   */
  id?: string | number;
  /**
   * è¡¨å•名称
   */
  formName?: string;
  /**
   * è¡¨å•类型
   */
  formType?: string;
  /**
   * è·¯ç”±åœ°å€/表单ID
   */
  router?: string;
  /**
   * å¤‡æ³¨
   */
  remork?: string;
}
export interface FormManageQuery extends PageQuery {
  /**
   * è¡¨å•名称
   */
  formName?: string;
  /**
   * è¡¨å•类型
   */
  formType?: string;
}
src/api/workflow/leave/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { LeaveVO, LeaveQuery, LeaveForm } from '@/api/workflow/leave/types';
/**
 * æŸ¥è¯¢è¯·å‡åˆ—表
 * @param query
 * @returns {*}
 */
export const listLeave = (query?: LeaveQuery): AxiosPromise<LeaveVO[]> => {
  return request({
    url: '/demo/leave/list',
    method: 'get',
    params: query
  });
};
/**
 * æŸ¥è¯¢è¯·å‡è¯¦ç»†
 * @param id
 */
export const getLeave = (id: string | number): AxiosPromise<LeaveVO> => {
  return request({
    url: '/demo/leave/' + id,
    method: 'get'
  });
};
/**
 * æ–°å¢žè¯·å‡
 * @param data
 */
export const addLeave = (data: LeaveForm): AxiosPromise<LeaveVO> => {
  return request({
    url: '/demo/leave',
    method: 'post',
    data: data
  });
};
/**
 * ä¿®æ”¹è¯·å‡
 * @param data
 */
export const updateLeave = (data: LeaveForm): AxiosPromise<LeaveVO> => {
  return request({
    url: '/demo/leave',
    method: 'put',
    data: data
  });
};
/**
 * åˆ é™¤è¯·å‡
 * @param id
 */
export const delLeave = (id: string | number | Array<string | number>) => {
  return request({
    url: '/demo/leave/' + id,
    method: 'delete'
  });
};
src/api/workflow/leave/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
export interface LeaveVO {
  id: string | number;
  leaveType: string;
  startDate: string;
  endDate: string;
  leaveDays: number;
  remark: string;
  processInstanceVo: any;
}
export interface LeaveForm extends BaseEntity {
  id?: string | number;
  leaveType?: string;
  startDate?: string;
  endDate?: string;
  leaveDays?: number;
  remark?: string;
  processInstanceVo?: any;
}
export interface LeaveQuery extends PageQuery {
  startLeaveDays?: number;
  endLeaveDays?: number;
}
src/api/workflow/model/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { ModelForm, ModelQuery, ModelVO } from '@/api/workflow/model/types';
/**
 * æŸ¥è¯¢æ¨¡åž‹åˆ—表
 * @param query
 * @returns {*}
 */
export const listModel = (query: ModelQuery): AxiosPromise<ModelVO[]> => {
  return request({
    url: '/workflow/model/list',
    method: 'get',
    params: query
  });
};
/**
 * æŸ¥è¯¢æ¨¡åž‹ä¿¡æ¯
 * @param query
 * @returns {*}
 */
export const getInfo = (id: string): AxiosPromise<ModelForm> => {
  return request({
    url: '/workflow/model/getInfo/'+id,
    method: 'get'
  });
};
/**
 * æ–°å¢žæ¨¡åž‹
 * @param data
 * @returns {*}
 */
export const addModel = (data: ModelForm): AxiosPromise<void> => {
  return request({
    url: '/workflow/model/save',
    method: 'post',
    data: data
  });
};
/**
 * ä¿®æ”¹æ¨¡åž‹ä¿¡æ¯
 * @param data
 * @returns {*}
 */
export function update(data: ModelForm): AxiosPromise<void> {
  return request({
    url: '/workflow/model/update',
    method: 'put',
    data: data
  });
}
/**
 * ä¿®æ”¹æ¨¡åž‹ä¿¡æ¯
 * @param data
 * @returns {*}
 */
export function editModelXml(data: ModelForm): AxiosPromise<void> {
  return request({
    url: '/workflow/model/editModelXml',
    method: 'put',
    data: data
  });
}
/**
 * æŒ‰id删除模型
 * @returns {*}
 * @param id æ¨¡åž‹id
 */
export function delModel(id: string | string[]): AxiosPromise<void> {
  return request({
    url: '/workflow/model/' + id,
    method: 'delete'
  });
}
/**
 * æ¨¡åž‹éƒ¨ç½²
 * @returns {*}
 * @param id æ¨¡åž‹id
 */
export const modelDeploy = (id: string): AxiosPromise<void> => {
  return request({
    url: `/workflow/model/modelDeploy/${id}`,
    method: 'post'
  });
};
/**
 * å¤åˆ¶æ¨¡åž‹
 * @param data
 * @returns {*}
 */
export const copyModel = (data: ModelForm): AxiosPromise<void> => {
  return request({
    url: '/workflow/model/copyModel',
    method: 'post',
    data: data
  });
};
src/api/workflow/model/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
export interface ModelForm {
  id: string,
  name: string;
  key: string;
  categoryCode: string;
  xml:string,
  svg:string,
  description: string;
}
export interface ModelQuery extends PageQuery {
  name?: string;
  key?: string;
  categoryCode?: string;
}
export interface OriginalPersistentState {
  metaInfo: string;
  editorSourceValueId: string;
  createTime: string;
  deploymentId?: string;
  name: string;
  tenantId: string;
  category?: string;
  version: number;
  editorSourceExtraValueId?: string;
  key: string;
  lastUpdateTime: string;
}
export interface PersistentState {
  metaInfo: string;
  editorSourceValueId: string;
  createTime: string;
  deploymentId?: string;
  name: string;
  tenantId: string;
  category?: string;
  version: number;
  editorSourceExtraValueId?: string;
  key: string;
  lastUpdateTime: string;
}
export interface ModelVO {
  id: string;
  revision: number;
  originalPersistentState: OriginalPersistentState;
  name: string;
  key: string;
  category?: string;
  createTime: string;
  lastUpdateTime: string;
  version: number;
  metaInfo: string;
  deploymentId?: string;
  editorSourceValueId: string;
  editorSourceExtraValueId?: string;
  tenantId: string;
  persistentState: PersistentState;
  revisionNext: number;
  idPrefix: string;
  inserted: boolean;
  updated: boolean;
  deleted: boolean;
}
src/api/workflow/nodeConfig/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { NodeConfigVO, NodeConfigForm, NodeConfigQuery } from '@/api/workflow/nodeConfig/types';
/**
 * æŸ¥è¯¢èŠ‚ç‚¹é…ç½®åˆ—è¡¨
 * @param query
 * @returns {*}
 */
export const listNodeConfig = (query?: NodeConfigQuery): AxiosPromise<NodeConfigVO[]> => {
  return request({
    url: '/workflow/nodeConfig/list',
    method: 'get',
    params: query
  });
};
/**
 * æŸ¥è¯¢èŠ‚ç‚¹é…ç½®è¯¦ç»†
 * @param id
 */
export const getNodeConfig = (id: string | number): AxiosPromise<NodeConfigVO> => {
  return request({
    url: '/workflow/nodeConfig/' + id,
    method: 'get'
  });
};
/**
 * æ–°å¢žèŠ‚ç‚¹é…ç½®
 * @param data
 */
export const addNodeConfig = (data: NodeConfigForm) => {
  return request({
    url: '/workflow/nodeConfig',
    method: 'post',
    data: data
  });
};
/**
 * ä¿®æ”¹èŠ‚ç‚¹é…ç½®
 * @param data
 */
export const updateNodeConfig = (data: NodeConfigForm) => {
  return request({
    url: '/workflow/nodeConfig',
    method: 'put',
    data: data
  });
};
/**
 * åˆ é™¤èŠ‚ç‚¹é…ç½®
 * @param id
 */
export const delNodeConfig = (id: string | number | Array<string | number>) => {
  return request({
    url: '/workflow/nodeConfig/' + id,
    method: 'delete'
  });
};
src/api/workflow/nodeConfig/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
import { FormManageVO } from '@/api/workflow/formManage/types';
export interface NodeConfigVO {
  /**
   * ä¸»é”®
   */
  id: string | number;
  /**
   * è¡¨å•id
   */
  formId: string | number;
  /**
   * è¡¨å•类型
   */
  formType: string;
  /**
   * èŠ‚ç‚¹åç§°
   */
  nodeName: string;
  /**
   * èŠ‚ç‚¹id
   */
  nodeId: string | number;
  /**
   * æµç¨‹å®šä¹‰id
   */
  definitionId: string | number;
  /**
   * è¡¨å•管理
   */
  wfFormManageVo: FormManageVO;
}
src/api/workflow/processDefinition/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
import request from '@/utils/request';
import { ProcessDefinitionQuery, ProcessDefinitionVO, definitionXmlVO } from '@/api/workflow/processDefinition/types';
import { AxiosPromise } from 'axios';
/**
 * èŽ·å–æµç¨‹å®šä¹‰åˆ—è¡¨
 * @param query æµç¨‹å®žä¾‹id
 * @returns
 */
export const listProcessDefinition = (query: ProcessDefinitionQuery): AxiosPromise<ProcessDefinitionVO[]> => {
  return request({
    url: `/workflow/processDefinition/list`,
    method: 'get',
    params: query
  });
};
/**
 * æŒ‰ç…§æµç¨‹å®šä¹‰key获取流程定义
 * @param processInstanceId æµç¨‹å®žä¾‹id
 * @returns
 */
export const getListByKey = (key: string) => {
  return request({
    url: `/workflow/processDefinition/getListByKey/${key}`,
    method: 'get'
  });
};
/**
 * é€šè¿‡æµç¨‹å®šä¹‰id获取流程图
 */
export const definitionImage = (processDefinitionId: string): AxiosPromise<any> => {
  return request({
    url: `/workflow/processDefinition/definitionImage/${processDefinitionId}` + '?t' + Math.random(),
    method: 'get'
  });
};
/**
 * é€šè¿‡æµç¨‹å®šä¹‰id获取xml
 * @param processDefinitionId æµç¨‹å®šä¹‰id
 * @returns
 */
export const definitionXml = (processDefinitionId: string): AxiosPromise<definitionXmlVO> => {
  return request({
    url: `/workflow/processDefinition/definitionXml/${processDefinitionId}`,
    method: 'get'
  });
};
/**
 * åˆ é™¤æµç¨‹å®šä¹‰
 * @param deploymentId éƒ¨ç½²id
 * @param processDefinitionId æµç¨‹å®šä¹‰id
 * @returns
 */
export const deleteProcessDefinition = (deploymentId: string | string[], processDefinitionId: string | string[]) => {
  return request({
    url: `/workflow/processDefinition/${deploymentId}/${processDefinitionId}`,
    method: 'delete'
  });
};
/**
 * æŒ‚èµ·/激活
 * @param processDefinitionId æµç¨‹å®šä¹‰id
 * @returns
 */
export const updateDefinitionState = (processDefinitionId: string) => {
  return request({
    url: `/workflow/processDefinition/updateDefinitionState/${processDefinitionId}`,
    method: 'put'
  });
};
/**
 * æµç¨‹å®šä¹‰è½¬æ¢ä¸ºæ¨¡åž‹
 * @param processDefinitionId æµç¨‹å®šä¹‰id
 * @returns
 */
export const convertToModel = (processDefinitionId: string) => {
  return request({
    url: `/workflow/processDefinition/convertToModel/${processDefinitionId}`,
    method: 'put'
  });
};
/**
 * é€šè¿‡zip或xml部署流程定义
 * @returns
 */
export function deployProcessFile(data: any) {
  return request({
    url: '/workflow/processDefinition/deployByFile',
    method: 'post',
    data: data,
    headers: {
      repeatSubmit: false
    }
  });
}
/**
 * è¿ç§»æµç¨‹
 * @param currentProcessDefinitionId
 * @param fromProcessDefinitionId
 * @returns
 */
export const migrationDefinition = (currentProcessDefinitionId: string, fromProcessDefinitionId: string) => {
  return request({
    url: `/workflow/processDefinition/migrationDefinition/${currentProcessDefinitionId}/${fromProcessDefinitionId}`,
    method: 'put'
  });
};
src/api/workflow/processDefinition/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
import { DefinitionConfigVO } from '@/api/workflow/definitionConfig/types';
export interface ProcessDefinitionQuery extends PageQuery {
  key?: string;
  name?: string;
  categoryCode?: string;
}
export interface ProcessDefinitionVO extends BaseEntity {
  id: string;
  name: string;
  key: string;
  version: number;
  suspensionState: number;
  resourceName: string;
  diagramResourceName: string;
  deploymentId: string;
  deploymentTime: string;
  wfDefinitionConfigVo: DefinitionConfigVO;
}
export interface definitionXmlVO {
  xml: string[];
  xmlStr: string;
}
src/api/workflow/processInstance/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,136 @@
import request from '@/utils/request';
import { ProcessInstanceQuery, ProcessInstanceVO } from '@/api/workflow/processInstance/types';
import { AxiosPromise } from 'axios';
/**
 * æŸ¥è¯¢è¿è¡Œä¸­å®žä¾‹åˆ—表
 * @param query
 * @returns {*}
 */
export const getPageByRunning = (query: ProcessInstanceQuery): AxiosPromise<ProcessInstanceVO[]> => {
  return request({
    url: '/workflow/processInstance/getPageByRunning',
    method: 'get',
    params: query
  });
};
/**
 * æŸ¥è¯¢å·²å®Œæˆå®žä¾‹åˆ—表
 * @param query
 * @returns {*}
 */
export const getPageByFinish = (query: ProcessInstanceQuery): AxiosPromise<ProcessInstanceVO[]> => {
  return request({
    url: '/workflow/processInstance/getPageByFinish',
    method: 'get',
    params: query
  });
};
/**
 * é€šè¿‡æµç¨‹å®žä¾‹id获取历史流程图
 */
export const getHistoryImage = (processInstanceId: string) => {
  return request({
    url: `/workflow/processInstance/getHistoryImage/${processInstanceId}` + '?t' + Math.random(),
    method: 'get'
  });
};
/**
 * é€šè¿‡æµç¨‹å®žä¾‹id获取历史流程图运行中,历史等节点
 */
export const getHistoryList = (instanceId: string): AxiosPromise<Record<string, any>> => {
  return request({
    url: `/workflow/processInstance/getHistoryList/${instanceId}` + '?t' + Math.random(),
    method: 'get'
  });
};
/**
 * èŽ·å–å®¡æ‰¹è®°å½•
 * @param processInstanceId æµç¨‹å®žä¾‹id
 * @returns
 */
export const getHistoryRecord = (processInstanceId: string) => {
  return request({
    url: `/workflow/processInstance/getHistoryRecord/${processInstanceId}`,
    method: 'get'
  });
};
/**
 * ä½œåºŸ
 * @param data å‚æ•°
 * @returns
 */
export const deleteRunInstance = (data: object) => {
  return request({
    url: `/workflow/processInstance/deleteRunInstance`,
    method: 'post',
    data: data
  });
};
/**
 * è¿è¡Œä¸­çš„实例 åˆ é™¤ç¨‹å®žä¾‹ï¼Œåˆ é™¤åŽ†å²è®°å½•ï¼Œåˆ é™¤ä¸šåŠ¡ä¸Žæµç¨‹å…³è”ä¿¡æ¯
 * @param processInstanceId æµç¨‹å®žä¾‹id
 * @returns
 */
export const deleteRunAndHisInstance = (processInstanceId: string | string[]) => {
  return request({
    url: `/workflow/processInstance/deleteRunAndHisInstance/${processInstanceId}`,
    method: 'delete'
  });
};
/**
 * å·²å®Œæˆçš„实例 åˆ é™¤ç¨‹å®žä¾‹ï¼Œåˆ é™¤åŽ†å²è®°å½•ï¼Œåˆ é™¤ä¸šåŠ¡ä¸Žæµç¨‹å…³è”ä¿¡æ¯
 * @param processInstanceId æµç¨‹å®žä¾‹id
 * @returns
 */
export const deleteFinishAndHisInstance = (processInstanceId: string | string[]) => {
  return request({
    url: `/workflow/processInstance/deleteFinishAndHisInstance/${processInstanceId}`,
    method: 'delete'
  });
};
/**
 * åˆ†é¡µæŸ¥è¯¢å½“前登录人单据
 * @param query
 * @returns {*}
 */
export const getPageByCurrent = (query: ProcessInstanceQuery): AxiosPromise<ProcessInstanceVO[]> => {
  return request({
    url: '/workflow/processInstance/getPageByCurrent',
    method: 'get',
    params: query
  });
};
/**
 * æ’¤é”€æµç¨‹
 * @param processInstanceId æµç¨‹å®žä¾‹id
 * @returns
 */
export const cancelProcessApply = (processInstanceId: string) => {
  return request({
    url: `/workflow/processInstance/cancelProcessApply/${processInstanceId}`,
    method: 'post'
  });
};
export default {
  getPageByRunning,
  getPageByFinish,
  getHistoryImage,
  getHistoryList,
  getHistoryRecord,
  deleteRunInstance,
  deleteRunAndHisInstance,
  deleteFinishAndHisInstance,
  getPageByCurrent,
  cancelProcessApply
};
src/api/workflow/processInstance/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import { TaskVO } from '@/api/workflow/task/types';
export interface ProcessInstanceQuery extends PageQuery {
  categoryCode?: string;
  name?: string;
  key?: string;
  startUserId?: string;
  businessKey?: string;
}
export interface ProcessInstanceVO extends BaseEntity {
  id: string;
  processDefinitionId: string;
  processDefinitionName: string;
  processDefinitionKey: string;
  processDefinitionVersion: string;
  deploymentId: string;
  businessKey: string;
  isSuspended?: any;
  tenantId: string;
  startTime: string;
  endTime?: string;
  startUserId: string;
  businessStatus: string;
  businessStatusName: string;
  taskVoList: TaskVO[];
}
src/api/workflow/task/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { TaskQuery, TaskVO } from '@/api/workflow/task/types';
/**
 * æŸ¥è¯¢å¾…办列表
 * @param query
 * @returns {*}
 */
export const getPageByTaskWait = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
  return request({
    url: '/workflow/task/getPageByTaskWait',
    method: 'get',
    params: query
  });
};
/**
 * æŸ¥è¯¢å·²åŠžåˆ—è¡¨
 * @param query
 * @returns {*}
 */
export const getPageByTaskFinish = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
  return request({
    url: '/workflow/task/getPageByTaskFinish',
    method: 'get',
    params: query
  });
};
/**
 * æŸ¥è¯¢å½“前用户的抄送列表
 * @param query
 * @returns {*}
 */
export const getPageByTaskCopy = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
  return request({
    url: '/workflow/task/getPageByTaskCopy',
    method: 'get',
    params: query
  });
};
/**
 * å½“前租户所有待办任务
 * @param query
 * @returns {*}
 */
export const getPageByAllTaskWait = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
  return request({
    url: '/workflow/task/getPageByAllTaskWait',
    method: 'get',
    params: query
  });
};
/**
 * å½“前租户所有已办任务
 * @param query
 * @returns {*}
 */
export const getPageByAllTaskFinish = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
  return request({
    url: '/workflow/task/getPageByAllTaskFinish',
    method: 'get',
    params: query
  });
};
/**
 * å¯åŠ¨æµç¨‹
 * @param data
 * @returns {*}
 */
export const startWorkFlow = (data: object): any => {
  return request({
    url: '/workflow/task/startWorkFlow',
    method: 'post',
    data: data
  });
};
/**
 * åŠžç†æµç¨‹
 * @param data
 * @returns {*}
 */
export const completeTask = (data: object) => {
  return request({
    url: '/workflow/task/completeTask',
    method: 'post',
    data: data
  });
};
/**
 * è®¤é¢†ä»»åŠ¡
 * @param taskId
 * @returns {*}
 */
export const claim = (taskId: string): any => {
  return request({
    url: '/workflow/task/claim/' + taskId,
    method: 'post'
  });
};
/**
 * å½’还任务
 * @param taskId
 * @returns {*}
 */
export const returnTask = (taskId: string): any => {
  return request({
    url: '/workflow/task/returnTask/' + taskId,
    method: 'post'
  });
};
/**
 * ä»»åŠ¡é©³å›ž
 * @param data
 * @returns {*}
 */
export const backProcess = (data: any): any => {
  return request({
    url: '/workflow/task/backProcess',
    method: 'post',
    data: data
  });
};
/**
 * èŽ·å–å½“å‰ä»»åŠ¡
 * @param taskId
 * @returns
 */
export const getTaskById = (taskId: string) => {
  return request({
    url: '/workflow/task/getTaskById/' + taskId,
    method: 'get'
  });
};
/**
 * åŠ ç­¾
 * @param data
 * @returns
 */
export const addMultiInstanceExecution = (data: any) => {
  return request({
    url: '/workflow/task/addMultiInstanceExecution',
    method: 'post',
    data: data
  });
};
/**
 * å‡ç­¾
 * @param data
 * @returns
 */
export const deleteMultiInstanceExecution = (data: any) => {
  return request({
    url: '/workflow/task/deleteMultiInstanceExecution',
    method: 'post',
    data: data
  });
};
/**
 * ä¿®æ”¹ä»»åŠ¡åŠžç†äºº
 * @param taskIds
 * @param userId
 * @returns
 */
export const updateAssignee = (taskIds: Array<string>, userId: string) => {
  return request({
    url: `/workflow/task/updateAssignee/${taskIds}/${userId}`,
    method: 'put'
  });
};
/**
 * è½¬åŠžä»»åŠ¡
 * @returns
 */
export const transferTask = (data: any) => {
  return request({
    url: `/workflow/task/transferTask`,
    method: 'post',
    data: data
  });
};
/**
 * ç»ˆæ­¢ä»»åŠ¡
 * @returns
 */
export const terminationTask = (data: any) => {
  return request({
    url: `/workflow/task/terminationTask`,
    method: 'post',
    data: data
  });
};
/**
 * æŸ¥è¯¢æµç¨‹å˜é‡
 * @returns
 */
export const getInstanceVariable = (taskId: string) => {
  return request({
    url: `/workflow/task/getInstanceVariable/${taskId}`,
    method: 'get'
  });
};
/**
 * èŽ·å–å¯é©³å›žå¾—ä»»åŠ¡èŠ‚ç‚¹
 * @returns
 */
export const getTaskNodeList = (processInstanceId: string) => {
  return request({
    url: `/workflow/task/getTaskNodeList/${processInstanceId}`,
    method: 'get'
  });
};
/**
 * å§”托任务
 * @returns
 */
export const delegateTask = (data: any) => {
  return request({
    url: `/workflow/task/delegateTask`,
    method: 'post',
    data: data
  });
};
/**
 * æŸ¥è¯¢å·¥ä½œæµä»»åŠ¡ç”¨æˆ·é€‰æ‹©åŠ ç­¾äººå‘˜
 * @param taskId
 * @returns {*}
 */
export const getTaskUserIdsByAddMultiInstance = (taskId: string) => {
  return request({
    url: '/workflow/task/getTaskUserIdsByAddMultiInstance/' + taskId,
    method: 'get'
  });
};
/**
 * æŸ¥è¯¢å·¥ä½œæµé€‰æ‹©å‡ç­¾äººå‘˜
 * @param taskId
 * @returns {*}
 */
export const getListByDeleteMultiInstance = (taskId: string) => {
  return request({
    url: '/workflow/task/getListByDeleteMultiInstance/' + taskId,
    method: 'get'
  });
};
src/api/workflow/task/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
import { NodeConfigVO } from '@/api/workflow/nodeConfig/types';
import { DefinitionConfigVO } from '@/api/workflow/definitionConfig/types';
export interface TaskQuery extends PageQuery {
  name?: string;
  processDefinitionKey?: string;
  processDefinitionName?: string;
}
export interface ParticipantVo {
  groupIds?: string[] | number[];
  candidate: string[] | number[];
  candidateName: string[];
  claim: boolean;
}
export interface TaskVO extends BaseEntity {
  id: string;
  name: string;
  description?: string;
  priority: number;
  owner?: string;
  assignee?: string | number;
  assigneeName?: string;
  processInstanceId: string;
  executionId: string;
  taskDefinitionId?: any;
  processDefinitionId: string;
  endTime?: string;
  taskDefinitionKey: string;
  dueDate?: string;
  category?: any;
  parentTaskId?: any;
  tenantId: string;
  claimTime?: string;
  businessStatus?: string;
  businessStatusName?: string;
  processDefinitionName?: string;
  processDefinitionKey?: string;
  participantVo?: ParticipantVo;
  multiInstance?: boolean;
  businessKey?: string;
  wfNodeConfigVo?: NodeConfigVO;
  wfDefinitionConfigVo?: DefinitionConfigVO;
}
export interface VariableVo {
  key: string;
  value: string;
}
src/api/workflow/workflowCommon/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
export default {
    routerJump(routerJumpVo: RouterJumpVo,proxy){
        if (routerJumpVo.wfNodeConfigVo && routerJumpVo.wfNodeConfigVo.formType === 'static' && routerJumpVo.wfNodeConfigVo.wfFormManageVo) {
            proxy.$tab.closePage(proxy.$route);
            proxy.$router.push({
                path: `${routerJumpVo.wfNodeConfigVo.wfFormManageVo.router}`,
                query: {
                    id: routerJumpVo.businessKey,
                    type: routerJumpVo.type,
                    taskId: routerJumpVo.taskId
                }
            });
        } else if (routerJumpVo.wfNodeConfigVo && routerJumpVo.wfNodeConfigVo.formType === 'dynamic' && routerJumpVo.wfNodeConfigVo.wfFormManageVo) {
            proxy.$tab.closePage(proxy.$route);
            proxy.$router.push({
                path: `${routerJumpVo.wfNodeConfigVo.wfFormManageVo.router}`,
                query: {
                    id: routerJumpVo.businessKey,
                    type: routerJumpVo.type,
                    taskId: routerJumpVo.taskId
                }
            });
        }else {
            proxy?.$modal.msgError('请到模型配置菜单!');
        }
    }
}
src/api/workflow/workflowCommon/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
import { NodeConfigVO } from '@/api/workflow/nodeConfig/types';
import { DefinitionConfigVO } from '@/api/workflow/definitionConfig/types';
export interface RouterJumpVo {
  wfNodeConfigVo: NodeConfigVO;
  wfDefinitionConfigVo: DefinitionConfigVO;
  businessKey: string;
  taskId: string;
  type: string;
}
export interface StartProcessBo {
  businessKey: string | number;
  tableName: string;
  variables: any;
}
src/assets/icons/svg/caret-back.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M321.94 98L158.82 237.78a24 24 0 000 36.44L321.94 414c15.57 13.34 39.62 2.28 39.62-18.22v-279.6c0-20.5-24.05-31.56-39.62-18.18z"/></svg>
src/assets/icons/svg/caret-forward.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M190.06 414l163.12-139.78a24 24 0 000-36.44L190.06 98c-15.57-13.34-39.62-2.28-39.62 18.22v279.6c0 20.5 24.05 31.56 39.62 18.18z"/></svg>
src/assets/icons/svg/category.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg t="1715954426124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3305" width="200" height="200"><path d="M664.081597 1023.943114a78.246037 78.246037 0 0 1-78.985549-76.795456v-284.996471a78.27448 78.27448 0 0 1 78.985549-76.93767h280.843828A78.189152 78.189152 0 0 1 1023.939417 662.151187v284.996471a78.246037 78.246037 0 0 1-79.013992 76.795456z m-585.067605 0a78.246037 78.246037 0 0 1-78.985549-76.795456v-284.996471a78.160709 78.160709 0 0 1 78.985549-76.93767h280.786942a78.302923 78.302923 0 0 1 79.042434 76.93767v284.996471h-0.170656a78.246037 78.246037 0 0 1-78.985549 76.795456z m0-585.096048a78.217594 78.217594 0 0 1-78.985549-76.93767V76.912925a78.189152 78.189152 0 0 1 78.957106-76.795456h280.786942a78.27448 78.27448 0 0 1 79.042435 76.93767v284.996471a78.27448 78.27448 0 0 1-79.013992 76.795456z m589.675333-5.688552a77.193655 77.193655 0 0 1-77.990052-75.885288V75.888985a77.25054 77.25054 0 0 1 77.990052-75.942173h277.26004a77.25054 77.25054 0 0 1 77.961609 75.942173v281.384241a77.421197 77.421197 0 0 1-78.132266 75.885288z" p-id="3306" fill="currentColor"></path></svg>
src/assets/icons/svg/finish.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg t="1716006237008" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12400" width="200" height="200"><path d="M738.826039 1005.166431c-150.226824 0-272.00251-121.916235-272.00251-272.303686 0-150.407529 121.775686-272.323765 272.00251-272.323765 150.206745 0 271.982431 121.916235 271.982432 272.323765 0 150.387451-121.775686 272.303686-271.982432 272.303686z m-0.040157-508.225255c-128.582275 0-232.789333 104.347608-232.789333 233.09051s104.207059 233.110588 232.789333 233.110589c128.562196 0 232.769255-104.367686 232.769255-233.110589 0-128.742902-104.207059-233.09051-232.769255-233.09051z m10.561255 318.243138s-3.694431 3.674353-7.408941 3.674353a18.010353 18.010353 0 0 1-25.941333 0l-74.10949-80.916079a17.66902 17.66902 0 0 1 0-25.740549c7.408941-7.368784 22.246902-7.368784 25.941333 0l63.006118 69.872941 129.686588-117.699764a18.010353 18.010353 0 0 1 25.941333 0 17.709176 17.709176 0 0 1 0 25.760627L749.347137 815.184314zM391.529412 682.666667H190.745098a20.078431 20.078431 0 0 1 0-40.156863h200.784314a20.078431 20.078431 0 1 1 0 40.156863zM170.666667 261.019608a20.078431 20.078431 0 0 1 20.078431-20.078432h481.882353a20.078431 20.078431 0 0 1 0 40.156863H190.745098a20.078431 20.078431 0 0 1-20.078431-20.078431z m341.333333 200.784314H190.745098a20.078431 20.078431 0 0 1 0-40.156863h321.254902a20.078431 20.078431 0 0 1 0 40.156863zM813.176471 120.470588a80.313725 80.313725 0 0 0-80.313726-80.313725H130.509804a80.313725 80.313725 0 0 0-80.313726 80.313725v762.980392a80.313725 80.313725 0 0 0 80.313726 80.313726h366.832941a346.112 346.112 0 0 0 40.417882 40.779294H130.509804a120.470588 120.470588 0 0 1-120.470588-120.470588V120.470588a120.470588 120.470588 0 0 1 120.470588-120.470588h602.352941a120.470588 120.470588 0 0 1 120.470588 120.470588v293.667137a340.188863 340.188863 0 0 0-40.156862-8.533333V120.470588z" fill="currentColor" p-id="12401"></path></svg>
src/assets/icons/svg/model.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg t="1715953291934" class="icon" viewBox="0 0 1061 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1715" id="mx_n_1715953291935" width="200" height="200"><path d="M447.122465 467.332105L49.240301 268.161564A33.501036 33.501036 0 0 0 0.136043 300.744763v441.020484a33.042117 33.042117 0 0 0 16.06214 27.994016L413.162511 1018.034062a33.959954 33.959954 0 0 0 17.438895 5.50702 33.042117 33.042117 0 0 0 33.042117-33.042118V497.161795a33.042117 33.042117 0 0 0-17.438895-29.82969zM398.018207 931.298504l-331.339011-208.348907v-367.134638l331.339011 162.915996zM1046.010843 263.572381a33.042117 33.042117 0 0 0-31.665363 0L550.838 467.332105a33.042117 33.042117 0 0 0-19.733487 30.288608v493.33717a33.042117 33.042117 0 0 0 49.563176 28.452934l463.048562-265.254776a33.042117 33.042117 0 0 0 16.521059-28.452934V291.566398a33.042117 33.042117 0 0 0-14.685386-27.994017z m-50.939931 441.020484L596.72983 931.298504v-413.026468l397.882163-176.224626zM991.399565 178.672496a33.042117 33.042117 0 0 0-22.486996-29.829689L550.838 1.530034a32.583199 32.583199 0 0 0-19.733487 0L83.659173 158.021173a33.042117 33.042117 0 0 0-4.130264 61.036134l397.882163 199.170541a33.042117 33.042117 0 0 0 14.685386 3.212428 33.959954 33.959954 0 0 0 13.30863 0l463.966399-205.595398a33.042117 33.042117 0 0 0 22.028078-37.172382zM494.391049 349.849021L180.490934 195.193555l358.874108-125.743613 328.126583 112.434982z m0 0" fill="currentColor" p-id="1716"></path></svg>
src/assets/icons/svg/my-copy.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg t="1716006583362" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="38505" width="200" height="200"><path d="M733.696 666.624l56.32-65.536-15.36-12.8c-41.472-34.816-87.552-61.44-136.192-79.36 75.264-49.152 124.928-134.144 124.928-230.912 0-152.576-123.904-276.48-276.48-276.48-74.24 0-143.872 28.672-195.584 80.384-52.224 51.712-80.896 121.344-80.896 195.584 0 92.16 45.568 174.08 115.2 224.256-81.408 26.624-156.672 74.752-215.552 144.896C34.304 736.768-4.096 850.944 0.512 968.192l1.024 20.48 86.528-4.608-1.024-19.968c-4.096-96.256 27.136-188.928 88.576-261.12 136.704-162.816 380.416-184.32 543.232-48.64l14.848 12.288zM296.96 278.016c0-106.496 83.456-189.952 189.952-189.952 104.96 0 189.952 84.992 189.952 189.952 0 106.496-83.456 189.952-189.952 189.952S296.96 384.512 296.96 278.016z m690.688 522.24H802.304c13.824-16.896 32.256-38.4 55.808-67.072 7.68-8.192 11.776-19.456 10.752-31.744-1.024-11.776-6.144-22.528-15.36-29.696-8.192-7.68-19.456-11.264-31.232-10.752-12.288 1.024-23.04 6.656-30.208 15.872-38.4 45.568-96.256 114.176-101.376 119.808-7.68 7.68-10.752 15.36-13.312 22.528-4.096 8.704-4.096 16.384-4.096 24.064 0 5.632 0 12.8 3.584 23.04 2.56 7.68 6.144 15.872 13.824 23.552l104.96 124.416 4.096 2.048c9.216 4.096 18.432 6.144 26.624 6.144 8.704 0 21.504-4.096 28.672-11.776 8.704-8.704 13.824-19.968 14.336-31.744 0-10.752-3.584-20.48-11.264-28.16l-54.272-63.488h183.296c19.456 0 35.84-18.944 36.352-43.008v-0.512c0.512-25.088-14.848-43.52-35.84-43.52z" fill="currentColor" p-id="38506"></path></svg>
src/assets/icons/svg/my-task.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg t="1715953932254" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11266" width="200" height="200"><path d="M955.59576334 565.84816928C921.71190561 470.40769794 828.48968821 401.08719229 717.79871055 401.08719229c-111.30100029 0-204.91141024 70.09689085-238.29616047 166.36920831h-253.43576475c-26.23088067 0-47.5261393 24.84446618-47.5261329 55.45640711s21.2952586 55.4564006 47.5261329 55.45640054h242.73267482c6.54385326 41.3150179 23.95716606 79.02537397 49.30074631 110.91280755h-292.03342113c-26.23088067 0-47.5261393 24.84446618-47.5261329 55.45640061s21.2952586 55.4564006 47.5261329 55.45640059h214.0617167a376.93717152 376.93717152 0 0 0-33.44021086 110.91280754h-292.47706801a46.36155474 46.36155474 0 0 1-45.91790151-46.8052016V169.1685161c0-25.06629616 21.23980441-45.36333956 47.35977009-45.3633331h63.38666838v55.45640059c0 50.07713168 67.6013528 55.4564006 110.91280122 55.45640061 43.31144833 0 88.50841878 5.76746809 88.5084187-55.45640061v-55.45640059h44.53149358v55.45640059c0 61.22386864 38.15400297 55.4564006 88.84115713 55.45640061 50.74260854 0 88.56387296 5.76746809 88.56387308-55.45640061v-55.45640059h44.19875511v55.45640059c0 61.22386864 38.43128719 55.4564006 89.11844137 55.45640061s110.91280771-2.82827659 110.91280118-55.45640061v-55.45640059h63.44212904a47.69250853 47.69250853 0 0 1 47.74796275 47.63704778l-0.22182994 394.4059385zM407.96379074 345.63079175h-181.95245955c-26.23088067 0-47.5261393 24.84446618-47.52613946 55.45640054s21.2952586 55.4564006 47.52613946 55.45640711h181.95245955c26.23088067 0 47.5261393-24.84446618 47.52613268-55.45640711s-21.2952586-55.4564006-47.52613268-55.45640054z m325.75090957-166.36920816c-30.61193437 0-55.4564006-18.63335139-55.45640712-41.59230208v-83.18460405c0-22.95895071 24.84446618-41.59230206 55.45640712-41.592302s55.4564006 18.63335139 55.4564006 41.592302v83.18460405c0 22.95895071-24.84446618 41.59230206-55.4564006 41.59230208z m-222.71291552 0c-30.61193437 0-55.4009463-18.63335139-55.4009464-41.59230208v-83.18460405c0-22.95895071 24.84446618-41.59230206 55.4009464-41.592302 30.61193437 0 55.4564006 18.63335139 55.45640054 41.592302v83.18460405c-0.0554542 22.95895071-24.84446618 41.59230206-55.45640054 41.59230208z m-220.6610244 0c-30.61193437 0-55.4009463-18.63335139-55.40094634-41.59230208v-83.18460405c0-22.95895071 24.84446618-41.59230206 55.40094634-41.592302 30.61193437 0 55.4564006 18.63335139 55.45640063 41.592302v83.18460405c0 22.95895071-24.84446618 41.59230206-55.45640063 41.59230208z m443.31847922 665.92048622c-91.28124099 0-165.53736215-74.69977454-165.53736218-166.59103817 0-91.83580289 74.2561211-166.59103164 165.53736218-166.59103164s165.53736215 74.75522879 165.53736218 166.59103164-74.2561211 166.59103164-165.53736218 166.59103817z m-115.73750824-29.33644132c32.1647114 24.45627358 71.98241282 39.59587158 115.68205395 39.59587144 44.14329437 0 84.34918863-15.47233629 116.68026924-40.3722568a235.13514545 235.13514545 0 0 1 105.14533952 195.98292729h-443.6512175c0-80.35632762 41.92504052-153.94697186 106.14355479-195.20654193z" fill="currentColor" p-id="11267"></path></svg>
src/assets/icons/svg/process-definition.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg t="1716005059256" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3268" width="200" height="200"><path d="M497.798958 952.225272A345.95419 345.95419 0 0 1 359.273733 892.652247a171.541601 171.541601 0 0 0 7.177473-47.37132A179.436821 179.436821 0 0 0 211.417793 665.126359a345.95419 345.95419 0 0 1 185.896546-380.40606 179.436821 179.436821 0 0 0 317.244299 0 351.696169 351.696169 0 0 1 143.549456 128.476763 35.169617 35.169617 0 0 0 30.145386 16.508188 37.322859 37.322859 0 0 0 19.379177-5.024231 36.605111 36.605111 0 0 0 10.766209-46.653574 424.188644 424.188644 0 0 0-183.743304-160.775391v-13.637198a180.872315 180.872315 0 1 0-358.873642 0 129.194511 129.194511 0 0 0 0 15.79044A423.470897 423.470897 0 0 0 132.465591 600.529103a467.253481 467.253481 0 0 0 6.459726 71.774728A180.154568 180.154568 0 0 0 187.014385 1024a178.001326 178.001326 0 0 0 139.96072-68.185992 430.64837 430.64837 0 0 0 158.62215 62.444014h6.459725a35.887364 35.887364 0 0 0 5.741978-71.774729z m57.419783-861.29674a109.097587 109.097587 0 1 1-108.37984 110.533082A109.097587 109.097587 0 0 1 555.218741 90.928532zM187.014385 952.225272a109.097587 109.097587 0 1 1 108.37984-108.37984A109.097587 109.097587 0 0 1 187.014385 952.225272zM933.471559 617.755038l-104.791103-71.774728a35.887364 35.887364 0 0 0-48.089068 8.612967L560.242972 858.918125a37.322859 37.322859 0 0 0-6.459725 24.403408l9.330714 104.073356a35.169617 35.169617 0 0 0 14.354946 25.838902 38.758353 38.758353 0 0 0 21.532418 7.177473h7.177473l98.331378-21.532419a38.758353 38.758353 0 0 0 22.250166-14.354946L945.673263 665.126359a36.605111 36.605111 0 0 0 5.741978-27.274397 37.322859 37.322859 0 0 0-17.943682-20.096924zM675.800285 930.692853l-45.218079 9.330715-4.306484-49.524563 193.791766-262.695505 45.218079 29.427638z m311.50232-399.067489l-103.355608-66.750497a35.887364 35.887364 0 0 0-49.524563 10.048462 35.169617 35.169617 0 0 0 10.766209 49.524562L947.826505 593.35163a34.45187 34.45187 0 0 0 20.096924 5.741979 37.322859 37.322859 0 0 0 30.145386-15.790441 36.605111 36.605111 0 0 0-10.76621-51.677804z" fill="currentColor" p-id="3269"></path></svg>
src/assets/icons/svg/topiam.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_446_540)">
<path d="M113.069 160.072C103.717 170.743 93.0453 180.216 81.5345 188.609C61.5105 174.46 44.3642 156.595 30.9349 135.971C23.5009 124.46 17.2659 112.11 12.4697 99.0407C9.592 91.3668 7.19392 83.3332 5.27545 75.2996C2.03803 61.3907 0.359375 47.0022 0.359375 32.1341C0.359375 30.6953 0.359375 29.1365 0.359375 27.6977C6.35459 23.9806 12.7095 20.7432 19.0644 17.7456C20.7431 32.1341 24.1004 46.043 28.8966 59.3524C31.6544 66.9063 34.7719 74.3404 38.4889 81.4147C44.604 93.5251 52.0381 104.796 60.4314 115.228C75.1796 133.093 92.9254 148.321 113.069 160.072Z" fill="url(#paint0_linear_446_540)"/>
<path d="M196.643 67.6256C195.084 76.3786 192.926 84.8918 190.168 93.1652C178.897 91.1269 167.266 90.0477 155.276 90.0477C154.197 90.0477 153.118 90.0477 152.039 90.0477C126.859 90.4074 102.878 95.6832 80.9352 105.036C72.302 94.8439 64.868 83.453 58.9927 71.3427C81.6546 61.8702 106.475 56.7144 132.614 56.7144C141.487 56.7144 150.24 57.3139 158.753 58.5129C171.823 60.1916 184.533 63.3091 196.643 67.6256Z" fill="url(#paint1_linear_446_540)"/>
<path d="M199.64 34.0528C199.64 39.2087 199.401 44.3646 199.041 49.4005C186.691 44.1247 173.621 40.048 160.072 37.53C148.321 35.2518 136.211 34.0528 123.981 34.0528C97.7218 34.0528 72.6619 39.3286 49.88 48.9209C42.6858 51.9185 35.7313 55.3958 29.0167 59.2327C24.2205 46.0432 20.8632 32.0144 19.1846 17.6259C26.6186 14.1487 34.2925 11.271 42.2062 8.75301C60.3117 3.11751 79.4964 0 99.4005 0C119.904 0 139.568 3.23741 158.153 9.11272C172.782 13.789 186.691 20.2638 199.52 28.1775C199.64 30.2159 199.64 32.1343 199.64 34.0528Z" fill="url(#paint2_linear_446_540)"/>
<path d="M190.168 93.2855C182.494 116.547 170.384 137.65 154.796 155.875C149.76 161.751 144.364 167.386 138.609 172.542C126.858 183.214 113.789 192.446 99.7601 200C93.4052 196.523 87.41 192.686 81.5347 188.609C93.0455 180.336 103.717 170.744 113.069 160.072C117.866 154.676 122.302 148.921 126.499 143.046C137.65 127.098 146.403 109.233 152.158 90.1679C153.237 90.1679 154.316 90.1679 155.396 90.1679C167.146 90.048 178.777 91.1272 190.168 93.2855Z" fill="url(#paint3_linear_446_540)"/>
</g>
<defs>
<linearGradient id="paint0_linear_446_540" x1="15.8569" y1="27.5782" x2="86.4712" y2="182.06" gradientUnits="userSpaceOnUse">
<stop stop-color="#57A4F7"/>
<stop offset="1" stop-color="#2158F9"/>
</linearGradient>
<linearGradient id="paint1_linear_446_540" x1="58.9501" y1="80.8427" x2="196.648" y2="80.8427" gradientUnits="userSpaceOnUse">
<stop stop-color="#2158F9"/>
<stop offset="1" stop-color="#33E1E5"/>
</linearGradient>
<linearGradient id="paint2_linear_446_540" x1="19.1564" y1="29.6353" x2="199.647" y2="29.6353" gradientUnits="userSpaceOnUse">
<stop stop-color="#255DF9"/>
<stop offset="1" stop-color="#7C35BA"/>
</linearGradient>
<linearGradient id="paint3_linear_446_540" x1="95.3808" y1="192.567" x2="174.674" y2="97.4815" gradientUnits="userSpaceOnUse">
<stop stop-color="#54A0F7"/>
<stop offset="1" stop-color="#2158F9"/>
</linearGradient>
<clipPath id="clip0_446_540">
<rect width="200" height="200" fill="white"/>
</clipPath>
</defs>
</svg>
src/assets/icons/svg/waiting.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg t="1716005941920" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6808" width="200" height="200"><path d="M739.555556 512a256 256 0 1 1 0 512 256 256 0 0 1 0-512z m18.887111-512a180.167111 180.167111 0 0 1 179.882666 169.870222l0.284445 10.24v311.068445a28.444444 28.444444 0 0 1-56.433778 5.12l-0.455111-5.12V180.110222a123.278222 123.278222 0 0 0-114.460445-122.936889l-8.817777-0.341333H209.237333a123.278222 123.278222 0 0 0-122.993777 114.460444l-0.284445 8.817778v662.641778c0 65.080889 50.460444 118.385778 114.460445 122.88l8.817777 0.341333h283.875556a28.444444 28.444444 0 0 1 5.12 56.433778l-5.12 0.455111h-283.875556a180.167111 180.167111 0 0 1-179.882666-169.927111l-0.284445-10.24V180.167111A180.167111 180.167111 0 0 1 198.997333 0.227556L209.237333 0h549.205334zM739.555556 568.888889a199.111111 199.111111 0 1 0 0 398.222222 199.111111 199.111111 0 0 0 0-398.222222z m115.712 314.026667a14.222222 14.222222 0 0 1 0 28.444444h-222.890667a14.222222 14.222222 0 0 1 0-28.444444h222.890667z m-45.738667-227.555556a74.126222 74.126222 0 0 1-37.660445 95.459556v24.234666c0 6.257778 5.12 11.377778 11.377778 11.377778h51.313778c19.057778-0.170667 34.645333 16.042667 34.929778 36.295111v25.486222a11.377778 11.377778 0 0 1-11.377778 11.377778h-228.579556a11.377778 11.377778 0 0 1-11.377777-11.377778v-25.486222c0.512-20.48 16.213333-36.693333 35.271111-36.295111h51.143111a11.377778 11.377778 0 0 0 11.377778-11.377778v-24.291555c-16.440889-7.793778-32.426667-21.674667-39.253334-38.570667a73.500444 73.500444 0 0 1 36.295111-95.459556c35.328-16.497778 81.123556 0.967111 96.540445 38.684445zM360.789333 682.666667a28.444444 28.444444 0 0 1 5.12 56.433777l-5.12 0.455112H199.111111a28.444444 28.444444 0 0 1-5.12-56.433778L199.111111 682.666667h161.678222z m113.777778-227.555556a28.444444 28.444444 0 0 1 5.12 56.433778L474.510222 512H199.111111a28.444444 28.444444 0 0 1-5.12-56.433778L199.111111 455.111111h275.456zM768 227.555556a28.444444 28.444444 0 0 1 5.12 56.433777L768 284.444444H199.111111a28.444444 28.444444 0 0 1-5.12-56.433777L199.111111 227.555556h568.888889z" fill="currentColor" p-id="6809"></path></svg>
src/assets/icons/svg/workflow.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg t="1716004936483" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2712" width="200" height="200"><path d="M1024.99477 113.778v227.555a57.458 57.458 0 0 1-58.027 56.89H734.86277a57.458 57.458 0 0 1-58.027-56.89v-56.889H560.83877v455.112h115.996v-56.89a57.458 57.458 0 0 1 58.027-56.888h231.936a57.458 57.458 0 0 1 58.197 56.889v227.555a57.458 57.458 0 0 1-58.027 56.89H734.86277a57.458 57.458 0 0 1-58.027-56.89v-56.889H502.86877a57.458 57.458 0 0 1-58.027-56.889V568.89L274.51677 735.972a46.763 46.763 0 0 1-65.252 0l-195.754-192a44.658 44.658 0 0 1 0-64l195.754-192.057a46.763 46.763 0 0 1 65.252 0L445.01277 455.11V227.556a57.458 57.458 0 0 1 58.027-56.89h173.966v-56.888a57.458 57.458 0 0 1 58.026-56.89h231.936a57.458 57.458 0 0 1 58.027 56.89z" fill="currentColor" p-id="2713"></path></svg>
src/assets/styles/element-ui.scss
@@ -1,4 +1,15 @@
// cover some element-ui styles
.el-collapse {
  .collapse__title {
    font-weight: 600;
    padding: 0 8px;
    font-size: 1.2em;
    line-height: 1.1em;
  }
  .el-collapse-item__content {
    padding: 0 8px;
  }
}
.el-divider--horizontal {
  margin-bottom: 10px;
@@ -68,6 +79,12 @@
      .el-dialog__body {
        padding: 15px !important;
      }
      .el-dialog__header {
        padding: 16px 16px 8px 16px;
        box-sizing: border-box;
        border-bottom: 1px solid var(--brder-color);
        margin-right: 0;
      }
    }
  }
}
@@ -114,3 +131,19 @@
.el-dropdown .el-dropdown-link {
  color: var(--el-color-primary) !important;
}
/* å½“ el-form çš„ inline å±žæ€§ä¸º true æ—¶ */
/* è®¾ç½® label çš„宽度默认为 68px */
.el-form--inline .el-form-item__label {
  width: 68px;
}
/* è®¾ç½® el-select çš„宽度默认为 240px */
.el-form--inline .el-select {
  width: 240px;
}
/* è®¾ç½® el-input çš„宽度默认为 240px */
.el-form--inline .el-input {
  width: 240px;
}
src/assets/styles/index.scss
@@ -14,7 +14,14 @@
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
  font-family:
    Helvetica Neue,
    Helvetica,
    PingFang SC,
    Hiragino Sans GB,
    Microsoft YaHei,
    Arial,
    sans-serif;
}
label {
src/assets/styles/sidebar.scss
@@ -28,7 +28,10 @@
    // reset element-ui css
    .horizontal-collapse-transition {
      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
      transition:
        0s width ease-in-out,
        0s padding-left ease-in-out,
        0s padding-right ease-in-out;
    }
    .scrollbar-wrapper {
@@ -105,7 +108,6 @@
        background-color: rgba(0, 0, 0, 0.1) !important;
      }
    }
    & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .theme-dark .el-sub-menu .el-menu-item {
src/assets/styles/variables.module.scss
@@ -13,6 +13,14 @@
  --fixedHeaderBg: #ffffff;
  --tableHeaderBg: #f8f8f9;
  --tableHeaderTextColor: #515a6e;
  // å·¥ä½œæµ
  --bpmn-panel-border: #eeeeee;
  --bpmn-panel-box-shadow: #cccccc;
  --bpmn-panel-bar-background-color: #f5f7fa;
  // ele
  --brder-color: #e8e8e8
}
html.dark {
  --menuBg: #1d1e1f;
@@ -33,6 +41,26 @@
  .el-tree-node__content {
    --el-color-primary-light-9: #262727;
  }
  // vxe-table ä¸»é¢˜
  --vxe-font-color: #98989E;
  --vxe-primary-color: #2C7ECF;
  --vxe-icon-background-color: #98989E;
  --vxe-table-font-color: #98989E;
  --vxe-table-resizable-color: #95969a;
  --vxe-table-header-background-color: #28282A;
  --vxe-table-body-background-color: #151518;
  --vxe-table-background-color: #4a5663;
  --vxe-table-border-width: 1px;
  --vxe-table-border-color: #37373A;
  --vxe-toolbar-background-color: #37373A;
  // å·¥ä½œæµ
  --bpmn-panel-border: #37373A;
  --bpmn-panel-box-shadow: #37373A;
  --bpmn-panel-bar-background-color: #37373A;
  // ele
  --brder-color: #37373A
}
// base color
src/bpmn/assets/defaultXML.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
function generateRandomValue() {
  // ç”Ÿæˆä¸€ä¸ªéšæœºæ•°
  const randomValue = Math.random().toString(36).slice(2, 12);
  return `Process_${randomValue}`;
}
const cartage: string = 'default';
export default `<?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:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:bioc="http://bpmn.io/schema/bpmn/biocolor/1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://www.flowable.org/processdef">
  <process id="process_${generateRandomValue()}" name="name_${generateRandomValue()}">
    <startEvent id="startNode1" name="开始" />
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_flow">
    <bpmndi:BPMNPlane id="BPMNPlane_flow" bpmnElement="T-2d89e7a3-ba79-4abd-9f64-ea59621c258c">
      <bpmndi:BPMNShape id="BPMNShape_startNode1" bpmnElement="startNode1" bioc:stroke="">
        <omgdc:Bounds x="240" y="200" width="30" height="30" />
        <bpmndi:BPMNLabel>
          <omgdc:Bounds x="242" y="237" width="23" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>`;
src/bpmn/assets/lang/zh.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
export const NodeName = {
  'bpmn:Process': '流程',
  'bpmn:StartEvent': '开始事件',
  'bpmn:IntermediateThrowEvent': '中间事件',
  'bpmn:Task': '任务',
  'bpmn:SendTask': '发送任务',
  'bpmn:ReceiveTask': '接收任务',
  'bpmn:UserTask': '用户任务',
  'bpmn:ManualTask': '手工任务',
  'bpmn:BusinessRuleTask': '业务规则任务',
  'bpmn:ServiceTask': '服务任务',
  'bpmn:ScriptTask': '脚本任务',
  'bpmn:EndEvent': '结束事件',
  'bpmn:SequenceFlow': '流程线',
  'bpmn:ExclusiveGateway': '互斥网关',
  'bpmn:ParallelGateway': '并行网关',
  'bpmn:InclusiveGateway': '相容网关',
  'bpmn:ComplexGateway': '复杂网关',
  'bpmn:EventBasedGateway': '事件网关',
  'bpmn:Participant': 'æ± /参与者',
  'bpmn:SubProcess': '子流程',
  'bpmn:DataObjectReference': '数据对象引用',
  'bpmn:DataStoreReference': '数据存储引用',
  'bpmn:Group': '组'
};
export default {
  'Activate hand tool': '启动手动工具',
  'Activate lasso tool': '启动 Lasso å·¥å…·',
  'Activate create/remove space tool': '启动创建/删除空间工具',
  'Activate global connect tool': '启动全局连接工具',
  'Ad-hoc': 'Ad-hoc',
  'Add lane above': '在上方添加泳道',
  'Add lane below': '在下方添加泳道',
  'Business rule task': '规则任务',
  'Call activity': '引用流程',
  'Compensation end event': '结束补偿事件',
  'Compensation intermediate throw event': '中间补偿抛出事件',
  'Complex gateway': '复杂网关',
  'Conditional intermediate catch event': '中间条件捕获事件',
  'Conditional start event (non-interrupting)': '条件启动事件 (非中断)',
  'Conditional start event': '条件启动事件',
  'Connect using association': '文本关联',
  'Connect using sequence/message flow or association': '消息关联',
  'Change element': '更改元素',
  'Change type': '更改类型',
  'Create data object reference': '创建数据对象引用',
  'Create data store reference': '创建数据存储引用',
  'Create expanded sub-process': '创建可折叠子流程',
  'Create pool/participant': '创建池/参与者',
  'Collection': '集合',
  'Connect using data input association': '数据输入关联',
  'Data store reference': '数据存储引用',
  'Data object reference': '数据对象引用',
  'Divide into two lanes': '分成两个泳道',
  'Divide into three lanes': '分成三个泳道',
  'End event': '结束事件',
  'Error end event': '结束错误事件',
  'Escalation end event': '结束升级事件',
  'Escalation intermediate throw event': '中间升级抛出事件',
  'Event sub-process': '事件子流程',
  'Event-based gateway': '事件网关',
  'Exclusive gateway': '互斥网关',
  'Empty pool/participant (removes content)': '清空池/参与者 (删除内容)',
  'Empty pool/participant': '清空池/参与者',
  'Expanded pool/participant': '展开池/参与者',
  'Inclusive gateway': '相容网关',
  'Intermediate throw event': '中间抛出事件',
  'Loop': '循环',
  'Link intermediate catch event': '中间链接捕获事件',
  'Link intermediate throw event': '中间链接抛出事件',
  'Manual task': '手动任务',
  'Message end event': '结束消息事件',
  'Message intermediate catch event': '中间消息捕获事件',
  'Message intermediate throw event': '中间消息抛出事件',
  'Message start event': '消息启动事件',
  'Parallel gateway': '并行网关',
  'Parallel multi-instance': '并行多实例',
  'Participant multiplicity': '参与者多重性',
  'Receive task': '接受任务',
  'Remove': '移除',
  'Script task': '脚本任务',
  'Send task': '发送任务',
  'Sequential multi-instance': '串行多实例',
  'Service task': '服务任务',
  'Signal end event': '结束信号事件',
  'Signal intermediate catch event': '中间信号捕获事件',
  'Signal intermediate throw event': '中间信号抛出事件',
  'Signal start event (non-interrupting)': '信号启动事件 (非中断)',
  'Signal start event': '信号启动事件',
  'Start event': '开始事件',
  'Sub-process (collapsed)': '可折叠子流程',
  'Sub-process (expanded)': '可展开子流程',
  'Sub rocess': '子流程',
  'Task': '任务',
  'Transaction': '事务',
  'Terminate end event': '终止边界事件',
  'Timer intermediate catch event': '中间定时捕获事件',
  'Timer start event (non-interrupting)': '定时启动事件 (非中断)',
  'Timer start event': '定时启动事件',
  'User task': '用户任务',
  'Create start event': '创建开始事件',
  'Create gateway': '创建网关',
  'Create intermediate/boundary event': '创建中间/边界事件',
  'Create end event': '创建结束事件',
  'Create group': '创建组',
  'Create startEvent': '开始节点',
  'Create endEvent': '结束节点',
  'Create exclusiveGateway': '互斥网关',
  'Create parallelGateway': '并行网关',
  'Create task': '任务节点',
  'Create userTask': '用户任务节点',
  'Condition type': '条件类型',
  'Append end event': '追加结束事件节点',
  'Append gateway': '追加网关节点',
  'Append task': '追加任务',
  'Append user task': '追加用户任务节点',
  'Append text annotation': '追加文本注释',
  'Append intermediate/boundary event': '追加中间或边界事件',
  'Append receive task': '追加接收任务节点',
  'Append message intermediate catch event': '追加中间消息捕获事件',
  'Append timer intermediate catch event': '追加中间定时捕获事件',
  'Append conditional intermediate catch event': '追加中间条件捕获事件',
  'Append signal intermediate catch event': '追加中间信号捕获事件',
  'flow elements must be children of pools/participants': '流程元素必须是池/参与者的子元素'
};
src/bpmn/assets/moddle/flowable.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1250 @@
export default {
  'name': 'Flowable',
  'uri': 'http://flowable.org/bpmn',
  'prefix': 'flowable',
  'xml': {
    'tagAlias': 'lowerCase'
  },
  'associations': [],
  'types': [
    {
      'name': 'flowable:extCandidateUsers',
      'isAbstract': true,
      'extends': [],
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['*']
      },
      'properties': [
        {
          'name': 'body',
          'type': 'String',
          'isBody': true
        }
      ]
    },
    {
      'name': 'flowable:extAssignee',
      'isAbstract': true,
      'extends': [],
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['*']
      },
      'properties': [
        {
          'name': 'body',
          'type': 'String',
          'isBody': true
        }
      ]
    },
    {
      'name': 'flowable:property',
      'superClass': ['Element'],
      'properties': [
        {
          'name': 'id',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'name',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'value',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'flowable:properties',
      'isAbstract': true,
      'extends': [],
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['*']
      },
      'properties': [
        {
          'name': 'values',
          'type': 'flowable:property',
          'isMany': true
        }
      ]
    },
    {
      'name': 'InOutBinding',
      'superClass': ['Element'],
      'isAbstract': true,
      'properties': [
        {
          'name': 'source',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'sourceExpression',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'target',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'businessKey',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'local',
          'isAttr': true,
          'type': 'Boolean',
          'default': false
        },
        {
          'name': 'variables',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'In',
      'superClass': ['InOutBinding'],
      'meta': {
        'allowedIn': ['bpmn:CallActivity']
      }
    },
    {
      'name': 'Out',
      'superClass': ['InOutBinding'],
      'meta': {
        'allowedIn': ['bpmn:CallActivity']
      }
    },
    {
      'name': 'AsyncCapable',
      'isAbstract': true,
      'extends': ['bpmn:Activity', 'bpmn:Gateway', 'bpmn:Event'],
      'properties': [
        {
          'name': 'async',
          'isAttr': true,
          'type': 'Boolean',
          'default': false
        },
        {
          'name': 'asyncBefore',
          'isAttr': true,
          'type': 'Boolean',
          'default': false
        },
        {
          'name': 'asyncAfter',
          'isAttr': true,
          'type': 'Boolean',
          'default': false
        },
        {
          'name': 'exclusive',
          'isAttr': true,
          'type': 'Boolean',
          'default': true
        }
      ]
    },
    {
      'name': 'flowable:in',
      'superClass': ['Element'],
      'properties': [
        {
          'name': 'source',
          'type': 'string',
          'isAttr': true
        },
        {
          'name': 'target',
          'type': 'string',
          'isAttr': true
        }
      ]
    },
    {
      'name': 'flowable:out',
      'superClass': ['Element'],
      'properties': [
        {
          'name': 'source',
          'type': 'string',
          'isAttr': true
        },
        {
          'name': 'target',
          'type': 'string',
          'isAttr': true
        }
      ]
    },
    {
      'name': 'BoundaryEvent',
      'superClass': ['CatchEvent'],
      'properties': [
        {
          'name': 'cancelActivity',
          'default': true,
          'isAttr': true,
          'type': 'Boolean'
        },
        {
          'name': 'attachedToRef',
          'type': 'Activity',
          'isAttr': true,
          'isReference': true
        }
      ]
    },
    {
      'name': 'JobPriorized',
      'isAbstract': true,
      'extends': ['bpmn:Process', 'flowable:AsyncCapable'],
      'properties': [
        {
          'name': 'jobPriority',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'SignalEventDefinition',
      'isAbstract': true,
      'extends': ['bpmn:SignalEventDefinition'],
      'properties': [
        {
          'name': 'async',
          'isAttr': true,
          'type': 'Boolean',
          'default': false
        }
      ]
    },
    {
      'name': 'ErrorEventDefinition',
      'isAbstract': true,
      'extends': ['bpmn:ErrorEventDefinition'],
      'properties': [
        {
          'name': 'errorCodeVariable',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'errorMessageVariable',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'Error',
      'isAbstract': true,
      'extends': ['bpmn:Error'],
      'properties': [
        {
          'name': 'flowable:errorMessage',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'PotentialStarter',
      'superClass': ['Element'],
      'properties': [
        {
          'name': 'resourceAssignmentExpression',
          'type': 'bpmn:ResourceAssignmentExpression'
        }
      ]
    },
    {
      'name': 'UserTask',
      'isAbstract': true,
      'extends': ['bpmn:UserTask'],
      'properties': [
        {
          'name': 'timerEventDefinition',
          'type': 'Expression'
        },
        {
          'name': 'multiInstanceLoopCharacteristics',
          'type': 'MultiInstanceLoopCharacteristics'
        }
      ]
    },
    {
      'name': 'StartEvent',
      'isAbstract': true,
      'extends': ['bpmn:StartEvent'],
      'properties': [
        {
          'name': 'timerEventDefinition',
          'type': 'Expression'
        }
      ]
    },
    {
      'name': 'FormSupported',
      'isAbstract': true,
      'extends': ['bpmn:StartEvent', 'bpmn:UserTask'],
      'properties': [
        {
          'name': 'formHandlerClass',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'formKey',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'TemplateSupported',
      'isAbstract': true,
      'extends': ['bpmn:Process', 'bpmn:FlowElement'],
      'properties': [
        {
          'name': 'modelerTemplate',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'Initiator',
      'isAbstract': true,
      'extends': ['bpmn:StartEvent'],
      'properties': [
        {
          'name': 'initiator',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'ScriptTask',
      'isAbstract': true,
      'extends': ['bpmn:ScriptTask'],
      'properties': [
        {
          'name': 'resultVariable',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'resource',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'Process',
      'isAbstract': true,
      'extends': ['bpmn:Process'],
      'properties': [
        {
          'name': 'candidateStarterGroups',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'candidateStarterUsers',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'versionTag',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'historyTimeToLive',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'isStartableInTasklist',
          'isAttr': true,
          'type': 'Boolean',
          'default': true
        }
      ]
    },
    {
      'name': 'EscalationEventDefinition',
      'isAbstract': true,
      'extends': ['bpmn:EscalationEventDefinition'],
      'properties': [
        {
          'name': 'escalationCodeVariable',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'FormalExpression',
      'isAbstract': true,
      'extends': ['bpmn:FormalExpression'],
      'properties': [
        {
          'name': 'resource',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'Assignable',
      'extends': ['bpmn:UserTask'],
      'properties': [
        {
          'name': 'candidateGroups',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'dueDate',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'followUpDate',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'priority',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'CallActivity',
      'extends': ['bpmn:CallActivity'],
      'properties': [
        {
          'name': 'calledElementBinding',
          'isAttr': true,
          'type': 'String',
          'default': 'latest'
        },
        {
          'name': 'calledElementVersion',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'calledElementVersionTag',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'calledElementTenantId',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'caseRef',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'caseBinding',
          'isAttr': true,
          'type': 'String',
          'default': 'latest'
        },
        {
          'name': 'caseVersion',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'caseTenantId',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'variableMappingClass',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'variableMappingDelegateExpression',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'ServiceTaskLike',
      'extends': ['bpmn:ServiceTask', 'bpmn:BusinessRuleTask', 'bpmn:SendTask', 'bpmn:MessageEventDefinition'],
      'properties': [
        {
          'name': 'expression',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'class',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'delegateExpression',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'resultVariable',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'ExclusiveGateway',
      'isAbstract': true,
      'extends': ['bpmn:ExclusiveGateway'],
      'properties': [
        {
          'name': 'serviceClass',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'DmnCapable',
      'extends': ['bpmn:BusinessRuleTask'],
      'properties': [
        {
          'name': 'decisionRef',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'decisionRefBinding',
          'isAttr': true,
          'type': 'String',
          'default': 'latest'
        },
        {
          'name': 'decisionRefVersion',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'mapDecisionResult',
          'isAttr': true,
          'type': 'String',
          'default': 'resultList'
        },
        {
          'name': 'decisionRefTenantId',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'ExternalCapable',
      'extends': ['flowable:ServiceTaskLike'],
      'properties': [
        {
          'name': 'type',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'topic',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'TaskPriorized',
      'extends': ['bpmn:Process', 'flowable:ExternalCapable'],
      'properties': [
        {
          'name': 'taskPriority',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'Properties',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['*']
      },
      'properties': [
        {
          'name': 'values',
          'type': 'Property',
          'isMany': true
        }
      ]
    },
    {
      'name': 'Property',
      'superClass': ['Element'],
      'properties': [
        {
          'name': 'id',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'name',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'value',
          'type': 'String',
          'isAttr': true
        }
      ]
    },
    {
      'name': 'Connector',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['flowable:ServiceTaskLike']
      },
      'properties': [
        {
          'name': 'inputOutput',
          'type': 'InputOutput'
        },
        {
          'name': 'connectorId',
          'type': 'String'
        }
      ]
    },
    {
      'name': 'InputOutput',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['bpmn:FlowNode', 'flowable:Connector']
      },
      'properties': [
        {
          'name': 'inputOutput',
          'type': 'InputOutput'
        },
        {
          'name': 'connectorId',
          'type': 'String'
        },
        {
          'name': 'inputParameters',
          'isMany': true,
          'type': 'InputParameter'
        },
        {
          'name': 'outputParameters',
          'isMany': true,
          'type': 'OutputParameter'
        }
      ]
    },
    {
      'name': 'InputOutputParameter',
      'properties': [
        {
          'name': 'name',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'value',
          'isBody': true,
          'type': 'String'
        },
        {
          'name': 'definition',
          'type': 'InputOutputParameterDefinition'
        }
      ]
    },
    {
      'name': 'InputOutputParameterDefinition',
      'isAbstract': true
    },
    {
      'name': 'List',
      'superClass': ['InputOutputParameterDefinition'],
      'properties': [
        {
          'name': 'items',
          'isMany': true,
          'type': 'InputOutputParameterDefinition'
        }
      ]
    },
    {
      'name': 'Map',
      'superClass': ['InputOutputParameterDefinition'],
      'properties': [
        {
          'name': 'entries',
          'isMany': true,
          'type': 'Entry'
        }
      ]
    },
    {
      'name': 'Entry',
      'properties': [
        {
          'name': 'key',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'value',
          'isBody': true,
          'type': 'String'
        },
        {
          'name': 'definition',
          'type': 'InputOutputParameterDefinition'
        }
      ]
    },
    {
      'name': 'Value',
      'superClass': ['InputOutputParameterDefinition'],
      'properties': [
        {
          'name': 'id',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'name',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'value',
          'isBody': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'Script',
      'superClass': ['InputOutputParameterDefinition'],
      'properties': [
        {
          'name': 'scriptFormat',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'resource',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'value',
          'isBody': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'Field',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['flowable:ServiceTaskLike', 'flowable:ExecutionListener', 'flowable:TaskListener']
      },
      'properties': [
        {
          'name': 'name',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'expression',
          'isAttr': true,
          'type': 'expression'
        },
        {
          'name': 'string',
          'type': 'string'
        },
        {
          'name': 'stringValue',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'string',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['flowable:Field']
      },
      'properties': [
        {
          'name': 'body',
          'isBody': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'expression',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['flowable:Field']
      },
      'properties': [
        {
          'name': 'body',
          'isBody': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'InputParameter',
      'superClass': ['InputOutputParameter']
    },
    {
      'name': 'OutputParameter',
      'superClass': ['InputOutputParameter']
    },
    {
      'name': 'Collectable',
      'isAbstract': true,
      'extends': ['bpmn:MultiInstanceLoopCharacteristics'],
      'superClass': ['flowable:AsyncCapable'],
      'properties': [
        {
          'name': 'collection',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'elementVariable',
          'isAttr': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'SequenceFlow',
      'superClass': ['FlowElement'],
      'properties': [
        {
          'name': 'isImmediate',
          'isAttr': true,
          'type': 'Boolean'
        },
        {
          'name': 'conditionExpression',
          'type': 'Expression'
        },
        {
          'name': 'sourceRef',
          'type': 'FlowNode',
          'isAttr': true,
          'isReference': true
        },
        {
          'name': 'targetRef',
          'type': 'FlowNode',
          'isAttr': true,
          'isReference': true
        }
      ]
    },
    {
      'name': 'MultiInstanceLoopCharacteristics',
      'superClass': ['LoopCharacteristics'],
      'properties': [
        {
          'name': 'isSequential',
          'default': false,
          'isAttr': true,
          'type': 'Boolean'
        },
        {
          'name': 'behavior',
          'type': 'MultiInstanceBehavior',
          'default': 'All',
          'isAttr': true
        },
        {
          'name': 'loopCardinality',
          'type': 'Expression',
          'xml': {
            'serialize': 'xsi:type'
          }
        },
        {
          'name': 'loopDataInputRef',
          'type': 'ItemAwareElement',
          'isReference': true
        },
        {
          'name': 'loopDataOutputRef',
          'type': 'ItemAwareElement',
          'isReference': true
        },
        {
          'name': 'inputDataItem',
          'type': 'DataInput',
          'xml': {
            'serialize': 'property'
          }
        },
        {
          'name': 'outputDataItem',
          'type': 'DataOutput',
          'xml': {
            'serialize': 'property'
          }
        },
        {
          'name': 'complexBehaviorDefinition',
          'type': 'ComplexBehaviorDefinition',
          'isMany': true
        },
        {
          'name': 'completionCondition',
          'type': 'Expression',
          'xml': {
            'serialize': 'xsi:type'
          }
        },
        {
          'name': 'oneBehaviorEventRef',
          'type': 'EventDefinition',
          'isAttr': true,
          'isReference': true
        },
        {
          'name': 'noneBehaviorEventRef',
          'type': 'EventDefinition',
          'isAttr': true,
          'isReference': true
        }
      ]
    },
    {
      'name': 'FailedJobRetryTimeCycle',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['flowable:AsyncCapable', 'bpmn:MultiInstanceLoopCharacteristics']
      },
      'properties': [
        {
          'name': 'body',
          'isBody': true,
          'type': 'String'
        }
      ]
    },
    {
      'name': 'ExecutionListener',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': [
          'bpmn:Task',
          'bpmn:ServiceTask',
          'bpmn:UserTask',
          'bpmn:BusinessRuleTask',
          'bpmn:ScriptTask',
          'bpmn:ReceiveTask',
          'bpmn:ManualTask',
          'bpmn:ExclusiveGateway',
          'bpmn:SequenceFlow',
          'bpmn:ParallelGateway',
          'bpmn:InclusiveGateway',
          'bpmn:EventBasedGateway',
          'bpmn:StartEvent',
          'bpmn:IntermediateCatchEvent',
          'bpmn:IntermediateThrowEvent',
          'bpmn:EndEvent',
          'bpmn:BoundaryEvent',
          'bpmn:CallActivity',
          'bpmn:SubProcess',
          'bpmn:Process'
        ]
      },
      'properties': [
        {
          'name': 'expression',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'class',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'delegateExpression',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'event',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'script',
          'type': 'Script'
        },
        {
          'name': 'fields',
          'type': 'Field',
          'isMany': true
        }
      ]
    },
    {
      'name': 'TaskListener',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['bpmn:UserTask']
      },
      'properties': [
        {
          'name': 'expression',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'class',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'delegateExpression',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'event',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'script',
          'type': 'Script'
        },
        {
          'name': 'fields',
          'type': 'Field',
          'isMany': true
        }
      ]
    },
    {
      'name': 'FormProperty',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['bpmn:StartEvent', 'bpmn:UserTask']
      },
      'properties': [
        {
          'name': 'id',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'name',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'type',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'required',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'readable',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'writable',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'variable',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'expression',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'datePattern',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'default',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'values',
          'type': 'Value',
          'isMany': true
        }
      ]
    },
    {
      'name': 'FormData',
      'superClass': ['Element'],
      'meta': {
        'allowedIn': ['bpmn:StartEvent', 'bpmn:UserTask']
      },
      'properties': [
        {
          'name': 'fields',
          'type': 'FormField',
          'isMany': true
        },
        {
          'name': 'businessKey',
          'type': 'String',
          'isAttr': true
        }
      ]
    },
    {
      'name': 'FormField',
      'superClass': ['Element'],
      'properties': [
        {
          'name': 'id',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'label',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'type',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'datePattern',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'defaultValue',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'properties',
          'type': 'Properties'
        },
        {
          'name': 'validation',
          'type': 'Validation'
        },
        {
          'name': 'values',
          'type': 'Value',
          'isMany': true
        }
      ]
    },
    {
      'name': 'Validation',
      'superClass': ['Element'],
      'properties': [
        {
          'name': 'constraints',
          'type': 'Constraint',
          'isMany': true
        }
      ]
    },
    {
      'name': 'Constraint',
      'superClass': ['Element'],
      'properties': [
        {
          'name': 'name',
          'type': 'String',
          'isAttr': true
        },
        {
          'name': 'config',
          'type': 'String',
          'isAttr': true
        }
      ]
    },
    {
      'name': 'ConditionalEventDefinition',
      'isAbstract': true,
      'extends': ['bpmn:ConditionalEventDefinition'],
      'properties': [
        {
          'name': 'variableName',
          'isAttr': true,
          'type': 'String'
        },
        {
          'name': 'variableEvent',
          'isAttr': true,
          'type': 'String'
        }
      ]
    }
  ],
  'emumerations': []
};
src/bpmn/assets/module/ContextPad/CustomContextPadProvider.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,138 @@
import ContextPadProvider from 'bpmn-js/lib/features/context-pad/ContextPadProvider';
import { Injector } from 'didi';
import EventBus from 'diagram-js/lib/core/EventBus';
import ContextPad from 'diagram-js/lib/features/context-pad/ContextPad';
import Modeling from 'bpmn-js/lib/features/modeling/Modeling.js';
import ElementFactory from 'bpmn-js/lib/features/modeling/ElementFactory';
import Connect from 'diagram-js/lib/features/connect/Connect';
import Create from 'diagram-js/lib/features/create/Create';
import PopupMenu from 'diagram-js/lib/features/popup-menu/PopupMenu';
import Canvas from 'diagram-js/lib/core/Canvas';
import Rules from 'diagram-js/lib/features/rules/Rules';
import { Element, Shape } from 'diagram-js/lib/model/Types';
import BpmnFactory from 'bpmn-js/lib/features/modeling/BpmnFactory';
import modeler from '@/store/modules/modeler';
// @Description: å¢žå¼ºå…ƒç´ è¿žçº¿äº‹ä»¶
class CustomContextPadProvider extends ContextPadProvider {
  private _contextPad: ContextPad;
  private _modeling: Modeling;
  private _elementFactory: ElementFactory;
  private _autoPlace: any;
  private _connect: Connect;
  private _create: Create;
  private _popupMenu: PopupMenu;
  private _canvas: Canvas;
  private _rules: Rules;
  constructor(
    config: any,
    injector: Injector,
    eventBus: EventBus,
    contextPad: ContextPad,
    modeling: Modeling,
    elementFactory: ElementFactory,
    connect: Connect,
    create: Create,
    popupMenu: PopupMenu,
    canvas: Canvas,
    rules: Rules,
    translate
  ) {
    // @ts-ignore
    super(config, injector, eventBus, contextPad, modeling, elementFactory, connect, create, popupMenu, canvas, rules, translate);
    this._contextPad = contextPad;
    this._modeling = modeling;
    this._elementFactory = elementFactory;
    this._connect = connect;
    this._create = create;
    this._popupMenu = popupMenu;
    this._canvas = canvas;
    this._rules = rules;
    this._autoPlace = injector.get('autoPlace', false);
  }
  getContextPadEntries(element: Element) {
    const actions: Record<string, any> = {};
    const appendUserTask = (event: Event, element: Shape) => {
      const shape = this._elementFactory.createShape({ type: 'bpmn:UserTask' });
      this._create.start(event, shape, {
        source: element
      });
    };
    const appendMultiInstanceUserTask = (event: Event, element: Shape) => {
      const store = modeler();
      const bpmnFactory = store.getModeler().get('bpmnFactory') as BpmnFactory;
      const businessObject = bpmnFactory.create('bpmn:UserTask', {
        // name: '多实例用户任务',
        isForCompensation: false
      });
      businessObject.loopCharacteristics = bpmnFactory.create('bpmn:MultiInstanceLoopCharacteristics');
      // åˆ›å»º Shape
      const shape = this._elementFactory.createShape({
        type: 'bpmn:UserTask',
        businessObject: businessObject
      });
      this._create.start(event, shape, { source: element });
    };
    const appendTask = this._autoPlace
      ? (event, element) => {
          const bpmnFactory: BpmnFactory | undefined = modeler().getModeler().get('bpmnFactory');
          const businessObject = bpmnFactory.create('bpmn:UserTask', {
            // name: '多实例用户任务',// å³é”®åˆ›å»ºæ˜¾ç¤º
            isForCompensation: false
          });
          // åˆ›å»ºå¤šå®žä¾‹å±žæ€§å¹¶åˆ†é…ç»™ç”¨æˆ·ä»»åŠ¡çš„ loopCharacteristics
          businessObject.loopCharacteristics = bpmnFactory.create('bpmn:MultiInstanceLoopCharacteristics');
          // åˆ›å»º Shape
          const shape = this._elementFactory.createShape({
            type: 'bpmn:UserTask',
            businessObject: businessObject
          });
          this._autoPlace.append(element, shape);
        }
      : appendMultiInstanceUserTask;
    const append = this._autoPlace
      ? (event: Event, element: Shape) => {
          const shape = this._elementFactory.createShape({ type: 'bpmn:UserTask' });
          this._autoPlace.append(element, shape);
        }
      : appendUserTask;
    // // æ·»åŠ åˆ›å»ºç”¨æˆ·ä»»åŠ¡æŒ‰é’®
    actions['append.append-user-task'] = {
      group: 'model',
      className: 'bpmn-icon-user-task',
      title: '用户任务',
      action: {
        dragstart: appendUserTask,
        click: append
      }
    };
    // æ·»åŠ åˆ›å»ºå¤šå®žä¾‹ç”¨æˆ·ä»»åŠ¡æŒ‰é’®
    actions['append.append-multi-instance-user-task'] = {
      group: 'model',
      className: 'bpmn-icon-user', // ä½ å¯ä»¥ä½¿ç”¨å¤šå®žä¾‹ç”¨æˆ·ä»»åŠ¡çš„å›¾æ ‡  bpmn-icon-user   bpmn-icon-user-task
      title: '多实例用户任务',
      action: {
        dragstart: appendMultiInstanceUserTask,
        click: appendTask
      }
    };
    return actions;
  }
}
export default CustomContextPadProvider;
src/bpmn/assets/module/Palette/CustomPaletteProvider.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
import { assign } from 'min-dash';
import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider';
import ElementFactory from 'bpmn-js/lib/features/modeling/ElementFactory';
import Create from 'diagram-js/lib/features/create/Create';
import SpaceTool from 'diagram-js/lib/features/space-tool/SpaceTool';
import LassoTool from 'diagram-js/lib/features/lasso-tool/LassoTool';
import HandTool from 'diagram-js/lib/features/hand-tool/HandTool';
import GlobalConnect from 'diagram-js/lib/features/global-connect/GlobalConnect';
import Palette from 'diagram-js/lib/features/palette/Palette';
import modeler from '@/store/modules/modeler';
import BpmnFactory from 'bpmn-js/lib/features/modeling/BpmnFactory';
// @Description: å¢žå¼ºå·¦ä¾§é¢æ¿
class CustomPaletteProvider extends PaletteProvider {
  private readonly _palette: Palette;
  private readonly _create: Create;
  private readonly _elementFactory: ElementFactory;
  private readonly _spaceTool: SpaceTool;
  private readonly _lassoTool: LassoTool;
  private readonly _handTool: HandTool;
  private readonly _globalConnect: GlobalConnect;
  private readonly _translate: any;
  constructor(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, translate) {
    super(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, translate);
    this._palette = palette;
    this._create = create;
    this._elementFactory = elementFactory;
    this._spaceTool = spaceTool;
    this._lassoTool = lassoTool;
    this._handTool = handTool;
    this._globalConnect = globalConnect;
    this._translate = translate;
  }
  getPaletteEntries() {
    const actions = {},
      create = this._create,
      elementFactory = this._elementFactory,
      translate = this._translate;
    function createAction(type: string, group: string, className: string, title: string, options?: object) {
      function createListener(event) {
        const shape = elementFactory.createShape(assign({ type: type }, options));
        if (options) {
          !shape.businessObject.di && (shape.businessObject.di = {});
          shape.businessObject.di.isExpanded = (options as { [key: string]: any }).isExpanded;
        }
        create.start(event, shape, null);
      }
      const shortType = type.replace(/^bpmn:/, '');
      return {
        group: group,
        className: className,
        title: title || translate('Create {type}', { type: shortType }),
        action: {
          dragstart: createListener,
          click: createListener
        }
      };
    }
    function createMultiInstanceUserTask(event) {
      const bpmnFactory: BpmnFactory | undefined = modeler().getBpmnFactory();
      // åˆ›å»ºä¸€ä¸ª bpmn:UserTask
      const userTask = bpmnFactory.create('bpmn:UserTask', {
        // name: '多实例用户任务', // åœ¨ç”»æ¿ä¸­æ˜¾ç¤ºå­—段
        isForCompensation: false
      });
      // å°†å¤šå®žä¾‹å±žæ€§åˆ†é…ç»™ bpmn:UserTask çš„ loopCharacteristics
      userTask.loopCharacteristics = bpmnFactory.create('bpmn:MultiInstanceLoopCharacteristics');
      const customUserTask = elementFactory.createShape({
        type: 'bpmn:UserTask',
        businessObject: userTask // åˆ†é…åˆ›å»ºçš„ userTask åˆ° businessObject
      });
      create.start(event, customUserTask, {});
    }
    assign(actions, {
      'create.parallel-gateway': createAction('bpmn:ParallelGateway', 'gateway', 'bpmn-icon-gateway-parallel', '并行网关'),
      'create.event-base-gateway': createAction('bpmn:EventBasedGateway', 'gateway', 'bpmn-icon-gateway-eventbased', '事件网关'),
      // åˆ†ç»„线
      'gateway-separator': {
        group: 'gateway',
        separator: true
      },
      'create.user-task': createAction('bpmn:UserTask', 'activity', 'bpmn-icon-user-task', '创建用户任务'),
      'create.multi-instance-user-task': {
        group: 'activity',
        type: 'bpmn:UserTask',
        className: 'bpmn-icon-user task-multi-instance',
        title: '创建多实例用户任务',
        action: {
          click: createMultiInstanceUserTask,
          dragstart: createMultiInstanceUserTask
        }
      },
      'task-separator': {
        group: 'activity',
        separator: true
      }
    });
    return actions;
  }
}
CustomPaletteProvider['$inject'] = ['palette', 'create', 'elementFactory', 'spaceTool', 'lassoTool', 'handTool', 'globalConnect', 'translate'];
export default CustomPaletteProvider;
src/bpmn/assets/module/Renderer/CustomRenderer.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';
import {
  append as svgAppend,
  attr as svgAttr,
  create as svgCreate,
  select as svgSelect,
  selectAll as svgSelectAll,
  clone as svgClone,
  clear as svgClear,
  remove as svgRemove
} from 'tiny-svg';
const HIGH_PRIORITY = 1500;
export default class CustomRenderer extends BaseRenderer {
  bpmnRenderer: BaseRenderer;
  modeling: any;
  constructor(eventBus, bpmnRenderer, modeling) {
    super(eventBus, HIGH_PRIORITY);
    this.bpmnRenderer = bpmnRenderer;
    this.modeling = modeling;
  }
  canRender(element) {
    // ignore labels
    return !element.labelTarget;
  }
  /**
   * è‡ªå®šä¹‰èŠ‚ç‚¹å›¾å½¢
   * @param {*} parentNode å½“前元素的svgNode
   * @param {*} element
   * @returns
   */
  drawShape(parentNode, element) {
    const shape = this.bpmnRenderer.drawShape(parentNode, element);
    const { type, width, height } = element;
    // å¼€å§‹ å¡«å……绿色
    if (type === 'bpmn:StartEvent') {
      svgAttr(shape, { fill: '#77DF6D' });
      return shape;
    }
    if (type === 'bpmn:EndEvent') {
      svgAttr(shape, { fill: '#EE7B77' });
      return shape;
    }
    if (type === 'bpmn:UserTask') {
      svgAttr(shape, { fill: '#A9C4F8' });
      return shape;
    }
    return shape;
  }
  getShapePath(shape) {
    return this.bpmnRenderer.getShapePath(shape);
  }
}
CustomRenderer['$inject'] = ['eventBus', 'bpmnRenderer'];
src/bpmn/assets/module/Translate/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
import zh from '../../lang/zh';
const customTranslate = (template: any, replacements: any) => {
  replacements = replacements || {};
  template = zh[template] || template;
  return template.replace(/{([^}]+)}/g, function (_: any, key: any) {
    return replacements[key] || '{' + key + '}';
  });
};
export const translateModule = {
  translate: ['value', customTranslate]
};
export default translateModule;
src/bpmn/assets/module/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
// ç¿»è¯‘模块
import TranslationModule from './Translate';
import { ModuleDeclaration } from 'didi';
import CustomPaletteProvider from './Palette/CustomPaletteProvider';
import CustomRenderer from './Renderer/CustomRenderer';
import CustomContextPadProvider from './ContextPad/CustomContextPadProvider';
const Module: ModuleDeclaration[] = [
  {
    __init__: ['customPaletteProvider', 'customContextPadProvider', 'customRenderer'],
    customPaletteProvider: ['type', CustomPaletteProvider],
    customRenderer: ['type', CustomRenderer],
    customContextPadProvider: ['type', CustomContextPadProvider]
  },
  TranslationModule
];
export default Module;
src/bpmn/assets/showConfig.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
export default {
  'bpmn:EndEvent': {},
  'bpmn:StartEvent': {
    initiator: true,
    formKey: true
  },
  'bpmn:UserTask': {
    allocationType: true,
    specifyDesc: true,
    multipleUserAuditType: true,
    async: true,
    priority: true,
    skipExpression: true,
    dueDate: true,
    taskListener: true,
    executionListener: true
  },
  'bpmn:ServiceTask': {
    async: true,
    skipExpression: true,
    isForCompensation: true,
    triggerable: true,
    class: true
  },
  'bpmn:ScriptTask': {
    async: true,
    isForCompensation: true,
    autoStoreVariables: true
  },
  'bpmn:ManualTask': {
    async: true,
    isForCompensation: true
  },
  'bpmn:ReceiveTask': {
    async: true,
    isForCompensation: true
  },
  'bpmn:SendTask': {
    async: true,
    isForCompensation: true
  },
  'bpmn:BusinessRuleTask': {
    async: true,
    isForCompensation: true,
    ruleVariablesInput: true,
    rules: true,
    resultVariable: true,
    exclude: true
  }
};
src/bpmn/assets/style/index.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,284 @@
.djs-palette {
  width: 300px;
  .bpmn-icon-hand-tool:hover {
    &:after {
      content: '启动手动工具';
      position: absolute;
      left: 45px;
      width: 120px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-lasso-tool:hover {
    &:after {
      content: '启动套索工具';
      position: absolute;
      left: 100px;
      width: 120px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-space-tool:hover {
    &:after {
      content: '启动创建/删除空间工具';
      position: absolute;
      left: 45px;
      width: 170px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-connection-multi:hover {
    &:after {
      content: '启动全局连接工具';
      position: absolute;
      left: 100px;
      width: 140px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-start-event-none:hover {
    &:after {
      content: '创建开始事件';
      position: absolute;
      left: 45px;
      width: 120px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-intermediate-event-none:hover {
    &:after {
      content: '创建中间/边界事件';
      position: absolute;
      left: 100px;
      width: 140px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-end-event-none:hover {
    &:after {
      content: '创建结束事件';
      position: absolute;
      left: 45px;
      width: 120px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-gateway-none:hover {
    &:after {
      content: '创建网关';
      position: absolute;
      left: 100px;
      width: 90px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-gateway-parallel:hover {
    &:after {
      content: '创建并行网关';
      position: absolute;
      left: 45px;
      width: 120px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-gateway-eventbased:hover {
    &:after {
      content: '创建事件网关';
      position: absolute;
      left: 100px;
      width: 120px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-task:hover {
    &:after {
      content: '创建任务';
      position: absolute;
      left: 45px;
      width: 80px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-subprocess-expanded:hover {
    &:after {
      content: '创建可折叠子流程';
      position: absolute;
      left: 100px;
      width: 140px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-user-task:hover {
    &:after {
      content: '创建用户任务';
      position: absolute;
      left: 45px;
      width: 120px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .task-multi-instance:hover {
    &:after {
      content: '创建多实例用户任务';
      position: absolute;
      left: 100px;
      width: 160px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-participant:hover {
    &:after {
      content: '创建泳池/泳道';
      position: absolute;
      left: 45px;
      width: 120px;
      font-size: 15px;
      font-weight: bold;
      color: #3a84de;
      border-radius: 2px;
      border: 1px solid #cccccc;
      background-color: #fafafa;
      opacity: 0.8;
    }
  }
  .bpmn-icon-data-object {
    display: none;
    &:hover {
      &:after {
        content: '创建数据对象';
        position: absolute;
        left: 45px;
        width: 120px;
        font-size: 15px;
        font-weight: bold;
        color: #3a84de;
        border-radius: 2px;
        border: 1px solid #cccccc;
        background-color: #fafafa;
        opacity: 0.8;
      }
    }
  }
  .bpmn-icon-data-store {
    display: none;
    &:hover {
      &:after {
        content: '创建数据存储';
        position: absolute;
        left: 100px;
        width: 120px;
        font-size: 15px;
        font-weight: bold;
        color: #3a84de;
        border-radius: 2px;
        border: 1px solid #cccccc;
        background-color: #fafafa;
        opacity: 0.8;
      }
    }
  }
  .bpmn-icon-group {
    display: none;
    &:hover {
      &:after {
        content: '创建分组';
        position: absolute;
        left: 100px;
        width: 100px;
        font-size: 15px;
        font-weight: bold;
        color: #3a84de;
        border-radius: 2px;
        border: 1px solid #cccccc;
        background-color: #fafafa;
        opacity: 0.8;
      }
    }
  }
}
src/bpmn/hooks/usePanel.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,145 @@
import showConfig from '../assets/showConfig';
import { ModdleElement } from 'bpmn';
import useModelerStore from '@/store/modules/modeler';
import { MultiInstanceTypeEnum } from '@/enums/bpmn/IndexEnums';
interface Options {
  element: ModdleElement;
}
export default (ops: Options) => {
  const { element } = ops;
  const { getModeling, getModdle } = useModelerStore();
  const modeling = getModeling();
  const moddle = getModdle();
  /**
   * å½“前节点类型
   */
  const elementType = computed(() => {
    const bizObj = element.businessObject;
    return bizObj.eventDefinitions ? bizObj.eventDefinitions[0].$type : bizObj.$type;
  });
  /**
   * ç”¨äºŽæŽ§åˆ¶é¢æ¿å­—段显示与隐藏的配置
   */
  const config = computed(() => showConfig[elementType.value] || {});
  /**
   * åˆ›å»ºä¸€ä¸ªèŠ‚ç‚¹
   * @param elementType èŠ‚ç‚¹ç±»åž‹
   * @param properties å±žæ€§
   * @param parent çˆ¶èŠ‚ç‚¹
   */
  const createModdleElement = (elementType: string, properties: any, parent: ModdleElement) => {
    const element = moddle.create(elementType, properties);
    parent && (element.$parent = parent);
    return element;
  };
  /**
   * èŽ·å–æ‰©å±•å±žæ€§ï¼Œå¦‚æžœä¸å­˜åœ¨ä¼šè‡ªåŠ¨åˆ›å»º
   */
  const getExtensionElements = (create = true) => {
    let extensionElements = element.businessObject.get<ModdleElement>('extensionElements');
    if (!extensionElements && create) {
      extensionElements = createModdleElement('bpmn:ExtensionElements', { values: [] }, element.businessObject);
      modeling.updateModdleProperties(element, element.businessObject, { extensionElements });
    }
    return extensionElements;
  };
  /**
   * èŽ·å–extensionElements下的properties
   * @param extensionElements å¯é€‰å‚数,默认获取当前Element下的extensionElements下的Properties
   */
  const getPropertiesElements = (extensionElements?: ModdleElement) => {
    if (!extensionElements) {
      extensionElements = getExtensionElements();
    }
    let propertiesElements = extensionElements.values.find((item) => item.$type === 'flowable:properties');
    if (!propertiesElements) {
      propertiesElements = createModdleElement('flowable:properties', { values: [] }, extensionElements);
      modeling.updateModdleProperties(element, extensionElements, {
        values: [...extensionElements.get<[]>('values'), propertiesElements]
      });
    }
    return propertiesElements;
  };
  /**
   * æ›´æ–°èŠ‚ç‚¹å±žæ€§
   * @param properties å±žæ€§å€¼
   */
  const updateProperties = (properties: any) => {
    modeling.updateProperties(element, properties);
  };
  /**
   * æ›´æ–°èŠ‚ç‚¹ä¿¡æ¯
   * @param updateElement éœ€è¦æ›´æ–°çš„节点
   * @param properties å±žæ€§
   */
  const updateModdleProperties = (updateElement, properties: any) => {
    modeling.updateModdleProperties(element, updateElement, properties);
  };
  /**
   * æ›´æ–°Property属性
   * @param name key值
   * @param value å€¼
   */
  const updateProperty = (name: string, value: string) => {
    const propertiesElements = getPropertiesElements();
    let propertyElements = propertiesElements.values.find((item) => item.name === name);
    if (!propertyElements) {
      propertyElements = createModdleElement('flowable:property', { name: name, value: value }, propertiesElements);
      modeling.updateModdleProperties(element, propertiesElements, {
        values: [...propertiesElements.get('values'), propertyElements]
      });
    } else {
      propertyElements.name = name;
      propertyElements.value = value;
    }
    return propertyElements;
  };
  const idChange = (newVal: string) => {
    if (newVal) {
      updateProperties({ id: newVal });
    }
  };
  const nameChange = (newVal: string) => {
    if (newVal) {
      updateProperties({ name: newVal });
    }
  };
  const formKeyChange = (newVal: string) => {
    updateProperties({ formKey: newVal });
  };
  const constant = {
    MultiInstanceType: [
      { id: '373d4b81-a0d1-4eb8-8685-0d2fb1b468e2', label: '无', value: MultiInstanceTypeEnum.NONE },
      { id: 'b5acea7c-b7e5-46b0-8778-390db091bdab', label: '串行', value: MultiInstanceTypeEnum.SERIAL },
      { id: 'b4f0c683-1ccc-43c4-8380-e1b998986caf', label: '并行', value: MultiInstanceTypeEnum.PARALLEL }
    ]
  };
  return {
    elementType,
    constant,
    showConfig: config,
    updateProperties,
    updateProperty,
    updateModdleProperties,
    createModdleElement,
    idChange,
    nameChange,
    formKeyChange,
    getExtensionElements,
    getPropertiesElements
  };
};
src/bpmn/hooks/useParseElement.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
import { ModdleElement } from 'bpmn';
interface Options {
  element: ModdleElement;
}
interface Data {
  id: string;
}
export default (ops: Options) => {
  const { element } = ops;
  const parseData = <T>(): T => {
    const result = {
      ...element.businessObject,
      ...element.businessObject.$attrs
    };
    // ç§»é™¤flowable前缀,格式化数组
    for (const key in result) {
      if (key.indexOf('flowable:') === 0) {
        const newKey = key.replace('flowable:', '');
        result[newKey] = result[key];
        delete result[key];
      }
    }
    return { ...result } as T;
  };
  return {
    parseData
  };
};
src/bpmn/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,496 @@
<template>
  <div class="containers-bpmn">
    <!-- dark模式下 è¿žæŽ¥çº¿çš„箭头样式 -->
    <svg width="0" height="0" style="position: absolute">
      <defs>
        <marker id="markerArrow-dark-mode" viewBox="0 0 20 20" refX="11" refY="10" markerWidth="10" markerHeight="10" orient="auto">
          <path d="M 1 5 L 11 10 L 1 15 Z" class="arrow-dark" />
        </marker>
      </defs>
    </svg>
    <div v-loading="loading" class="app-containers-bpmn">
      <el-container class="h-full">
        <el-container style="align-items: stretch">
          <el-header>
            <div class="process-toolbar">
              <el-space wrap :size="10">
                <el-tooltip effect="dark" content="自适应屏幕" placement="bottom">
                  <el-button size="small" icon="Rank" @click="fitViewport" />
                </el-tooltip>
                <el-tooltip effect="dark" content="放大" placement="bottom">
                  <el-button size="small" icon="ZoomIn" @click="zoomViewport(true)" />
                </el-tooltip>
                <el-tooltip effect="dark" content="缩小" placement="bottom">
                  <el-button size="small" icon="ZoomOut" @click="zoomViewport(false)" />
                </el-tooltip>
                <el-tooltip effect="dark" content="后退" placement="bottom">
                  <el-button size="small" icon="Back" @click="bpmnModeler.get('commandStack').undo()" />
                </el-tooltip>
                <el-tooltip effect="dark" content="前进" placement="bottom">
                  <el-button size="small" icon="Right" @click="bpmnModeler.get('commandStack').redo()" />
                </el-tooltip>
              </el-space>
              <el-space wrap :size="10" style="float: right; padding-right: 10px">
                <el-button size="small" type="primary" @click="saveXml">保 å­˜</el-button>
                <el-dropdown size="small">
                  <el-button size="small" type="primary"> é¢„ è§ˆ </el-button>
                  <template #dropdown>
                    <el-dropdown-menu>
                      <el-dropdown-item icon="Document" @click="previewXML">XML预览</el-dropdown-item>
                      <el-dropdown-item icon="View" @click="previewSVG"> SVG预览</el-dropdown-item>
                    </el-dropdown-menu>
                  </template>
                </el-dropdown>
                <el-dropdown size="small">
                  <el-button size="small" type="primary"> ä¸‹ è½½ </el-button>
                  <template #dropdown>
                    <el-dropdown-menu>
                      <el-dropdown-item icon="Download" @click="downloadXML">下载XML</el-dropdown-item>
                      <el-dropdown-item icon="Download" @click="downloadSVG"> ä¸‹è½½SVG</el-dropdown-item>
                    </el-dropdown-menu>
                  </template>
                </el-dropdown>
              </el-space>
            </div>
          </el-header>
          <div ref="canvas" class="canvas" />
        </el-container>
        <div :class="{ 'process-panel': true, 'hide': panelFlag }">
          <div class="process-panel-bar" @click="panelBarClick">
            <div class="open-bar">
              <el-link type="default" :underline="false">
                <svg-icon class-name="open-bar" :icon-class="panelFlag ? 'caret-back' : 'caret-forward'"></svg-icon>
              </el-link>
            </div>
          </div>
          <transition enter-active-class="animate__animated animate__fadeIn">
            <div v-show="showPanel" v-if="bpmnModeler" class="panel-content">
              <PropertyPanel :modeler="bpmnModeler" />
            </div>
          </transition>
        </div>
      </el-container>
    </div>
  </div>
  <div>
    <el-dialog v-model="perviewXMLShow" title="XML预览" width="80%" append-to-body>
      <highlightjs :code="xmlStr" language="XML" />
    </el-dialog>
  </div>
  <div>
    <el-dialog v-model="perviewSVGShow" title="SVG预览" width="80%" append-to-body>
      <div style="text-align: center" v-html="svgData" />
    </el-dialog>
  </div>
</template>
<script lang="ts" setup name="BpmnDesign">
import 'bpmn-js/dist/assets/diagram-js.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
import './assets/style/index.scss';
import { Canvas, Modeler } from 'bpmn';
import PropertyPanel from './panel/index.vue';
import BpmnModeler from 'bpmn-js/lib/Modeler.js';
import defaultXML from './assets/defaultXML';
import flowableModdle from './assets/moddle/flowable';
import Modules from './assets/module/index';
import useModelerStore from '@/store/modules/modeler';
import useDialog from '@/hooks/useDialog';
const emit = defineEmits(['closeCallBack', 'saveCallBack']);
const { visible, title, openDialog, closeDialog } = useDialog({
  title: '编辑流程'
});
const modelerStore = useModelerStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const panelFlag = ref(false);
const showPanel = ref(true);
const canvas = ref<HTMLDivElement>();
const panel = ref<HTMLDivElement>();
const bpmnModeler = ref<Modeler>();
const zoom = ref(1);
const perviewXMLShow = ref(false);
const perviewSVGShow = ref(false);
const xmlStr = ref('');
const svgData = ref('');
const loading = ref(false);
const panelBarClick = () => {
  // å»¶è¿Ÿæ‰§è¡Œï¼Œå¦åˆ™ä¼šå¯¼è‡´é¢æ¿æ”¶èµ·æ—¶ï¼Œå±žæ€§é¢æ¿ä¸æ˜¾ç¤º
  panelFlag.value = !panelFlag.value;
  setTimeout(() => {
    showPanel.value = !panelFlag.value;
  }, 100);
};
/**
 * åˆå§‹åŒ–Canvas
 */
const initCanvas = () => {
  bpmnModeler.value = new BpmnModeler({
    container: canvas.value,
    // é”®ç›˜
    keyboard: {
      bindTo: window // æˆ–者window,注意与外部表单的键盘监听事件是否冲突
    },
    propertiesPanel: {
      parent: panel.value
    },
    additionalModules: Modules,
    moddleExtensions: {
      flowable: flowableModdle
    }
  });
};
/**
 * åˆå§‹åŒ–Model
 */
const initModel = () => {
  if (modelerStore.getModeler()) {
    modelerStore.getModeler().destroy();
    modelerStore.setModeler(undefined);
  }
  modelerStore.setModeler(bpmnModeler.value);
};
/**
 * æ–°å»º
 */
const newDiagram = async () => {
  await proxy?.$modal.confirm('是否确认新建');
  initDiagram();
};
/**
 * åˆå§‹åŒ–
 */
const initDiagram = (xml?: string) => {
  if (!xml) xml = defaultXML;
  bpmnModeler.value.importXML(xml);
};
/**
 * è‡ªé€‚应屏幕
 */
const fitViewport = () => {
  zoom.value = bpmnModeler.value.get<Canvas>('canvas').zoom('fit-viewport');
  const bbox = document.querySelector<SVGGElement>('.app-containers-bpmn .viewport').getBBox();
  const currentViewBox = bpmnModeler.value.get<Canvas>('canvas').viewbox();
  const elementMid = {
    x: bbox.x + bbox.width / 2 - 65,
    y: bbox.y + bbox.height / 2
  };
  bpmnModeler.value.get<Canvas>('canvas').viewbox({
    x: elementMid.x - currentViewBox.width / 2,
    y: elementMid.y - currentViewBox.height / 2,
    width: currentViewBox.width,
    height: currentViewBox.height
  });
  zoom.value = (bbox.width / currentViewBox.width) * 1.8;
};
/**
 * æ”¾å¤§æˆ–者缩小
 * @param zoomIn true æ”¾å¤§ | false ç¼©å°
 */
const zoomViewport = (zoomIn = true) => {
  zoom.value = bpmnModeler.value.get<Canvas>('canvas').zoom();
  zoom.value += zoomIn ? 0.1 : -0.1;
  bpmnModeler.value.get<Canvas>('canvas').zoom(zoom.value);
};
/**
 * ä¸‹è½½XML
 */
const downloadXML = async () => {
  try {
    const { xml } = await bpmnModeler.value.saveXML({ format: true });
    downloadFile(`${getProcessElement().name}.bpmn20.xml`, xml, 'application/xml');
  } catch (e) {
    proxy?.$modal.msgError(e);
  }
};
/**
 * ä¸‹è½½SVG
 */
const downloadSVG = async () => {
  try {
    const { svg } = await bpmnModeler.value.saveSVG();
    downloadFile(getProcessElement().name, svg, 'image/svg+xml');
  } catch (e) {
    proxy?.$modal.msgError(e);
  }
};
/**
 * XML预览
 */
const previewXML = async () => {
  try {
    const { xml } = await bpmnModeler.value.saveXML({ format: true });
    xmlStr.value = xml;
    perviewXMLShow.value = true;
  } catch (e) {
    proxy?.$modal.msgError(e);
  }
};
/**
 * SVG预览
 */
const previewSVG = async () => {
  try {
    const { svg } = await bpmnModeler.value.saveSVG();
    svgData.value = svg;
    perviewSVGShow.value = true;
  } catch (e) {
    proxy?.$modal.msgError(e);
  }
};
const curNodeInfo = reactive({
  curType: '', // ä»»åŠ¡ç±»åž‹ ç”¨æˆ·ä»»åŠ¡
  curNode: '',
  expValue: '' //多用户和部门角色实现
});
const downloadFile = (fileName: string, data: any, type: string) => {
  const a = document.createElement('a');
  const url = window.URL.createObjectURL(new Blob([data], { type: type }));
  a.href = url;
  a.download = fileName;
  a.click();
  window.URL.revokeObjectURL(url);
};
const getProcessElement = () => {
  const rootElements = bpmnModeler.value?.getDefinitions().rootElements;
  for (let i = 0; i < rootElements.length; i++) {
    if (rootElements[i].$type === 'bpmn:Process') return rootElements[i];
  }
};
const getProcess = () => {
  const element = getProcessElement();
  return {
    id: element.id,
    name: element.name
  };
};
const saveXml = async () => {
  const { xml } = await bpmnModeler.value.saveXML({ format: true });
  const { svg } = await bpmnModeler.value.saveSVG();
  const process = getProcess();
  let data = {
    xml: xml,
    svg: svg,
    key: process.id,
    name: process.name,
    loading: loading
  };
  emit('saveCallBack', data);
};
const open = (xml?: string) => {
  openDialog();
  nextTick(() => {
    initDiagram(xml);
  });
};
const close = () => {
  closeDialog();
};
onMounted(() => {
  nextTick(() => {
    initCanvas();
    initModel();
  });
});
/**
 * å¯¹å¤–暴露子组件方法
 */
defineExpose({
  initDiagram,
  saveXml,
  open,
  close
});
</script>
<style lang="scss">
/** å¤œé—´æ¨¡å¼ çº¿æ¡çš„颜色 */
$stroke-color-dark: white;
$bpmn-font-size: 12px;
/** æ—¥é—´æ¨¡å¼ å­—体颜色 */
$bpmn-font-color-dark: white;
/** å¤œé—´æ¨¡å¼ å­—体颜色 */
$bpmn-font-color-light: #222;
/* èƒŒæ™¯ç½‘æ ¼ */
@mixin djs-container {
  background-image: linear-gradient(90deg, hsl(0deg 0% 78.4% / 15%) 10%, transparent 0), linear-gradient(hsl(0deg 0% 78.4% / 15%) 10%, transparent 0) !important;
  background-size: 10px 10px !important;
}
html[class='light'] {
  /** ä»Žå·¦ä¾§æ‹–动时的背景图 */
  svg.new-parent {
    @include djs-container;
  }
  /** åŒå‡»ç¼–辑元素时样式保持一致 */
  div.djs-direct-editing-parent {
    border-radius: 10px;
    background-color: transparent !important;
    color: $bpmn-font-color-light;
  }
  g.djs-visual {
    .djs-label {
      fill: $bpmn-font-color-light !important;
      font-size: $bpmn-font-size !important;
    }
  }
}
html[class='dark'] {
  /** dark模式下 è¿žæŽ¥çº¿çš„箭头样式 */
  .arrow-dark {
    stroke-width: 1px;
    stroke-linecap: round;
    stroke: $stroke-color-dark;
    fill: $stroke-color-dark;
    stroke-linejoin: round;
  }
  /** ä»Žå·¦ä¾§æ‹–动时的背景图 */
  svg.new-parent {
    background-color: black !important;
    @include djs-container;
  }
  /** åŒå‡»ç¼–辑元素时样式保持一致 */
  div.djs-direct-editing-parent {
    border-radius: 10px;
    background-color: transparent !important;
    color: $bpmn-font-color-dark;
  }
  /** å…ƒç´ ç›¸å…³è®¾ç½® */
  g.djs-visual {
    /** å…ƒç´ è¾¹æ¡† éœ€è¦åŽ»é™¤æ–‡å­—(.djs-label) */
    & > *:first-child:not(.djs-label) {
      stroke: $stroke-color-dark !important;
    }
    /** å­—体颜色 */
    .djs-label {
      fill: $bpmn-font-color-dark !important;
      font-size: $bpmn-font-size !important;
    }
    /* è¿žæŽ¥çº¿æ ·å¼ */
    path[data-corner-radius] {
      stroke: $stroke-color-dark !important;
      marker-end: url('#markerArrow-dark-mode') !important;
    }
  }
}
.containers-bpmn {
  height: 100%;
  .app-containers-bpmn {
    width: 100%;
    height: 100%;
    .canvas {
      width: 100%;
      height: 100%;
      @include djs-container;
    }
    .el-header {
      height: 35px;
      padding: 0;
    }
    .process-panel {
      transition: width 0.25s ease-in;
      .process-panel-bar {
        width: 34px;
        height: 40px;
        .open-bar {
          width: 34px;
          line-height: 40px;
        }
      }
      // æ”¶èµ·é¢æ¿æ ·å¼
      &.hide {
        width: 34px;
        overflow: hidden;
        padding: 0;
        .process-panel-bar {
          width: 34px;
          height: 100%;
          box-sizing: border-box;
          display: block;
          text-align: left;
          line-height: 34px;
        }
        .process-panel-bar:hover {
          background-color: var(--bpmn-panel-bar-background-color);
        }
      }
    }
  }
}
pre {
  margin: 0;
  height: 100%;
  max-height: calc(80vh - 32px);
  overflow-x: hidden;
  overflow-y: auto;
  .hljs {
    word-break: break-word;
    white-space: pre-wrap;
    padding: 0.5em;
  }
}
.open-bar {
  font-size: 20px;
  cursor: pointer;
  text-align: center;
}
.process-panel {
  box-sizing: border-box;
  padding: 0 8px 0 8px;
  border-left: 1px solid var(--bpmn-panel-border);
  box-shadow: var(--bpmn-panel-box-shadow) 0 0 8px;
  max-height: 100%;
  width: 25%;
  height: calc(100vh - 100px);
  .el-collapse {
    height: calc(100vh - 182px);
    overflow: auto;
  }
}
// ä»»åŠ¡æ  é€æ˜Žåº¦
//:deep(.djs-palette) {
//  opacity: 0.3;
//  transition: all 1s;
//}
//
//:deep(.djs-palette:hover) {
//  opacity: 1;
//  transition: all 1s;
//}
</style>
src/bpmn/panel/GatewayPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
<template>
  <div>
    <el-collapse v-model="currentCollapseItem">
      <el-collapse-item name="1">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <InfoFilled />
            </el-icon>
            å¸¸è§„
          </div>
        </template>
        <div>
          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
            <el-form-item prop="id" label="节点 ID">
              <el-input v-model="formData.id" @change="idChange"> </el-input>
            </el-form-item>
            <el-form-item prop="name" label="节点名称">
              <el-input v-model="formData.name" @change="nameChange"> </el-input>
            </el-form-item>
          </el-form>
        </div>
      </el-collapse-item>
      <el-collapse-item name="2">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <BellFilled />
            </el-icon>
            æ‰§è¡Œç›‘听器
          </div>
        </template>
        <div>
          <ExecutionListener :element="element"></ExecutionListener>
        </div>
      </el-collapse-item>
    </el-collapse>
  </div>
</template>
<script setup lang="ts">
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import { Modeler, ModdleElement } from 'bpmn';
import { GatewayPanel } from 'bpmnDesign';
import ExecutionListener from './property/ExecutionListener.vue';
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const { nameChange, idChange } = usePanel({
  element: toRaw(props.element)
});
const { parseData } = useParseElement({
  element: toRaw(props.element)
});
const currentCollapseItem = ref(['1', '2']);
const formData = ref(parseData<GatewayPanel>());
const formRules = ref<ElFormRules>({
  processCategory: [{ required: true, message: '请选择', trigger: 'blur' }],
  id: [{ required: true, message: '请输入', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }]
});
</script>
<style lang="scss" scoped></style>
src/bpmn/panel/ParticipantPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
<template>
  <div>
    <el-collapse v-model="currentCollapseItem">
      <el-collapse-item name="1">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <InfoFilled />
            </el-icon>
            å¸¸è§„
          </div>
        </template>
        <div>
          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
            <el-form-item prop="id" label="节点 ID">
              <el-input v-model="formData.id" @change="idChange"></el-input>
            </el-form-item>
            <el-form-item prop="name" label="节点名称">
              <el-input v-model="formData.name" @change="nameChange"></el-input>
            </el-form-item>
          </el-form>
        </div>
      </el-collapse-item>
      <el-collapse-item name="2">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <BellFilled />
            </el-icon>
            æ‰§è¡Œç›‘听器
          </div>
        </template>
        <div>
          <ExecutionListener :element="element"></ExecutionListener>
        </div>
      </el-collapse-item>
    </el-collapse>
  </div>
</template>
<script setup lang="ts">
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import ExecutionListener from './property/ExecutionListener.vue';
import { ModdleElement } from 'bpmn';
import { ParticipantPanel } from 'bpmnDesign';
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const { nameChange, idChange } = usePanel({
  element: toRaw(props.element)
});
const { parseData } = useParseElement({
  element: toRaw(props.element)
});
const formData = ref(parseData<ParticipantPanel>());
const currentCollapseItem = ref(['1', '2']);
const formRules = ref<ElFormRules>({
  id: [{ required: true, message: '请输入', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }]
});
</script>
<style lang="scss" scoped></style>
src/bpmn/panel/ProcessPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
<template>
  <div>
    <el-collapse v-model="currentCollapseItem">
      <el-collapse-item name="1">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <InfoFilled />
            </el-icon>
            å¸¸è§„
          </div>
        </template>
        <div>
          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
            <el-form-item label="流程标识" prop="id">
              <el-input v-model="formData.id" @change="idChange"></el-input>
            </el-form-item>
            <el-form-item label="流程名称" prop="name">
              <el-input v-model="formData.name" @change="nameChange"></el-input>
            </el-form-item>
          </el-form>
        </div>
      </el-collapse-item>
      <el-collapse-item name="2">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <BellFilled />
            </el-icon>
            æ‰§è¡Œç›‘听器
          </div>
        </template>
        <div>
          <ExecutionListener :element="element"></ExecutionListener>
        </div>
      </el-collapse-item>
    </el-collapse>
  </div>
</template>
<script setup lang="ts">
import ExecutionListener from './property/ExecutionListener.vue';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import { Modeler, ModdleElement } from 'bpmn';
import { ProcessPanel } from 'bpmnDesign';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const { parseData } = useParseElement({
  element: toRaw(props.element)
});
const { idChange, nameChange } = usePanel({
  element: toRaw(props.element)
});
const currentCollapseItem = ref(['1', '2']);
const formData = ref<ProcessPanel>(parseData<ProcessPanel>());
const formRules = ref<ElFormRules>({
  id: [{ required: true, message: '请输入', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }]
});
</script>
<style scoped lang="scss"></style>
src/bpmn/panel/SequenceFlowPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
<template>
  <div>
    <el-collapse v-model="currentCollapseItem">
      <el-collapse-item name="1">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <InfoFilled />
            </el-icon>
            å¸¸è§„
          </div>
        </template>
        <div>
          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
            <el-form-item prop="id" label="节点 ID">
              <el-input v-model="formData.id" @change="idChange"> </el-input>
            </el-form-item>
            <el-form-item prop="name" label="节点名称">
              <el-input v-model="formData.name" @change="nameChange"> </el-input>
            </el-form-item>
            <el-form-item prop="conditionExpression" label="跳转条件">
              <el-input v-model="formData.conditionExpressionValue" @change="conditionExpressionChange"> </el-input>
            </el-form-item>
            <el-form-item prop="skipExpression" label="跳过表达式">
              <el-input v-model="formData.skipExpression" @change="skipExpressionChange"> </el-input>
            </el-form-item>
          </el-form>
        </div>
      </el-collapse-item>
      <el-collapse-item name="2">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <BellFilled />
            </el-icon>
            æ‰§è¡Œç›‘听器
          </div>
        </template>
        <div>
          <ExecutionListener :element="element"></ExecutionListener>
        </div>
      </el-collapse-item>
    </el-collapse>
  </div>
</template>
<script setup lang="ts">
import useParseElement from '../hooks/useParseElement';
import useModelerStore from '@/store/modules/modeler';
import usePanel from '../hooks/usePanel';
import ExecutionListener from './property/ExecutionListener.vue';
import { Modeler, ModdleElement } from 'bpmn';
import { SequenceFlowPanel } from 'bpmnDesign';
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const { nameChange, idChange, updateProperties } = usePanel({
  element: toRaw(props.element)
});
const { parseData } = useParseElement({
  element: toRaw(props.element)
});
const moddle = useModelerStore().getModdle();
const currentCollapseItem = ref(['1', '2']);
const formData = ref(parseData<SequenceFlowPanel>());
const formRules = ref<ElFormRules>({
  processCategory: [{ required: true, message: '请选择', trigger: 'blur' }],
  id: [{ required: true, message: '请输入', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }]
});
const conditionExpressionChange = (val: string) => {
  if (val) {
    const newCondition = moddle.create('bpmn:FormalExpression', { body: val });
    updateProperties({ conditionExpression: newCondition });
  } else {
    updateProperties({ conditionExpression: null });
  }
};
const skipExpressionChange = (val: string) => {
  updateProperties({ 'flowable:skipExpression': val });
};
onBeforeMount(() => {
  if (formData.value.conditionExpression) {
    formData.value.conditionExpressionValue = formData.value.conditionExpression.body;
  }
});
</script>
<style lang="scss" scoped></style>
src/bpmn/panel/StartEndPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
<template>
  <div>
    <el-collapse v-model="currentCollapseItem">
      <el-collapse-item name="1">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <InfoFilled />
            </el-icon>
            å¸¸è§„
          </div>
        </template>
        <div>
          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
            <el-form-item prop="id" label="节点 ID">
              <el-input v-model="formData.id" @change="idChange"> </el-input>
            </el-form-item>
            <el-form-item prop="name" label="节点名称">
              <el-input v-model="formData.name" @change="nameChange"> </el-input>
            </el-form-item>
          </el-form>
        </div>
      </el-collapse-item>
      <el-collapse-item name="2">
        <template #title>
          <div class="collapse__title">
            <el-icon>
              <BellFilled />
            </el-icon>
            æ‰§è¡Œç›‘听器
          </div>
        </template>
        <div>
          <ExecutionListener :element="element"></ExecutionListener>
        </div>
      </el-collapse-item>
    </el-collapse>
  </div>
</template>
<script setup lang="ts">
import ExecutionListener from './property/ExecutionListener.vue';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import { Modeler, ModdleElement } from 'bpmn';
import { StartEndPanel } from 'bpmnDesign';
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const { nameChange, idChange } = usePanel({
  element: toRaw(props.element)
});
const { parseData } = useParseElement({
  element: toRaw(props.element)
});
const formData = ref(parseData<StartEndPanel>());
const currentCollapseItem = ref(['1', '2']);
const formRules = ref<ElFormRules>({
  id: [{ required: true, message: '请输入', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }]
});
</script>
<style lang="scss" scoped></style>
src/bpmn/panel/SubProcessPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,193 @@
<template>
  <div>
    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
      <el-collapse v-model="currentCollapseItem">
        <el-collapse-item name="1">
          <template #title>
            <div class="collapse__title">
              <el-icon>
                <InfoFilled />
              </el-icon>
              å¸¸è§„
            </div>
          </template>
          <div>
            <el-form-item prop="id" label="节点 ID">
              <el-input v-model="formData.id" @change="idChange"> </el-input>
            </el-form-item>
            <el-form-item prop="name" label="节点名称">
              <el-input v-model="formData.name" @change="nameChange"> </el-input>
            </el-form-item>
          </div>
        </el-collapse-item>
        <el-collapse-item name="2">
          <template #title>
            <div class="collapse__title">
              <el-icon>
                <BellFilled />
              </el-icon>
              æ‰§è¡Œç›‘听器
            </div>
          </template>
          <div>
            <ExecutionListener :element="element"></ExecutionListener>
          </div>
        </el-collapse-item>
        <el-collapse-item name="3">
          <template #title>
            <div class="collapse__title">
              <el-icon>
                <HelpFilled />
              </el-icon>
              å¤šå®žä¾‹
            </div>
          </template>
          <div>
            <el-form-item label="多实例类型">
              <el-select v-model="formData.multiInstanceType" @change="multiInstanceTypeChange">
                <el-option v-for="item in constant.MultiInstanceType" :key="item.id" :value="item.value" :label="item.label"> </el-option>
              </el-select>
            </el-form-item>
            <div v-if="formData.multiInstanceType !== MultiInstanceTypeEnum.NONE">
              <el-form-item label="集合">
                <template #label>
                  <span>
                    é›†åˆ
                    <el-tooltip placement="top">
                      <el-icon><QuestionFilled /></el-icon>
                      <template #content>
                        å±žæ€§ä¼šä½œä¸ºè¡¨è¾¾å¼è¿›è¡Œè§£æžã€‚如果表达式解析为字符串而不是一个集合,<br />
                        ä¸è®ºæ˜¯å› ä¸ºæœ¬èº«é…ç½®çš„就是静态字符串值,还是表达式计算结果为字符串,<br />
                        è¿™ä¸ªå­—符串都会被当做变量名,并从流程变量中用于获取实际的集合。
                      </template>
                    </el-tooltip>
                  </span>
                </template>
                <el-input v-model="formData.collection" @change="collectionChange"></el-input>
              </el-form-item>
              <el-form-item label="元素变量">
                <template #label>
                  <span>
                    å…ƒç´ å˜é‡
                    <el-tooltip placement="top">
                      <el-icon><QuestionFilled /></el-icon>
                      <template #content>
                        æ¯åˆ›å»ºä¸€ä¸ªç”¨æˆ·ä»»åŠ¡å‰ï¼Œå…ˆä»¥è¯¥å…ƒç´ å˜é‡ä¸ºlabel,集合中的一项为value,<br />
                        åˆ›å»ºï¼ˆå±€éƒ¨ï¼‰æµç¨‹å˜é‡ï¼Œè¯¥å±€éƒ¨æµç¨‹å˜é‡è¢«ç”¨äºŽæŒ‡æ´¾ç”¨æˆ·ä»»åŠ¡ã€‚<br />
                        ä¸€èˆ¬æ¥è¯´ï¼Œè¯¥å­—符串应与指定人员变量相同。
                      </template>
                    </el-tooltip>
                  </span>
                </template>
                <el-input v-model="formData.elementVariable" @change="elementVariableChange"> </el-input>
              </el-form-item>
              <el-form-item label="完成条件">
                <template #label>
                  <span>
                    å®Œæˆæ¡ä»¶
                    <el-tooltip placement="top">
                      <el-icon><QuestionFilled /></el-icon>
                      <template #content>
                        å¤šå®žä¾‹æ´»åŠ¨åœ¨æ‰€æœ‰å®žä¾‹éƒ½å®Œæˆæ—¶ç»“æŸï¼Œç„¶è€Œä¹Ÿå¯ä»¥æŒ‡å®šä¸€ä¸ªè¡¨è¾¾å¼ï¼Œåœ¨æ¯ä¸ªå®žä¾‹<br />
                        ç»“束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例<br />
                        æ´»åŠ¨ï¼Œç»§ç»­æ‰§è¡Œæµç¨‹ã€‚ä¾‹å¦‚ ${nrOfCompletedInstances/nrOfInstances >= 0.6 },<br />
                        è¡¨ç¤ºå½“任务完成60%时,该节点就算完成
                      </template>
                    </el-tooltip>
                  </span>
                </template>
                <el-input v-model="formData.completionCondition" @change="completionConditionChange"> </el-input>
              </el-form-item>
            </div>
          </div>
        </el-collapse-item>
      </el-collapse>
    </el-form>
  </div>
</template>
<script setup lang="ts">
import ExecutionListener from './property/ExecutionListener.vue';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import { ModdleElement } from 'bpmn';
import { SubProcessPanel } from 'bpmnDesign';
import { MultiInstanceTypeEnum } from '@/enums/bpmn/IndexEnums';
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const { nameChange, idChange, updateProperties, createModdleElement, constant } = usePanel({
  element: toRaw(props.element)
});
const { parseData } = useParseElement({
  element: toRaw(props.element)
});
const formData = ref(parseData<SubProcessPanel>());
const currentCollapseItem = ref(['1', '2', '3']);
const multiInstanceTypeChange = (newVal) => {
  if (newVal !== MultiInstanceTypeEnum.NONE) {
    let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
    if (!loopCharacteristics) {
      loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
    }
    loopCharacteristics.isSequential = newVal === MultiInstanceTypeEnum.SERIAL;
    updateProperties({ loopCharacteristics: loopCharacteristics });
  } else {
    updateProperties({ loopCharacteristics: undefined });
  }
};
const collectionChange = (newVal) => {
  let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
  if (!loopCharacteristics) {
    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
  }
  loopCharacteristics.collection = newVal && newVal.length > 0 ? newVal : undefined;
  updateProperties({ loopCharacteristics: loopCharacteristics });
};
const elementVariableChange = (newVal) => {
  let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
  if (!loopCharacteristics) {
    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
  }
  loopCharacteristics.elementVariable = newVal && newVal.length > 0 ? newVal : undefined;
  updateProperties({ loopCharacteristics: loopCharacteristics });
};
const completionConditionChange = (newVal) => {
  let loopCharacteristics = props.element.businessObject.get<ModdleElement>('loopCharacteristics');
  if (!loopCharacteristics) {
    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
  }
  if (newVal && newVal.length > 0) {
    if (!loopCharacteristics.completionCondition) {
      loopCharacteristics.completionCondition = createModdleElement('bpmn:Expression', { body: newVal }, loopCharacteristics);
    } else {
      loopCharacteristics.completionCondition.body = newVal;
    }
  } else {
    loopCharacteristics.completionCondition = undefined;
  }
  updateProperties({ loopCharacteristics: loopCharacteristics });
};
onBeforeMount(() => {
  if (formData.value.loopCharacteristics) {
    const loopCharacteristics = formData.value.loopCharacteristics;
    formData.value.collection = loopCharacteristics.collection || '';
    formData.value.elementVariable = loopCharacteristics.elementVariable || '';
    formData.value.completionCondition = loopCharacteristics.completionCondition?.body || '';
    formData.value.multiInstanceType = loopCharacteristics.isSequential ? MultiInstanceTypeEnum.SERIAL : MultiInstanceTypeEnum.PARALLEL;
  }
});
const formRules = ref<ElFormRules>({
  id: [{ required: true, message: '请输入', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }]
});
</script>
<style lang="scss" scoped></style>
src/bpmn/panel/TaskPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,492 @@
<template>
  <div>
    <el-form ref="formRef" size="default" :model="formData" :rules="formRules" label-width="100px">
      <el-collapse v-model="currentCollapseItem">
        <el-collapse-item name="1">
          <template #title>
            <div class="collapse__title">
              <el-icon>
                <InfoFilled />
              </el-icon>
              å¸¸è§„
            </div>
          </template>
          <div>
            <el-form-item prop="id" label="节点 ID">
              <el-input v-model="formData.id" @change="idChange"> </el-input>
            </el-form-item>
            <el-form-item prop="name" label="节点名称">
              <el-input v-model="formData.name" @change="nameChange"> </el-input>
            </el-form-item>
            <el-form-item v-if="showConfig.skipExpression" prop="skipExpression" label="跳过表达式">
              <el-input v-model="formData.skipExpression" @change="skipExpressionChange"> </el-input>
            </el-form-item>
            <el-form-item v-loading="formManageListLoading" prop="formKey" label="表单地址">
              <el-select v-model="formData.formKey" clearable filterable placeholder="请选择表单" style="width: 260px" @change="formKeyChange">
                <el-option
                  v-for="item in formManageList"
                  :key="item.id"
                  :label="item.formTypeName + ':' + item.formName"
                  :value="item.formType + ':' + item.id"
                />
              </el-select>
            </el-form-item>
          </div>
        </el-collapse-item>
        <el-collapse-item name="2">
          <template #title>
            <div class="collapse__title">
              <el-icon>
                <Checked />
              </el-icon>
              ä»»åŠ¡
            </div>
          </template>
          <div>
            <el-form-item v-if="showConfig.async" prop="sync" label="是否异步">
              <el-switch v-model="formData.async" inline-prompt active-text="是" inactive-text="否" @change="syncChange" />
            </el-form-item>
            <el-tabs tab-position="left" class="demo-tabs">
              <el-tab-pane label="身份存储">
                <el-form-item label="分配人员">
                  <el-input v-model="formData.assignee" @blur="blurAssignee(formData.assignee)">
                    <template #append>
                      <el-button icon="Search" type="primary" @click="openSingleUserSelect" />
                    </template>
                  </el-input>
                </el-form-item>
                <el-form-item label="候选人员">
                  <el-badge :value="selectUserLength" :max="99">
                    <el-button size="small" type="primary" @click="openUserSelect">选择人员</el-button>
                  </el-badge>
                </el-form-item>
                <el-form-item label="候选组">
                  <el-badge :value="selectRoleLength" :max="99">
                    <el-button size="small" type="primary" @click="openRoleSelect">选择组</el-button>
                  </el-badge>
                </el-form-item>
              </el-tab-pane>
              <!-- <el-tab-pane label="固定值">
                <el-form-item prop="auditUserType" label="分配类型">
                  <el-select v-model="formData.allocationType">
                    <el-option v-for="item in AllocationTypeSelect" :key="item.id" :value="item.value" :label="item.label"> </el-option>
                  </el-select>
                </el-form-item>
                <el-form-item v-if="formData.allocationType === AllocationTypeEnum.USER" label="分配人员">
                  <el-input v-model="formData.assignee">
                    <template #append>
                      <el-button icon="Search" type="primary" @click="openSingleUserSelect" />
                    </template>
                  </el-input>
                </el-form-item>
                <div v-if="formData.allocationType === AllocationTypeEnum.CANDIDATE">
                  <el-form-item label="候选人员">
                    <el-badge :value="selectUserLength" :max="99">
                      <el-button size="small" type="primary" @click="openUserSelect">选择人员</el-button>
                    </el-badge>
                  </el-form-item>
                  <el-form-item label="候选组">
                    <el-badge :value="selectRoleLength" :max="99">
                      <el-button size="small" type="primary" @click="openRoleSelect">选择组</el-button>
                    </el-badge>
                  </el-form-item>
                </div>
                <el-form-item v-if="formData.allocationType === AllocationTypeEnum.SPECIFY && showConfig.specifyDesc" style="">
                  <el-radio-group v-model="formData.specifyDesc" class="ml-4">
                    <el-radio v-for="item in SpecifyDesc" :key="item.id" :value="item.value" size="large">{{ item.label }}</el-radio>
                  </el-radio-group>
                </el-form-item>
              </el-tab-pane> -->
            </el-tabs>
            <el-form-item v-if="showConfig.dueDate" prop="dueDate" label="到期时间">
              <el-input v-model="formData.dueDate" clearable @change="dueDateChange" @click="openDueDate">
                <template #append>
                  <el-button icon="Search" type="primary" @click="openDueDate" />
                </template>
              </el-input>
            </el-form-item>
            <el-form-item v-if="showConfig.priority" prop="priority" label="优先级">
              <el-input-number v-model="formData.priority" :min="0" @change="priorityChange"> </el-input-number>
            </el-form-item>
          </div>
        </el-collapse-item>
        <el-collapse-item name="3">
          <template #title>
            <div class="collapse__title">
              <el-icon>
                <HelpFilled />
              </el-icon>
              å¤šå®žä¾‹
            </div>
          </template>
          <div>
            <el-form-item label="多实例类型">
              <el-select v-model="formData.multiInstanceType" @change="multiInstanceTypeChange">
                <el-option v-for="item in constant.MultiInstanceType" :key="item.id" :value="item.value" :label="item.label"> </el-option>
              </el-select>
            </el-form-item>
            <div v-if="formData.multiInstanceType !== MultiInstanceTypeEnum.NONE">
              <el-form-item label="集合">
                <template #label>
                  <span>
                    é›†åˆ
                    <el-tooltip placement="top">
                      <el-icon><QuestionFilled /></el-icon>
                      <template #content>
                        å±žæ€§ä¼šä½œä¸ºè¡¨è¾¾å¼è¿›è¡Œè§£æžã€‚如果表达式解析为字符串而不是一个集合,<br />
                        ä¸è®ºæ˜¯å› ä¸ºæœ¬èº«é…ç½®çš„就是静态字符串值,还是表达式计算结果为字符串,<br />
                        è¿™ä¸ªå­—符串都会被当做变量名,并从流程变量中用于获取实际的集合。
                      </template>
                    </el-tooltip>
                  </span>
                </template>
                <el-input v-model="formData.collection" @change="collectionChange"></el-input>
              </el-form-item>
              <el-form-item label="元素变量">
                <template #label>
                  <span>
                    å…ƒç´ å˜é‡
                    <el-tooltip placement="top">
                      <el-icon><QuestionFilled /></el-icon>
                      <template #content>
                        æ¯åˆ›å»ºä¸€ä¸ªç”¨æˆ·ä»»åŠ¡å‰ï¼Œå…ˆä»¥è¯¥å…ƒç´ å˜é‡ä¸ºlabel,集合中的一项为value,<br />
                        åˆ›å»ºï¼ˆå±€éƒ¨ï¼‰æµç¨‹å˜é‡ï¼Œè¯¥å±€éƒ¨æµç¨‹å˜é‡è¢«ç”¨äºŽæŒ‡æ´¾ç”¨æˆ·ä»»åŠ¡ã€‚<br />
                        ä¸€èˆ¬æ¥è¯´ï¼Œè¯¥å­—符串应与指定人员变量相同。
                      </template>
                    </el-tooltip>
                  </span>
                </template>
                <el-input v-model="formData.elementVariable" @change="elementVariableChange"> </el-input>
              </el-form-item>
              <el-form-item label="完成条件">
                <template #label>
                  <span>
                    å®Œæˆæ¡ä»¶
                    <el-tooltip placement="top">
                      <el-icon><QuestionFilled /></el-icon>
                      <template #content>
                        å¤šå®žä¾‹æ´»åŠ¨åœ¨æ‰€æœ‰å®žä¾‹éƒ½å®Œæˆæ—¶ç»“æŸï¼Œç„¶è€Œä¹Ÿå¯ä»¥æŒ‡å®šä¸€ä¸ªè¡¨è¾¾å¼ï¼Œåœ¨æ¯ä¸ªå®žä¾‹<br />
                        ç»“束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例<br />
                        æ´»åŠ¨ï¼Œç»§ç»­æ‰§è¡Œæµç¨‹ã€‚ä¾‹å¦‚ ${nrOfCompletedInstances/nrOfInstances >= 0.6 },<br />
                        è¡¨ç¤ºå½“任务完成60%时,该节点就算完成
                      </template>
                    </el-tooltip>
                  </span>
                </template>
                <el-input v-model="formData.completionCondition" @change="completionConditionChange"> </el-input>
              </el-form-item>
            </div>
          </div>
        </el-collapse-item>
        <el-collapse-item v-if="showConfig.taskListener" name="4">
          <template #title>
            <div class="collapse__title">
              <el-icon>
                <BellFilled />
              </el-icon>
              ä»»åŠ¡ç›‘å¬å™¨
            </div>
          </template>
          <div>
            <TaskListener v-if="showConfig.taskListener" :element="element"></TaskListener>
          </div>
        </el-collapse-item>
        <el-collapse-item v-if="showConfig.executionListener" name="5">
          <template #title>
            <div class="collapse__title">
              <el-icon>
                <BellFilled />
              </el-icon>
              æ‰§è¡Œç›‘听器
            </div>
          </template>
          <div>
            <ExecutionListener v-if="showConfig.executionListener" :element="element"></ExecutionListener>
          </div>
        </el-collapse-item>
        <el-form-item v-if="showConfig.isForCompensation" prop="isForCompensation" label="是否为补偿">
          <el-switch v-model="formData.isForCompensation" inline-prompt active-text="是" inactive-text="否" />
        </el-form-item>
        <el-form-item v-if="showConfig.triggerServiceTask" prop="triggerServiceTask" label="服务任务可触发">
          <el-switch v-model="formData.triggerServiceTask" inline-prompt active-text="是" inactive-text="否" />
        </el-form-item>
        <el-form-item v-if="showConfig.autoStoreVariables" prop="autoStoreVariables" label="自动存储变量">
          <el-switch v-model="formData.autoStoreVariables" inline-prompt active-text="是" inactive-text="否" />
        </el-form-item>
        <el-form-item v-if="showConfig.ruleVariablesInput" prop="skipExpression" label="输入变量">
          <el-input v-model="formData.ruleVariablesInput"> </el-input>
        </el-form-item>
        <el-form-item v-if="showConfig.exclude" prop="exclude" label="排除">
          <el-switch v-model="formData.exclude" inline-prompt active-text="是" inactive-text="否" />
        </el-form-item>
        <el-form-item v-if="showConfig.class" prop="class" label="ç±»">
          <el-input v-model="formData.class"> </el-input>
        </el-form-item>
      </el-collapse>
    </el-form>
    <UserSelect ref="userSelectRef" :data="formData.candidateUsers" @confirm-call-back="userSelectCallBack"></UserSelect>
    <UserSelect ref="singleUserSelectRef" :data="formData.assignee" :multiple="false" @confirm-call-back="singleUserSelectCallBack"></UserSelect>
    <RoleSelect ref="roleSelectRef" :data="formData.candidateGroups" @confirm-call-back="roleSelectCallBack"></RoleSelect>
    <DueDate ref="dueDateRef" v-model="formData.dueDate" :data="formData.dueDate" @confirm-call-back="dueDateCallBack"></DueDate>
  </div>
</template>
<script setup lang="ts">
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import UserSelect from '@/components/UserSelect';
import RoleSelect from '@/components/RoleSelect';
import ExecutionListener from './property/ExecutionListener.vue';
import TaskListener from './property/TaskListener.vue';
import DueDate from './property/DueDate.vue';
import { ModdleElement } from 'bpmn';
import { TaskPanel } from 'bpmnDesign';
import { AllocationTypeEnum, MultiInstanceTypeEnum, SpecifyDescEnum } from '@/enums/bpmn/IndexEnums';
import { UserVO } from '@/api/system/user/types';
import { RoleVO } from '@/api/system/role/types';
import { selectListFormManage } from '@/api/workflow/formManage';
import { FormManageVO } from '@/api/workflow/formManage/types';
const formManageList = ref<FormManageVO[]>([]);
const formManageListLoading = ref(false);
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const { showConfig, nameChange, formKeyChange, idChange, updateProperties, getExtensionElements, createModdleElement, constant } = usePanel({
  element: toRaw(props.element)
});
const { parseData } = useParseElement({
  element: toRaw(props.element)
});
const initFormData = {
  id: '',
  name: '',
  dueDate: '',
  multiInstanceType: MultiInstanceTypeEnum.NONE,
  allocationType: AllocationTypeEnum.USER,
  specifyDesc: SpecifyDescEnum.SPECIFY_SINGLE
};
const formData = ref({ ...initFormData, ...parseData<TaskPanel>() });
const assignee = ref<Partial<UserVO>>({
  userName: ''
});
const currentCollapseItem = ref(['1', '2']);
const userSelectRef = ref<InstanceType<typeof UserSelect>>();
const singleUserSelectRef = ref<InstanceType<typeof UserSelect>>();
const roleSelectRef = ref<InstanceType<typeof RoleSelect>>();
const dueDateRef = ref<InstanceType<typeof DueDate>>();
const isMultiple = ref(true);
const openUserSelect = () => {
  userSelectRef.value.open();
};
const openSingleUserSelect = () => {
  if (formData.value.assignee.includes('$')) {
    formData.value.assignee = '';
  }
  singleUserSelectRef.value.open();
};
const openRoleSelect = () => {
  roleSelectRef.value.open();
};
const openDueDate = (e) => {
  dueDateRef.value.openDialog();
};
const blurAssignee = (assignee) => {
  updateProperties({ 'flowable:assignee': assignee ? assignee : undefined });
};
const singleUserSelectCallBack = (data: UserVO[]) => {
  const user: UserVO = data.length !== 0 ? data[0] : undefined;
  updateProperties({ 'flowable:assignee': user?.userId });
  assignee.value = user ? user : { userName: '' };
  formData.value.assignee = String(user?.userId);
  let extensionElements = getExtensionElements();
  extensionElements.values = extensionElements.get('values').filter((item) => item.$type !== 'flowable:extAssignee');
  if (user) {
    const extAssigneeElement = createModdleElement('flowable:extAssignee', { body: '' }, extensionElements);
    extensionElements.get('values').push(extAssigneeElement);
    extAssigneeElement.body = JSON.stringify({ userName: user.userName, userId: user.userId });
  }
  if (extensionElements.values.length === 0) {
    extensionElements = undefined;
  }
  updateProperties({ extensionElements: extensionElements });
};
const userSelectCallBack = (data: UserVO[]) => {
  let extensionElements = getExtensionElements();
  extensionElements.values = extensionElements.values.filter((item) => item.$type !== 'flowable:extCandidateUsers');
  if (data.length === 0) {
    formData.value.candidateUsers = undefined;
    updateProperties({ 'flowable:candidateUsers': undefined });
  } else {
    const userIds = data.map((item) => item.userId).join(',');
    formData.value.candidateUsers = userIds;
    updateProperties({ 'flowable:candidateUsers': userIds });
    const extCandidateUsersElement = createModdleElement('flowable:extCandidateUsers', { body: '' }, extensionElements);
    extensionElements.values.push(extCandidateUsersElement);
    const users = data.map((item) => {
      return {
        userId: item.userId,
        userName: item.userName
      };
    });
    extCandidateUsersElement.body = JSON.stringify(users);
  }
  if (extensionElements.values.length === 0) {
    extensionElements = undefined;
  }
  updateProperties({ extensionElements: extensionElements });
};
const roleSelectCallBack = (data: RoleVO[]) => {
  if (data.length === 0) {
    formData.value.candidateGroups = '';
    updateProperties({ 'flowable:candidateGroups': undefined });
  } else {
    const roleIds = data.map((item) => item.roleId).join(',');
    formData.value.candidateGroups = roleIds;
    updateProperties({ 'flowable:candidateGroups': roleIds });
  }
};
const dueDateCallBack = (data: string) => {
  updateProperties({ 'flowable:dueDate': data });
};
const taskTabClick = (e) => {
  formData.value.candidateGroups = '';
  formData.value.candidateUsers = '';
  formData.value.assignee = '';
  // formData.value.fixedAssignee = '';
  assignee.value = {};
};
const syncChange = (newVal) => {
  updateProperties({ 'flowable:async': newVal });
};
const skipExpressionChange = (newVal) => {
  updateProperties({ 'flowable:skipExpression': newVal && newVal.length > 0 ? newVal : undefined });
};
const priorityChange = (newVal) => {
  updateProperties({ 'flowable:priority': newVal });
};
const fixedAssigneeChange = (newVal) => {
  updateProperties({ 'flowable:assignee': newVal && newVal.length > 0 ? newVal : undefined });
};
const multiInstanceTypeChange = (newVal) => {
  if (newVal !== MultiInstanceTypeEnum.NONE) {
    let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
    if (!loopCharacteristics) {
      loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
    }
    loopCharacteristics.isSequential = newVal === MultiInstanceTypeEnum.SERIAL;
    updateProperties({ loopCharacteristics: loopCharacteristics });
  } else {
    updateProperties({ loopCharacteristics: undefined });
  }
};
const collectionChange = (newVal) => {
  let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
  if (!loopCharacteristics) {
    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
  }
  loopCharacteristics.collection = newVal && newVal.length > 0 ? newVal : undefined;
  updateProperties({ loopCharacteristics: loopCharacteristics });
};
const elementVariableChange = (newVal) => {
  let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
  if (!loopCharacteristics) {
    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
  }
  loopCharacteristics.elementVariable = newVal && newVal.length > 0 ? newVal : undefined;
  updateProperties({ loopCharacteristics: loopCharacteristics });
};
const completionConditionChange = (newVal) => {
  let loopCharacteristics = props.element.businessObject.get<ModdleElement>('loopCharacteristics');
  if (!loopCharacteristics) {
    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
  }
  if (newVal && newVal.length > 0) {
    if (!loopCharacteristics.completionCondition) {
      loopCharacteristics.completionCondition = createModdleElement('bpmn:Expression', { body: newVal }, loopCharacteristics);
    } else {
      loopCharacteristics.completionCondition.body = newVal;
    }
  } else {
    loopCharacteristics.completionCondition = undefined;
  }
  updateProperties({ loopCharacteristics: loopCharacteristics });
};
const dueDateChange = (newVal) => {
  updateProperties({ 'flowable:dueDate': newVal && newVal.length > 0 ? newVal : undefined });
};
const selectUserLength = computed(() => {
  if (formData.value.candidateUsers) {
    return formData.value.candidateUsers.split(',').length;
  } else {
    return 0;
  }
});
const selectRoleLength = computed(() => {
  if (formData.value.candidateGroups) {
    return formData.value.candidateGroups.split(',').length;
  } else {
    return 0;
  }
});
onBeforeMount(() => {
  const extensionElements = getExtensionElements(false);
  if (extensionElements && extensionElements.get('values')) {
    let extAssigneeElement = extensionElements.get('values').find((item) => item.$type === 'flowable:extAssignee');
    if (extAssigneeElement) {
      assignee.value = JSON.parse(extAssigneeElement.body);
    }
  }
  if (formData.value.loopCharacteristics) {
    const loopCharacteristics = formData.value.loopCharacteristics;
    formData.value.collection = loopCharacteristics.collection || '';
    formData.value.elementVariable = loopCharacteristics.elementVariable || '';
    formData.value.completionCondition = loopCharacteristics.completionCondition?.body || '';
    formData.value.multiInstanceType = loopCharacteristics.isSequential ? MultiInstanceTypeEnum.SERIAL : MultiInstanceTypeEnum.PARALLEL;
  }
  if (formData.value.assignee) {
    formData.value.fixedAssignee = formData.value.assignee;
  }
});
const formRules = ref<ElFormRules>({
  id: [{ required: true, message: '请输入', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }]
});
const AllocationTypeSelect = [
  { id: 'b9cdf970-dd91-47c0-819f-42a7010ca2a6', label: '指定人员', value: AllocationTypeEnum.USER },
  { id: '3f7ccbcd-c464-4602-bb9d-e96649d10585', label: '候选人员', value: AllocationTypeEnum.CANDIDATE },
  { id: 'c49065e0-7f2d-4c09-aedb-ab2d47d9a454', label: '发起人自己', value: AllocationTypeEnum.YOURSELF },
  { id: '6ef40a03-7e9a-4898-89b2-c88fe9064542', label: '发起人指定', value: AllocationTypeEnum.SPECIFY }
];
const SpecifyDesc = [
  { id: 'fa253b34-4335-458c-b1bc-b039e2a2b7a6', label: '指定一个人', value: 'specifySingle' },
  { id: '7365ff54-2e05-4312-9bfb-0b8edd779c5b', label: '指定多个人', value: 'specifyMultiple' }
];
const listFormManage = async () => {
  formManageListLoading.value = true;
  const res = await selectListFormManage();
  formManageList.value = res.data;
  formManageListLoading.value = false;
};
onMounted(() => {
  nextTick(() => {
    listFormManage();
  });
});
</script>
<style lang="scss" scoped></style>
src/bpmn/panel/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
<template>
  <div ref="propertyPanel">
    <div v-if="nodeName" class="node-name">{{ nodeName }}</div>
    <component :is="component" v-if="element" :element="element" />
  </div>
</template>
<script setup lang="ts" name="PropertyPanel">
import { NodeName } from '../assets/lang/zh';
import TaskPanel from './TaskPanel.vue';
import ProcessPanel from './ProcessPanel.vue';
import StartEndPanel from './StartEndPanel.vue';
import GatewayPanel from './GatewayPanel.vue';
import SequenceFlowPanel from './SequenceFlowPanel.vue';
import ParticipantPanel from './ParticipantPanel.vue';
import SubProcessPanel from './SubProcessPanel.vue';
import { Modeler, ModdleElement } from 'bpmn';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
interface propsType {
  modeler: Modeler;
}
const props = withDefaults(defineProps<propsType>(), {});
const element = ref<ModdleElement>();
const processElement = ref<ModdleElement>();
const startEndType = ['bpmn:IntermediateThrowEvent', 'bpmn:StartEvent', 'bpmn:EndEvent'];
const taskType = [
  'bpmn:UserTask',
  'bpmn:Task',
  'bpmn:SendTask',
  'bpmn:ReceiveTask',
  'bpmn:ManualTask',
  'bpmn:BusinessRuleTask',
  'bpmn:ServiceTask',
  'bpmn:ScriptTask'
];
const sequenceType = ['bpmn:SequenceFlow'];
const gatewayType = ['bpmn:InclusiveGateway', 'bpmn:ExclusiveGateway', 'bpmn:ParallelGateway', 'bpmn:EventBasedGateway', 'bpmn:ComplexGateway'];
const processType = ['bpmn:Process'];
// ç»„件计算
const component = computed(() => {
  if (!element.value) return null;
  const type = element.value.type;
  if (startEndType.includes(type)) return StartEndPanel;
  if (taskType.includes(type)) return TaskPanel;
  if (sequenceType.includes(type)) return SequenceFlowPanel;
  if (gatewayType.includes(type)) return GatewayPanel;
  if (processType.includes(type)) return ProcessPanel;
  if (type === 'bpmn:Participant') return ParticipantPanel;
  if (type === 'bpmn:SubProcess') return SubProcessPanel;
  //return proxy?.$modal.msgWarning('面板开发中....');
  return undefined;
});
const nodeName = computed(() => {
  if (element.value) {
    const bizObj = element.value.businessObject;
    const type = bizObj?.eventDefinitions && bizObj?.eventDefinitions.length > 0 ? bizObj.eventDefinitions[0].$type : bizObj.$type;
    return NodeName[type] || type;
  }
  return '';
});
const handleModeler = () => {
  props.modeler.on('root.added', (e: any) => {
    element.value = null;
    if (e.element.type === 'bpmn:Process') {
      nextTick(() => {
        element.value = e.element;
        processElement.value = e.element;
      });
    }
  });
  props.modeler.on('element.click', (e: any) => {
    if (e.element.type === 'bpmn:Process') {
      nextTick(() => {
        element.value = e.element;
        processElement.value = e.element;
      });
    }
  });
  props.modeler.on('selection.changed', (e: any) => {
    // å…ˆç»™null为了让vue刷新
    element.value = null;
    const newElement = e.newSelection[0];
    if (newElement) {
      nextTick(() => {
        element.value = newElement;
      });
    } else {
      nextTick(() => {
        element.value = processElement.value;
      });
    }
  });
};
onMounted(() => {
  handleModeler();
});
</script>
<style scoped lang="scss">
.node-name {
  font-size: 16px;
  font-weight: bold;
  padding: 10px;
}
</style>
src/bpmn/panel/property/DueDate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,252 @@
<template>
  <div>
    <el-dialog v-model="visible" :title="title" width="600px" append-to-body>
      <el-form label-width="100px">
        <el-form-item label="小时">
          <el-radio-group v-model="hourValue" @change="hourChange">
            <el-radio-button label="4" value="4" />
            <el-radio-button label="8" value="8" />
            <el-radio-button label="12" value="12" />
            <el-radio-button label="24" value="24" />
            <el-radio-button label="自定义" value="自定义" />
            <el-input-number v-show="hourValue === '自定义'" v-model="customHourValue" :min="1" @change="customHourValueChange"></el-input-number>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="天">
          <el-radio-group v-model="dayValue" @change="dayChange">
            <el-radio-button label="1" value="1" />
            <el-radio-button label="2" value="2" />
            <el-radio-button label="3" value="3" />
            <el-radio-button label="4" value="4" />
            <el-radio-button label="自定义" value="自定义" />
            <el-input-number v-show="dayValue === '自定义'" v-model="customDayValue" :min="1" @change="customDayValueChange"></el-input-number>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="周">
          <el-radio-group v-model="weekValue" @change="weekChange">
            <el-radio-button label="1" value="1" />
            <el-radio-button label="2" value="2" />
            <el-radio-button label="3" value="3" />
            <el-radio-button label="4" value="4" />
            <el-radio-button label="自定义" value="自定义" />
            <el-input-number v-show="weekValue === '自定义'" v-model="customWeekValue" :min="1" @change="customWeekValueChange"></el-input-number>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="月">
          <el-radio-group v-model="monthValue" @change="monthChange">
            <el-radio-button label="1" value="1" />
            <el-radio-button label="2" value="2" />
            <el-radio-button label="3" value="3" />
            <el-radio-button label="4" value="4" />
            <el-radio-button label="自定义" value="自定义" />
            <el-input-number v-show="monthValue === '自定义'" v-model="customMonthValue" :min="1" @change="customMonthValueChange"></el-input-number>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <div>
          <el-button @click="closeDialog">取消</el-button>
          <el-button type="primary" @click="confirm">确定</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import useDialog from '@/hooks/useDialog';
interface PropType {
  modelValue?: string;
  data?: string;
}
const prop = withDefaults(defineProps<PropType>(), {
  modelValue: '',
  data: ''
});
const emit = defineEmits(['update:modelValue', 'confirmCallBack']);
const { title, visible, openDialog, closeDialog } = useDialog({
  title: '设置任务到期时间'
});
const formValue = ref();
const valueType = ref();
const hourValue = ref('');
const dayValue = ref('');
const weekValue = ref('');
const monthValue = ref('');
const customHourValue = ref(1);
const customDayValue = ref(1);
const customWeekValue = ref(1);
const customMonthValue = ref(1);
const hourValueConst = ['4', '8', '12', '24'];
const dayAndWeekAndMonthValueConst = ['1', '2', '3', '4'];
const initValue = () => {
  formValue.value = prop.data;
  if (prop.data) {
    const lastStr = prop.data.substring(prop.data.length - 1);
    if (lastStr === 'H') {
      const hourValueValue = prop.data.substring(2, prop.data.length - 1);
      if (hourValueConst.includes(hourValueValue)) {
        hourValue.value = hourValueValue;
      } else {
        hourValue.value = '自定义';
        customHourValue.value = Number(hourValueValue);
      }
    }
    const dayAndWeekAndMonthValue = prop.data.substring(1, prop.data.length - 1);
    if (lastStr === 'D') {
      if (dayAndWeekAndMonthValueConst.includes(dayAndWeekAndMonthValue)) {
        dayValue.value = dayAndWeekAndMonthValue;
      } else {
        dayValue.value = '自定义';
        customDayValue.value = Number(dayAndWeekAndMonthValue);
      }
    }
    if (lastStr === 'W') {
      if (dayAndWeekAndMonthValueConst.includes(dayAndWeekAndMonthValue)) {
        weekValue.value = dayAndWeekAndMonthValue;
      } else {
        weekValue.value = '自定义';
        customWeekValue.value = Number(dayAndWeekAndMonthValue);
      }
    }
    if (lastStr === 'M') {
      if (dayAndWeekAndMonthValueConst.includes(dayAndWeekAndMonthValue)) {
        monthValue.value = dayAndWeekAndMonthValue;
      } else {
        monthValue.value = '自定义';
        customMonthValue.value = Number(dayAndWeekAndMonthValue);
      }
    }
  }
};
const confirm = () => {
  emit('update:modelValue', formValue.value);
  emit('confirmCallBack', formValue.value);
  closeDialog();
};
const customHourValueChange = (customHourValue) => {
  formValue.value = `PT${customHourValue}H`;
  dayValue.value = '';
  weekValue.value = '';
  monthValue.value = '';
  customDayValue.value = 1;
  customWeekValue.value = 1;
  customMonthValue.value = 1;
};
const customDayValueChange = (customDayValue) => {
  formValue.value = `P${customDayValue}D`;
  hourValue.value = '';
  weekValue.value = '';
  monthValue.value = '';
  customHourValue.value = 1;
  customWeekValue.value = 1;
  customMonthValue.value = 1;
};
const customWeekValueChange = (customWeekValue) => {
  formValue.value = `P${customWeekValue}W`;
  hourValue.value = '';
  dayValue.value = '';
  monthValue.value = '';
  customHourValue.value = 1;
  customDayValue.value = 1;
  customMonthValue.value = 1;
};
const customMonthValueChange = (customMonthValue) => {
  formValue.value = `P${customMonthValue}M`;
  hourValue.value = '';
  dayValue.value = '';
  weekValue.value = '';
  customHourValue.value = 1;
  customDayValue.value = 1;
  customWeekValue.value = 1;
};
const hourChange = (hourValue) => {
  if (hourValue === '自定义') {
    formValue.value = `PT${customHourValue.value}H`;
  } else {
    formValue.value = `PT${hourValue}H`;
  }
  dayValue.value = '';
  weekValue.value = '';
  monthValue.value = '';
  customDayValue.value = 1;
  customWeekValue.value = 1;
  customMonthValue.value = 1;
};
const dayChange = (dayValue) => {
  if (dayValue === '自定义') {
    formValue.value = `P${customDayValue.value}D`;
  } else {
    formValue.value = `P${dayValue}D`;
  }
  hourValue.value = '';
  weekValue.value = '';
  monthValue.value = '';
  customHourValue.value = 1;
  customWeekValue.value = 1;
  customMonthValue.value = 1;
};
const weekChange = (weekValue) => {
  if (weekValue === '自定义') {
    formValue.value = `P${customWeekValue.value}W`;
  } else {
    formValue.value = `P${weekValue}W`;
  }
  hourValue.value = '';
  dayValue.value = '';
  monthValue.value = '';
  customHourValue.value = 1;
  customDayValue.value = 1;
  customMonthValue.value = 1;
};
const monthChange = (monthValue) => {
  if (monthValue === '自定义') {
    formValue.value = `P${customMonthValue.value}M`;
  } else {
    formValue.value = `P${monthValue}M`;
  }
  hourValue.value = '';
  dayValue.value = '';
  weekValue.value = '';
  customHourValue.value = 1;
  customDayValue.value = 1;
  customWeekValue.value = 1;
};
watch(
  () => visible.value,
  () => {
    if (visible.value) {
      initValue();
    }
  }
);
defineExpose({
  openDialog,
  closeDialog
});
</script>
src/bpmn/panel/property/ExecutionListener.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,308 @@
<template>
  <div>
    <vxe-toolbar>
      <template #buttons>
        <el-button type="primary" link size="small" @click="insertEvent">新增</el-button>
        <el-button type="primary" link size="small" @click="removeSelectRowEvent">删除</el-button>
      </template>
    </vxe-toolbar>
    <vxe-table
      ref="tableRef"
      size="mini"
      height="100px"
      border
      show-overflow
      keep-source
      :data="tableData"
      :menu-config="menuConfig"
      @cell-dblclick="cellDBLClickEvent"
      @menu-click="contextMenuClickEvent"
    >
      <vxe-column type="checkbox" width="40"></vxe-column>
      <vxe-column type="seq" width="40"></vxe-column>
      <vxe-column field="event" title="事件" min-width="100px">
        <template #default="slotParams">
          <span>{{ eventSelect.find((e) => e.value === slotParams.row.event)?.label }}</span>
        </template>
      </vxe-column>
      <vxe-column field="type" title="类型" min-width="100px">
        <template #default="slotParams">
          <span>{{ typeSelect.find((e) => e.value === slotParams.row.type)?.label }}</span>
        </template>
      </vxe-column>
      <vxe-column field="className" title="Java ç±»å" min-width="100px"> </vxe-column>
    </vxe-table>
    <el-dialog
      v-model="formDialog.visible.value"
      :title="formDialog.title.value"
      width="600px"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      :show-close="false"
      append-to-body
    >
      <el-form ref="formRef" :model="formData" :rules="tableRules" label-width="100px">
        <el-form-item label="事件" prop="event">
          <el-select v-model="formData.event">
            <el-option v-for="item in eventSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="类型" prop="type">
          <template #label>
            <span>
              ç±»åž‹
              <el-tooltip placement="top">
                <el-icon><QuestionFilled /></el-icon>
                <template #content>
                  ç±»ï¼šç¤ºä¾‹ com.company.MyCustomListener,自定义类必须实现 org.flowable.engine.delegate.TaskListener æŽ¥å£<br />
                  è¡¨è¾¾å¼ï¼šç¤ºä¾‹ ${myObject.callMethod(task, task.eventName)}<br />
                  å§”托表达式:示例 ${myListenerSpringBean} ï¼Œè¯¥ springBean éœ€è¦å®žçް org.flowable.engine.delegate.TaskListener æŽ¥å£
                </template>
              </el-tooltip>
            </span>
          </template>
          <el-select v-model="formData.type">
            <el-option v-for="item in typeSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item
          :label="typeSelect.filter((e) => e.value === formData.type)[0] ? typeSelect.filter((e) => e.value === formData.type)[0]?.label : '表达式'"
          prop="className"
        >
          <el-input v-model="formData.className" type="text"></el-input>
        </el-form-item>
      </el-form>
      <el-tabs type="border-card">
        <el-tab-pane label="参数">
          <ListenerParam ref="listenerParamRef" :table-data="formData.params" />
        </el-tab-pane>
      </el-tabs>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="formDialog.closeDialog">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitEvent">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import ListenerParam from './ListenerParam.vue';
import { VxeTableEvents, VxeTableInstance, VxeTablePropTypes } from 'vxe-table';
import { ExecutionListenerVO } from 'bpmnDesign';
import { Moddle, Modeler, ModdleElement } from 'bpmn';
import usePanel from '../../hooks/usePanel';
import useDialog from '@/hooks/useDialog';
import useModelerStore from '@/store/modules/modeler';
const emit = defineEmits(['close']);
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const selectRow = ref<ExecutionListenerVO | null>();
const formDialog = useDialog({
  title: selectRow.value ? '编辑&保存' : '新增&保存'
});
const { showConfig, elementType, updateProperties } = usePanel({
  element: toRaw(props.element)
});
const { getModdle } = useModelerStore();
const moddle = getModdle();
const listenerParamRef = ref<InstanceType<typeof ListenerParam>>();
const tableRef = ref<VxeTableInstance<ExecutionListenerVO>>();
const formRef = ref<ElFormInstance>();
const initData: ExecutionListenerVO = {
  event: '',
  type: '',
  className: '',
  params: []
};
const formData = ref<ExecutionListenerVO>({ ...initData });
const tableData = ref<ExecutionListenerVO[]>([]);
const tableRules = ref<ElFormRules>({
  event: [{ required: true, message: '请选择', trigger: 'blur' }],
  type: [{ required: true, message: '请选择', trigger: 'blur' }],
  className: [{ required: true, message: '请输入', trigger: 'blur' }]
});
const submitEvent = async () => {
  const error = await listenerParamRef.value.validate();
  await formRef.value.validate((validate) => {
    if (validate && !error) {
      const $table = tableRef.value;
      if ($table) {
        formData.value.params = listenerParamRef.value.getTableData();
        if (selectRow.value) {
          Object.assign(selectRow.value, formData.value);
        } else {
          $table.insertAt({ ...formData.value }, -1);
        }
        updateElement();
        formDialog.closeDialog();
      }
    }
  });
};
const removeSelectRowEvent = async () => {
  const $table = tableRef.value;
  if ($table) {
    const selectCount = $table.getCheckboxRecords().length;
    if (selectCount === 0) {
      proxy?.$modal.msgWarning('请选择行');
    } else {
      await $table.removeCheckboxRow();
      updateElement();
    }
  }
};
const insertEvent = async () => {
  Object.assign(formData.value, initData);
  selectRow.value = null;
  formDialog.openDialog();
};
const editEvent = (row: ExecutionListenerVO) => {
  Object.assign(formData.value, row);
  selectRow.value = row;
  formDialog.openDialog();
};
const removeEvent = async (row: ExecutionListenerVO) => {
  await proxy?.$modal.confirm('您确定要删除该数据?');
  const $table = tableRef.value;
  if ($table) {
    await $table.remove(row);
    updateElement();
  }
};
const updateElement = () => {
  const $table = tableRef.value;
  const data = $table.getTableData().fullData;
  if (data.length) {
    let extensionElements = props.element.businessObject.get('extensionElements');
    if (!extensionElements) {
      extensionElements = moddle.create('bpmn:ExtensionElements');
    }
    // æ¸…除旧值
    extensionElements.values = extensionElements.values?.filter((item) => item.$type !== 'flowable:ExecutionListener') ?? [];
    data.forEach((item) => {
      const executionListener = moddle.create('flowable:ExecutionListener');
      executionListener['event'] = item.event;
      executionListener[item.type] = item.className;
      if (item.params && item.params.length) {
        item.params.forEach((field) => {
          const fieldElement = moddle.create('flowable:Field');
          fieldElement['name'] = field.name;
          fieldElement[field.type] = field.value;
          executionListener.get('fields').push(fieldElement);
        });
      }
      extensionElements.get('values').push(executionListener);
    });
    updateProperties({ extensionElements: extensionElements });
  } else {
    const extensionElements = props.element.businessObject[`extensionElements`];
    if (extensionElements) {
      extensionElements.values = extensionElements.values?.filter((item) => item.$type !== 'flowable:ExecutionListener') ?? [];
    }
  }
};
const cellDBLClickEvent: VxeTableEvents.CellDblclick<ExecutionListenerVO> = ({ row }) => {
  editEvent(row);
};
const menuConfig = reactive<VxeTablePropTypes.MenuConfig<ExecutionListenerVO>>({
  body: {
    options: [
      [
        { code: 'edit', name: '编辑', prefixIcon: 'vxe-icon-edit', disabled: false },
        { code: 'remove', name: '删除', prefixIcon: 'vxe-icon-delete', disabled: false }
      ]
    ]
  },
  visibleMethod({ options, column }) {
    const isDisabled = !column;
    options.forEach((list) => {
      list.forEach((item) => {
        item.disabled = isDisabled;
      });
    });
    return true;
  }
});
const contextMenuClickEvent: VxeTableEvents.MenuClick<ExecutionListenerVO> = ({ menu, row, column }) => {
  const $table = tableRef.value;
  if ($table) {
    switch (menu.code) {
      case 'edit':
        editEvent(row);
        break;
      case 'remove':
        removeEvent(row);
        break;
    }
  }
};
const initTableData = () => {
  tableData.value =
    props.element.businessObject.extensionElements?.values
      .filter((item) => item.$type === 'flowable:ExecutionListener')
      .map((item) => {
        let type;
        if ('class' in item) type = 'class';
        if ('expression' in item) type = 'expression';
        if ('delegateExpression' in item) type = 'delegateExpression';
        return {
          event: item.event,
          type: type,
          className: item[type],
          params:
            item.fields?.map((field) => {
              let fieldType;
              if ('stringValue' in field) fieldType = 'stringValue';
              if ('expression' in field) fieldType = 'expression';
              return {
                name: field.name,
                type: fieldType,
                value: field[fieldType]
              };
            }) ?? []
        };
      }) ?? [];
};
onMounted(() => {
  initTableData();
});
const typeSelect = [
  { id: '742fdeb7-23b4-416b-ac66-cd4ec8b901b7', label: 'ç±»', value: 'class' },
  { id: '660c9c46-8fae-4bae-91a0-0335420019dc', label: '表达式', value: 'expression' },
  { id: '4b8135ab-6bc3-4a0f-80be-22f58bc6c5fd', label: '委托表达式', value: 'delegateExpression' }
];
const eventSelect = [
  { id: 'e6e0a51a-2d5d-4dc4-b847-b5c14f43a6ab', label: '开始', value: 'start' },
  { id: '6da97c1e-15fc-4445-8943-75d09f49778e', label: '结束', value: 'end' },
  { id: '6a2cbcec-e026-4f11-bef7-fff0b5c871e2', label: '启用', value: 'take' }
];
</script>
<style scoped lang="scss">
.el-badge {
  :deep(.el-badge__content) {
    top: 10px;
  }
}
</style>
src/bpmn/panel/property/ListenerParam.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
<template>
  <vxe-toolbar>
    <template #buttons>
      <el-button icon="Plus" @click="insertRow">新增</el-button>
    </template>
  </vxe-toolbar>
  <vxe-table
    ref="tableRef"
    :height="height"
    border
    show-overflow
    keep-source
    :data="tableData"
    :edit-rules="tableRules"
    :edit-config="{ trigger: 'click', mode: 'row', showStatus: true }"
  >
    <vxe-column type="seq" width="40"></vxe-column>
    <vxe-column field="type" title="类型" :edit-render="{}">
      <template #default="slotParams">
        <span>{{ typeSelect.find((e) => e.value === slotParams.row.type)?.label }}</span>
      </template>
      <template #edit="slotParams">
        <vxe-select v-model="slotParams.row.type">
          <vxe-option v-for="item in typeSelect" :key="item.id" :value="item.value" :label="item.label"></vxe-option>
        </vxe-select>
      </template>
    </vxe-column>
    <vxe-column field="name" title="名称" :edit-render="{}">
      <template #edit="slotParams">
        <vxe-input v-model="slotParams.row.name" type="text"></vxe-input>
      </template>
    </vxe-column>
    <vxe-column field="value" title="值" :edit-render="{}">
      <template #edit="slotParams">
        <vxe-input v-model="slotParams.row.value" type="text"></vxe-input>
      </template>
    </vxe-column>
    <vxe-column title="操作" width="100" show-overflow align="center">
      <template #default="slotParams">
        <el-tooltip content="删除" placement="top">
          <el-button link type="danger" icon="Delete" @click="removeRow(slotParams.row)"></el-button>
        </el-tooltip>
      </template>
    </vxe-column>
  </vxe-table>
</template>
<script setup lang="ts">
import { VXETable, VxeTableInstance, VxeTablePropTypes } from 'vxe-table';
import { ParamVO } from 'bpmnDesign';
import useDialog from '@/hooks/useDialog';
interface PropType {
  height?: string;
  tableData?: ParamVO[];
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = withDefaults(defineProps<PropType>(), {
  height: '200px',
  tableData: () => []
});
const tableRules = ref<VxeTablePropTypes.EditRules>({
  type: [{ required: true, message: '请选择', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }],
  value: [{ required: true, message: '请输入', trigger: 'blur' }]
});
const { title, visible, openDialog, closeDialog } = useDialog({
  title: '监听器参数'
});
const typeSelect = [
  { id: '742fdeb7-23b4-416b-ac66-cd4ec8b901b7', label: '字符串', value: 'stringValue' },
  { id: '660c9c46-8fae-4bae-91a0-0335420019dc', label: '表达式', value: 'expression' }
];
const tableRef = ref<VxeTableInstance<ParamVO>>();
const getTableData = () => {
  const $table = tableRef.value;
  if ($table) {
    return $table.getTableData().fullData;
  }
  return [];
};
const insertRow = async () => {
  const $table = tableRef.value;
  if ($table) {
    const { row: newRow } = await $table.insertAt({}, -1);
    // æ’入一条数据并触发校验
    await $table.validate(newRow);
  }
};
const removeRow = async (row: ParamVO) => {
  await proxy?.$modal.confirm('您确定要删除该数据?');
  const $table = tableRef.value;
  if ($table) {
    await $table.remove(row);
  }
};
const validate = async () => {
  const $table = tableRef.value;
  if ($table) {
    return await $table.validate(true);
  }
};
defineExpose({
  closeDialog,
  openDialog,
  validate,
  getTableData
});
</script>
<style scoped lang="scss"></style>
src/bpmn/panel/property/TaskListener.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,310 @@
<template>
  <div>
    <vxe-toolbar>
      <template #buttons>
        <el-button type="primary" link size="small" @click="insertEvent">新增</el-button>
        <el-button type="primary" link size="small" @click="removeSelectRowEvent">删除</el-button>
      </template>
    </vxe-toolbar>
    <vxe-table
      ref="tableRef"
      size="mini"
      height="100px"
      border
      show-overflow
      keep-source
      :data="tableData"
      :menu-config="menuConfig"
      @cell-dblclick="cellDBLClickEvent"
      @menu-click="contextMenuClickEvent"
    >
      <vxe-column type="checkbox" width="40"></vxe-column>
      <vxe-column type="seq" width="40"></vxe-column>
      <vxe-column field="event" title="事件" min-width="100px">
        <template #default="slotParams">
          <span>{{ eventSelect.find((e) => e.value === slotParams.row.event)?.label }}</span>
        </template>
      </vxe-column>
      <vxe-column field="type" title="类型" min-width="100px">
        <template #default="slotParams">
          <span>{{ typeSelect.find((e) => e.value === slotParams.row.type)?.label }}</span>
        </template>
      </vxe-column>
      <vxe-column field="className" title="Java ç±»å" min-width="100px"> </vxe-column>
    </vxe-table>
    <el-dialog
      v-model="formDialog.visible.value"
      :title="formDialog.title.value"
      width="600px"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      :show-close="false"
      append-to-body
    >
      <el-form ref="formRef" :model="formData" :rules="tableRules" label-width="100px">
        <el-form-item label="事件" prop="event">
          <template #label>
            <span>
              äº‹ä»¶
              <el-tooltip placement="top">
                <el-icon><QuestionFilled /></el-icon>
                <template #content>
                  create(创建):当任务已经创建,并且所有任务参数都已经设置时触发。<br />
                  assignment(指派):当任务已经指派给某人时触发。请注意:当流程执行到达用户任务时,在触发create事件之前,会首先触发assignment事件。<br />
                  complete(完成):当任务已经完成,从运行时数据中删除前触发。<br />
                  delete(删除):在任务即将被删除前触发。请注意任务由completeTask正常完成时也会触发。
                </template>
              </el-tooltip>
            </span>
          </template>
          <el-select v-model="formData.event">
            <el-option v-for="item in eventSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="类型" prop="type">
          <el-select v-model="formData.type">
            <el-option v-for="item in typeSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item
          :label="typeSelect.filter((e) => e.value === formData.type)[0] ? typeSelect.filter((e) => e.value === formData.type)[0]?.label : '表达式'"
          prop="className"
        >
          <el-input v-model="formData.className" type="text"></el-input>
        </el-form-item>
      </el-form>
      <el-tabs type="border-card">
        <el-tab-pane label="参数">
          <ListenerParam ref="listenerParamRef" :table-data="formData.params" />
        </el-tab-pane>
      </el-tabs>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="formDialog.closeDialog">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitEvent">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import ListenerParam from './ListenerParam.vue';
import { VxeTableEvents, VxeTableInstance, VxeTablePropTypes } from 'vxe-table';
import { TaskListenerVO } from 'bpmnDesign';
import { ModdleElement } from 'bpmn';
import usePanel from '../../hooks/usePanel';
import useDialog from '@/hooks/useDialog';
import useModelerStore from '@/store/modules/modeler';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
interface PropType {
  element: ModdleElement;
}
const props = withDefaults(defineProps<PropType>(), {});
const selectRow = ref<TaskListenerVO | null>();
const formDialog = useDialog({
  title: selectRow.value ? '编辑&保存' : '新增&保存'
});
const { showConfig, elementType, updateProperties } = usePanel({
  element: toRaw(props.element)
});
const { getModdle } = useModelerStore();
const moddle = getModdle();
const listenerParamRef = ref<InstanceType<typeof ListenerParam>>();
const tableRef = ref<VxeTableInstance<TaskListenerVO>>();
const formRef = ref<ElFormInstance>();
const initData: TaskListenerVO = {
  event: '',
  type: '',
  className: '',
  name: '',
  params: []
};
const formData = ref<TaskListenerVO>({ ...initData });
const currentIndex = ref(0);
const tableData = ref<TaskListenerVO[]>([]);
const tableRules = ref<VxeTablePropTypes.EditRules>({
  event: [{ required: true, message: '请选择', trigger: 'blur' }],
  type: [{ required: true, message: '请选择', trigger: 'blur' }],
  name: [{ required: true, message: '请输入', trigger: 'blur' }],
  className: [{ required: true, message: '请输入', trigger: 'blur' }]
});
const submitEvent = async () => {
  const error = await listenerParamRef.value.validate();
  await formRef.value.validate((validate) => {
    if (validate && !error) {
      const $table = tableRef.value;
      if ($table) {
        formData.value.params = listenerParamRef.value.getTableData();
        if (selectRow.value) {
          Object.assign(selectRow.value, formData.value);
        } else {
          $table.insertAt({ ...formData.value }, -1);
        }
        updateElement();
        formDialog.closeDialog();
      }
    }
  });
};
const insertEvent = async () => {
  Object.assign(formData.value, initData);
  selectRow.value = null;
  formDialog.openDialog();
};
const editEvent = (row: TaskListenerVO) => {
  Object.assign(formData.value, row);
  selectRow.value = row;
  formDialog.openDialog();
};
const removeEvent = async (row: TaskListenerVO) => {
  await proxy?.$modal.confirm('您确定要删除该数据?');
  const $table = tableRef.value;
  if ($table) {
    await $table.remove(row);
    updateElement();
  }
};
const removeSelectRowEvent = async () => {
  const $table = tableRef.value;
  if ($table) {
    const selectCount = $table.getCheckboxRecords().length;
    if (selectCount === 0) {
      proxy?.$modal.msgWarning('请选择行');
    } else {
      await $table.removeCheckboxRow();
      updateElement();
    }
  }
};
const updateElement = () => {
  const $table = tableRef.value;
  const data = $table.getTableData().fullData;
  if (data.length) {
    let extensionElements = props.element.businessObject.get('extensionElements');
    if (!extensionElements) {
      extensionElements = moddle.create('bpmn:ExtensionElements');
    }
    // æ¸…除旧值
    extensionElements.values = extensionElements.values?.filter((item) => item.$type !== 'flowable:TaskListener') ?? [];
    data.forEach((item) => {
      const taskListener = moddle.create('flowable:TaskListener');
      taskListener['event'] = item.event;
      taskListener[item.type] = item.className;
      if (item.params && item.params.length) {
        item.params.forEach((field) => {
          const fieldElement = moddle.create('flowable:Field');
          fieldElement['name'] = field.name;
          fieldElement[field.type] = field.value;
          taskListener.get('fields').push(fieldElement);
        });
      }
      extensionElements.get('values').push(taskListener);
    });
    updateProperties({ extensionElements: extensionElements });
  } else {
    const extensionElements = props.element.businessObject[`extensionElements`];
    if (extensionElements) {
      extensionElements.values = extensionElements.values?.filter((item) => item.$type !== 'flowable:TaskListener') ?? [];
    }
  }
};
const cellDBLClickEvent: VxeTableEvents.CellDblclick<TaskListenerVO> = ({ row }) => {
  editEvent(row);
};
const menuConfig = reactive<VxeTablePropTypes.MenuConfig<TaskListenerVO>>({
  body: {
    options: [
      [
        { code: 'edit', name: '编辑', prefixIcon: 'vxe-icon-edit', disabled: false },
        { code: 'remove', name: '删除', prefixIcon: 'vxe-icon-delete', disabled: false }
      ]
    ]
  },
  visibleMethod({ options, column }) {
    const isDisabled = !column;
    options.forEach((list) => {
      list.forEach((item) => {
        item.disabled = isDisabled;
      });
    });
    return true;
  }
});
const contextMenuClickEvent: VxeTableEvents.MenuClick<TaskListenerVO> = ({ menu, row, column }) => {
  const $table = tableRef.value;
  if ($table) {
    switch (menu.code) {
      case 'edit':
        editEvent(row);
        break;
      case 'remove':
        removeEvent(row);
        break;
    }
  }
};
const initTableData = () => {
  tableData.value =
    props.element.businessObject.extensionElements?.values
      .filter((item) => item.$type === 'flowable:TaskListener')
      .map((item) => {
        let type;
        if ('class' in item) type = 'class';
        if ('expression' in item) type = 'expression';
        if ('delegateExpression' in item) type = 'delegateExpression';
        return {
          event: item.event,
          type: type,
          className: item[type],
          params:
            item.fields?.map((field) => {
              let fieldType;
              if ('stringValue' in field) fieldType = 'stringValue';
              if ('expression' in field) fieldType = 'expression';
              return {
                name: field.name,
                type: fieldType,
                value: field[fieldType]
              };
            }) ?? []
        };
      }) ?? [];
};
onMounted(() => {
  initTableData();
});
const typeSelect = [
  { id: '742fdeb7-23b4-416b-ac66-cd4ec8b901b7', label: 'ç±»', value: 'class' },
  { id: '660c9c46-8fae-4bae-91a0-0335420019dc', label: '表达式', value: 'expression' },
  { id: '4b8135ab-6bc3-4a0f-80be-22f58bc6c5fd', label: '委托表达式', value: 'delegateExpression' }
];
const eventSelect = [
  { id: 'e6e0a51a-2d5d-4dc4-b847-b5c14f43a6ab', label: '创建', value: 'create' },
  { id: '6da97c1e-15fc-4445-8943-75d09f49778e', label: '指派', value: 'assignment' },
  { id: '6a2cbcec-e026-4f11-bef7-fff0b5c871e2', label: '完成', value: 'complete' },
  { id: '68801972-85f1-482f-bd86-1fad015c26ed', label: '删除', value: 'delete' }
];
</script>
<style scoped lang="scss">
.el-badge {
  :deep(.el-badge__content) {
    top: 10px;
  }
}
</style>
src/components/BpmnDesign/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
<template>
  <div class="design">
    <el-dialog v-model="visible" width="100%" fullscreen :title="title">
      <div class="modeler">
        <bpmn-design ref="bpmnDesignRef" @save-call-back="saveCallBack"></bpmn-design>
      </div>
    </el-dialog>
  </div>
</template>
<script lang="ts" setup name="Design">
import { getInfo, editModelXml } from '@/api/workflow/model';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { ModelForm } from '@/api/workflow/model/types';
import BpmnDesign from '@/bpmn/index.vue';
import useDialog from '@/hooks/useDialog';
const bpmnDesignRef = ref<InstanceType<typeof BpmnDesign>>();
const modelForm = ref<ModelForm>();
const emit = defineEmits(['closeCallBack']);
const { visible, title } = useDialog({
  title: '编辑流程'
});
const modelId = ref('');
const open = async (id) => {
  visible.value = true;
  modelId.value = id;
  const { data } = await getInfo(id);
  modelForm.value = data;
  bpmnDesignRef.value.initDiagram(modelForm.value.xml);
};
//保存模型
const saveCallBack = async (data) => {
  await proxy?.$modal.confirm('是否确认保存?');
  data.loading.value = true;
  modelForm.value.id = modelId.value;
  modelForm.value.xml = data.xml;
  modelForm.value.svg = data.svg;
  modelForm.value.key = data.key;
  modelForm.value.name = data.name;
  editModelXml(modelForm.value).then((res) => {
    if (res.code === 200) {
      visible.value = false;
      proxy?.$modal.msgSuccess('保存成功');
      emit('closeCallBack', data);
    }
  });
  data.loading.value = false;
};
/**
 * å¯¹å¤–暴露子组件方法
 */
defineExpose({
  open
});
</script>
<style lang="scss" scoped>
.design {
  :deep(.el-dialog .el-dialog__body) {
    max-height: 100% !important;
    min-height: calc(100vh - 80px);
    padding: 10px 0 10px 0 !important;
  }
  :deep(.el-dialog__header) {
    padding: 0 0 5px 0 !important;
  }
}
</style>
src/components/BpmnView/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,410 @@
<template>
  <div v-loading="loading" class="bpmnDialogContainers">
    <el-header style="border-bottom: 1px solid rgb(218 218 218); height: auto">
      <div class="header-div">
        <div>
          <el-tooltip effect="dark" content="自适应屏幕" placement="bottom">
            <el-button size="small" icon="Rank" @click="fitViewport" />
          </el-tooltip>
          <el-tooltip effect="dark" content="放大" placement="bottom">
            <el-button size="small" icon="ZoomIn" @click="zoomViewport(true)" />
          </el-tooltip>
          <el-tooltip effect="dark" content="缩小" placement="bottom">
            <el-button size="small" icon="ZoomOut" @click="zoomViewport(false)" />
          </el-tooltip>
        </div>
        <div>
          <div class="tips-label">
            <div class="un-complete">未完成</div>
            <div class="in-progress">进行中</div>
            <div class="complete">已完成</div>
          </div>
        </div>
      </div>
    </el-header>
    <div class="flow-containers">
      <el-container class="bpmn-el-container" style="align-items: stretch">
        <el-main style="padding: 0">
          <div ref="canvas" class="canvas" />
        </el-main>
      </el-container>
    </div>
  </div>
</template>
<script lang="ts" setup>
import BpmnViewer from 'bpmn-js/lib/Viewer';
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
import { ModuleDeclaration } from 'didi';
import { Canvas, ModdleElement } from 'bpmn';
import EventBus from 'diagram-js/lib/core/EventBus';
import Overlays from 'diagram-js/lib/features/overlays/Overlays';
import processApi from '@/api/workflow/processInstance/index';
const canvas = ref<HTMLElement>();
const modeler = ref<BpmnViewer>();
const taskList = ref([]);
const zoom = ref(1);
const xml = ref('');
const loading = ref(false);
const bpmnVisible = ref(true);
const historyList = ref([]);
const init = (instanceId) => {
  loading.value = true;
  bpmnVisible.value = true;
  nextTick(async () => {
    if (modeler.value) modeler.value.destroy();
    modeler.value = new BpmnViewer({
      container: canvas.value,
      additionalModules: [
        {
          //禁止滚轮滚动
          zoomScroll: ['value', '']
        },
        ZoomScrollModule,
        MoveCanvasModule
      ] as ModuleDeclaration[]
    });
    const resp = await processApi.getHistoryList(instanceId);
    xml.value = resp.data.xml;
    taskList.value = resp.data.taskList;
    historyList.value = resp.data.historyList;
    await createDiagram(xml.value);
    loading.value = false;
  });
};
const initXml = (xmlStr: string) => {
  loading.value = true;
  bpmnVisible.value = true;
  nextTick(async () => {
    if (modeler.value) modeler.value.destroy();
    modeler.value = new BpmnViewer({
      container: canvas.value,
      additionalModules: [
        {
          //禁止滚轮滚动
          zoomScroll: ['value', '']
        },
        ZoomScrollModule,
        MoveCanvasModule
      ] as ModuleDeclaration[]
    });
    xml.value = xmlStr;
    await createDiagram(xml.value);
    loading.value = false;
  });
};
const createDiagram = async (data) => {
  try {
    await modeler.value.importXML(data);
    fitViewport();
    fillColor();
    loading.value = false;
    addEventBusListener();
  } catch (err) {
    console.log(err);
  }
};
const addEventBusListener = () => {
  const eventBus = modeler.value.get<EventBus>('eventBus');
  const overlays = modeler.value.get<Overlays>('overlays');
  eventBus.on<ModdleElement>('element.hover', (e) => {
    let data = historyList.value.find((t) => t.taskDefinitionKey === e.element.id);
    if (e.element.type === 'bpmn:UserTask' && data) {
      setTimeout(() => {
        genNodeDetailBox(e, overlays, data);
      }, 10);
    }
  });
  eventBus.on('element.out', (e) => {
    overlays.clear();
  });
};
const genNodeDetailBox = (e, overlays, data) => {
  overlays.add(e.element.id, {
    position: { top: e.element.height, left: 0 },
    html: `<div class="verlays">
                    <p>审批人员: ${data.nickName || ''}<p/>
                    <p>节点状态:${data.status || ''}</p>
                    <p>开始时间:${data.startTime || ''}</p>
                    <p>结束时间:${data.endTime || ''}</p>
                    <p>审批耗时:${data.runDuration || ''}</p>
                   </div>`
  });
};
// è®©å›¾èƒ½è‡ªé€‚应屏幕
const fitViewport = () => {
  zoom.value = modeler.value.get<Canvas>('canvas').zoom('fit-viewport');
  const bbox = document.querySelector<SVGGElement>('.flow-containers .viewport').getBBox();
  const currentViewBox = modeler.value.get('canvas').viewbox();
  const elementMid = {
    x: bbox.x + bbox.width / 2 - 65,
    y: bbox.y + bbox.height / 2
  };
  modeler.value.get<Canvas>('canvas').viewbox({
    x: elementMid.x - currentViewBox.width / 2,
    y: elementMid.y - currentViewBox.height / 2,
    width: currentViewBox.width,
    height: currentViewBox.height
  });
  zoom.value = (bbox.width / currentViewBox.width) * 1.8;
};
// æ”¾å¤§ç¼©å°
const zoomViewport = (zoomIn = true) => {
  zoom.value = modeler.value.get<Canvas>('canvas').zoom();
  zoom.value += zoomIn ? 0.1 : -0.1;
  modeler.value.get<Canvas>('canvas').zoom(zoom.value);
};
//上色
const fillColor = () => {
  const canvas = modeler.value.get<Canvas>('canvas');
  bpmnNodeList(modeler.value._definitions.rootElements[0].flowElements, canvas);
};
//递归上色
const bpmnNodeList = (flowElements, canvas) => {
  flowElements.forEach((n) => {
    if (n.$type === 'bpmn:UserTask') {
      const completeTask = taskList.value.find((m) => m.key === n.id);
      if (completeTask) {
        canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo');
        n.outgoing?.forEach((nn) => {
          const targetTask = taskList.value.find((m) => m.key === nn.targetRef.id);
          if (targetTask) {
            canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo');
          } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
            canvas.addMarker(nn.id, completeTask.completed ? 'highlight' : 'highlight-todo');
            canvas.addMarker(nn.targetRef.id, completeTask.completed ? 'highlight' : 'highlight-todo');
            nn.targetRef.outgoing.forEach((e) => {
              gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed);
            });
          } else if (nn.targetRef.$type === 'bpmn:ParallelGateway') {
            canvas.addMarker(nn.id, completeTask.completed ? 'highlight' : 'highlight-todo');
            canvas.addMarker(nn.targetRef.id, completeTask.completed ? 'highlight' : 'highlight-todo');
            nn.targetRef.outgoing.forEach((e) => {
              gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed);
            });
          } else if (nn.targetRef.$type === 'bpmn:InclusiveGateway') {
            canvas.addMarker(nn.id, completeTask.completed ? 'highlight' : 'highlight-todo');
            canvas.addMarker(nn.targetRef.id, completeTask.completed ? 'highlight' : 'highlight-todo');
            nn.targetRef.outgoing.forEach((e) => {
              gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed);
            });
          }
        });
      }
    } else if (n.$type === 'bpmn:ExclusiveGateway') {
      n.outgoing.forEach((nn) => {
        const targetTask = taskList.value.find((m) => m.key === nn.targetRef.id);
        if (targetTask) {
          canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo');
        }
      });
    } else if (n.$type === 'bpmn:ParallelGateway') {
      n.outgoing.forEach((nn) => {
        const targetTask = taskList.value.find((m) => m.key === nn.targetRef.id);
        if (targetTask) {
          canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo');
        }
      });
    } else if (n.$type === 'bpmn:InclusiveGateway') {
      n.outgoing.forEach((nn) => {
        const targetTask = taskList.value.find((m) => m.key === nn.targetRef.id);
        if (targetTask) {
          canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo');
        }
      });
    } else if (n.$type === 'bpmn:SubProcess') {
      const completeTask = taskList.value.find((m) => m.key === n.id);
      if (completeTask) {
        canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo');
      }
      bpmnNodeList(n.flowElements, canvas);
    } else if (n.$type === 'bpmn:StartEvent') {
      canvas.addMarker(n.id, 'startEvent');
      if (n.outgoing) {
        n.outgoing.forEach((nn) => {
          const completeTask = taskList.value.find((m) => m.key === nn.targetRef.id);
          if (completeTask) {
            canvas.addMarker(nn.id, 'highlight');
            canvas.addMarker(n.id, 'highlight');
          }
        });
      }
    } else if (n.$type === 'bpmn:EndEvent') {
      canvas.addMarker(n.id, 'endEvent');
      const completeTask = taskList.value.find((m) => m.key === n.id);
      if (completeTask) {
        canvas.addMarker(completeTask.key, 'highlight');
        canvas.addMarker(n.id, 'highlight');
        return;
      }
    }
  });
};
const gateway = (id, targetRefType, targetRefId, canvas, completed) => {
  if (targetRefType === 'bpmn:ExclusiveGateway') {
    canvas.addMarker(id, completed ? 'highlight' : 'highlight-todo');
    canvas.addMarker(targetRefId, completed ? 'highlight' : 'highlight-todo');
  }
  if (targetRefType === 'bpmn:ParallelGateway') {
    canvas.addMarker(id, completed ? 'highlight' : 'highlight-todo');
    canvas.addMarker(targetRefId, completed ? 'highlight' : 'highlight-todo');
  }
  if (targetRefType === 'bpmn:InclusiveGateway') {
    canvas.addMarker(id, completed ? 'highlight' : 'highlight-todo');
    canvas.addMarker(targetRefId, completed ? 'highlight' : 'highlight-todo');
  }
};
defineExpose({
  init,
  initXml
});
</script>
<style lang="scss" scoped>
.canvas {
  width: 100%;
  height: 100%;
}
.header-div {
  display: flex;
  padding: 10px 0;
  justify-content: space-between;
  .tips-label {
    display: flex;
    div {
      margin-right: 10px;
      padding: 5px;
      font-size: 12px;
    }
    .un-complete {
      border: 1px solid #000;
    }
    .in-progress {
      background-color: rgb(255, 237, 204);
      border: 1px dashed orange;
    }
    .complete {
      background-color: rgb(204, 230, 204);
      border: 1px solid green;
    }
  }
}
.view-mode {
  .el-header,
  .el-aside,
  .djs-palette,
  .bjs-powered-by {
    display: none;
  }
  .el-loading-mask {
    background-color: initial;
  }
  .el-loading-spinner {
    display: none;
  }
}
.bpmn-el-container {
  height: calc(100vh - 350px);
}
.flow-containers {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  .canvas {
    width: 100%;
    height: 100%;
  }
  .load {
    margin-right: 10px;
  }
  :deep(.el-form-item__label) {
    font-size: 13px;
  }
  :deep(.djs-palette) {
    left: 0 !important;
    top: 0;
    border-top: none;
  }
  :deep(.djs-container svg) {
    min-height: 650px;
  }
  :deep(.startEvent.djs-shape .djs-visual > :nth-child(1)) {
    fill: #77df6d !important;
  }
  :deep(.endEvent.djs-shape .djs-visual > :nth-child(1)) {
    fill: #ee7b77 !important;
  }
  :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;
  }
  // è¾¹æ¡†æ»šåŠ¨åŠ¨ç”»
  @keyframes path-animation {
    from {
      stroke-dashoffset: 100%;
    }
    to {
      stroke-dashoffset: 0%;
    }
  }
  :deep(.highlight-todo.djs-connection > .djs-visual > path) {
    animation: path-animation 60s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    stroke-dasharray: 4px !important;
    stroke: orange !important;
    fill-opacity: 0.2 !important;
    marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
  }
  :deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
    animation: path-animation 60s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    stroke-dasharray: 4px !important;
    stroke: orange !important;
    fill: orange !important;
    fill-opacity: 0.2 !important;
  }
}
:deep(.verlays) {
  width: 250px;
  background: rgb(102, 102, 102);
  border-radius: 4px;
  border: 1px solid #ebeef5;
  color: #fff;
  padding: 15px 10px;
  p {
    line-height: 28px;
    margin: 0;
    padding: 0;
  }
  cursor: pointer;
}
</style>
src/components/Breadcrumb/index.vue
@@ -2,8 +2,7 @@
  <el-breadcrumb class="app-breadcrumb" separator="/">
    <transition-group name="breadcrumb">
      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{
          item.meta?.title }}</span>
        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta?.title }}</span>
        <a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
      </el-breadcrumb-item>
    </transition-group>
@@ -11,42 +10,42 @@
</template>
<script setup lang="ts">
import { RouteLocationMatched } from 'vue-router'
import { RouteLocationMatched } from 'vue-router';
const route = useRoute();
const router = useRouter();
const levelList = ref<RouteLocationMatched[]>([])
const levelList = ref<RouteLocationMatched[]>([]);
const getBreadcrumb = () => {
  // only show routes with meta.title
  let matched = route.matched.filter(item => item.meta && item.meta.title);
  const first = matched[0]
  let matched = route.matched.filter((item) => item.meta && item.meta.title);
  const first = matched[0];
  // åˆ¤æ–­æ˜¯å¦ä¸ºé¦–页
  if (!isDashboard(first)) {
    matched = ([{ path: '/index', meta: { title: '首页' } }] as any).concat(matched)
    matched = ([{ path: '/index', meta: { title: '首页' } }] as any).concat(matched);
  }
  levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
  levelList.value = matched.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false);
};
const isDashboard = (route: RouteLocationMatched) => {
  const name = route && route.name as string
  const name = route && (route.name as string);
  if (!name) {
    return false
    return false;
  }
  return name.trim() === 'Index'
}
const handleLink = (item: RouteLocationMatched) => {
  const { redirect, path } = item
  redirect ? router.push(redirect as string) : router.push(path)
}
  return name.trim() === 'Index';
};
const handleLink = (item) => {
  const { redirect, path } = item;
  redirect ? router.push(redirect) : router.push(path);
};
watchEffect(() => {
  // if you go to the redirect page, do not update the breadcrumbs
  if (route.path.startsWith('/redirect/')) return
  getBreadcrumb()
})
  if (route.path.startsWith('/redirect/')) return;
  getBreadcrumb();
});
onMounted(() => {
  getBreadcrumb();
})
});
</script>
<style lang="scss" scoped>
src/components/BuildCode/index.vue
@@ -1,53 +1,50 @@
<!-- ä»£ç æž„建 -->
<script setup lang="ts">
const props = defineProps({
  showBtn: {
    type: Boolean,
    default: false
  },
  formJson: {
    type: Object,
    default: undefined
  }
})
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const buildRef = ref();
const emits = defineEmits(['reJson', 'saveDesign']);
//获取表单json
const getJson = () => {
  const formJson = JSON.stringify(buildRef.value.getFormJson())
  const fieldJson = JSON.stringify(buildRef.value.getFieldWidgets())
  let data = {
    formJson, fieldJson
  }
  emits("saveDesign", data)
}
onMounted(() => {
  if (props.formJson) {
    buildRef.value.setFormJson(props.formJson)
  }
})
</script>
<template>
  <!-- ä»£ç æž„建 -->
  <div>
    <v-form-designer
      class="build"
      ref="buildRef"
      class="build"
      :designer-config="{ importJsonButton: true, exportJsonButton: true, exportCodeButton: true, generateSFCButton: true, formTemplates: true }"
    >
      <template #customToolButtons v-if="showBtn">
      <template v-if="showBtn" #customToolButtons>
        <el-button link type="primary" icon="Select" @click="getJson">保存</el-button>
      </template>
    </v-form-designer>
  </div>
</template>
<script setup lang="ts">
interface Props {
  showBtn: boolean;
  formJson: any;
}
const props = withDefaults(defineProps<Props>(), {
  showBtn: true,
  formJson: ''
});
const buildRef = ref();
const emits = defineEmits(['reJson', 'saveDesign']);
//获取表单json
const getJson = () => {
  const formJson = JSON.stringify(buildRef.value.getFormJson());
  const fieldJson = JSON.stringify(buildRef.value.getFieldWidgets());
  let data = {
    formJson,
    fieldJson
  };
  emits('saveDesign', data);
};
onMounted(() => {
  if (props.formJson) {
    buildRef.value.setFormJson(props.formJson);
  }
});
</script>
<style lang="scss">
.build {
  margin: 0 !important;
src/components/BuildCode/render.vue
@@ -1,62 +1,57 @@
<template>
  <div class="">
    <v-form-render ref="vFormRef" :form-json="formJson" :form-data="formData" />
  </div>
</template>
<!-- åŠ¨æ€è¡¨å•æ¸²æŸ“ -->
<script setup name="Render">
<script setup name="Render" lang="ts">
interface Props {
  formJson: string | object;
  formData: string | object;
  isView: boolean;
}
const props = defineProps({
  formJson: {
    type: [String, Object],
    default: ""
  },
  formData: {
    type: [String, Object],
    default: ""
  },
  isView: {
    type: Boolean,
    default: false
  }
})
const props = withDefaults(defineProps<Props>(), {
  formJson: '',
  formData: '',
  isView: false
});
const vFormRef = ref(null)
const vFormRef = ref();
// èŽ·å–è¡¨å•æ•°æ®-异步
const getFormData = () => {
  return vFormRef.value.getFormData()
}
  return vFormRef.value.getFormData();
};
/**
 * è®¾ç½®è¡¨å•内容
 * @param {表单配置} formConf
 * formConfig:{ formTemplate:表单模板,formData:表单数据,hiddenField:需要隐藏的字段字符串集合,disabledField:需要禁用的自读字符串集合}
 */
const initForm = (formConf) => {
  const { formTemplate, formData, hiddenField, disabledField } = toRaw(formConf)
const initForm = (formConf: any) => {
  const { formTemplate, formData, hiddenField, disabledField } = toRaw(formConf);
  if (formTemplate) {
    vFormRef.value.setFormJson(formTemplate)
    vFormRef.value.setFormJson(formTemplate);
    if (formData) {
      vFormRef.value.setFormData(formData)
      vFormRef.value.setFormData(formData);
    }
    if (disabledField && disabledField.length > 0) {
      setTimeout(() => {
        vFormRef.value.disableWidgets(disabledField)
      }, 200)
        vFormRef.value.disableWidgets(disabledField);
      }, 200);
    }
    if (hiddenField && hiddenField.length > 0) {
      setTimeout(() => {
        vFormRef.value.hideWidgets(hiddenField)
      }, 200)
        vFormRef.value.hideWidgets(hiddenField);
      }, 200);
    }
    if (props.isView) {
      console.log(props.isView)
      setTimeout(() => {
        vFormRef.value.disableForm()
      }, 100)
        vFormRef.value.disableForm();
      }, 100);
    }
  }
}
defineExpose({ getFormData, initForm })
};
defineExpose({ getFormData, initForm });
</script>
<template>
  <div class="">
    <v-form-render ref="vFormRef" :form-json="formJson" :form-data="formData" />
  </div>
</template>
src/components/DictTag/index.vue
@@ -2,19 +2,31 @@
  <div>
    <template v-for="(item, index) in options">
      <template v-if="values.includes(item.value)">
        <span v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
              :key="item.value" :index="index" :class="item.elTagClass">
          {{ item.label + " " }}
        <span
          v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
          :key="item.value"
          :index="index"
          :class="item.elTagClass"
        >
          {{ item.label + ' ' }}
        </span>
        <el-tag
          v-else
          :disable-transitions="true"
          :key="item.value + ''"
          :disable-transitions="true"
          :index="index"
          :type="(item.elTagType === 'primary' || item.elTagType === 'default')? '' : item.elTagType"
          :type="
            item.elTagType === 'primary' ||
            item.elTagType === 'success' ||
            item.elTagType === 'info' ||
            item.elTagType === 'warning' ||
            item.elTagType === 'danger'
              ? item.elTagType
              : 'primary'
          "
          :class="item.elTagClass"
        >
          {{ item.label + " " }}
          {{ item.label + ' ' }}
        </el-tag>
      </template>
    </template>
@@ -25,57 +37,54 @@
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
  // æ•°æ®
  options: {
    type: Array as PropType<DictDataOption[]>,
    default: null,
  },
  // å½“前的值
  value: [Number, String, Array] as PropType<number | string | Array<number | string>>,
  // å½“未找到匹配的数据时,显示value
  showValue: propTypes.bool.def(true),
  separator: propTypes.string.def(","),
interface Props {
  options: Array<DictDataOption>;
  value: number | string | Array<number | string>;
  showValue?: boolean;
  separator?: string;
}
const props = withDefaults(defineProps<Props>(), {
  showValue: true,
  separator: ','
});
const values = computed(() => {
  if (props.value === '' || props.value === null || typeof props.value === "undefined") return []
  return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator);
  if (props.value === '' || props.value === null || typeof props.value === 'undefined') return [];
  return Array.isArray(props.value) ? props.value.map((item) => '' + item) : String(props.value).split(props.separator);
});
const unmatch = computed(() => {
  if (props.options?.length == 0 || props.value === '' || props.value === null || typeof props.value === "undefined") return false
  if (props.options?.length == 0 || props.value === '' || props.value === null || typeof props.value === 'undefined') return false;
  // ä¼ å…¥å€¼ä¸ºéžæ•°ç»„
  values.value.forEach(item => {
    if (!props.options.some(v => v.value === item)) {
      return true // å¦‚果有未匹配项,将标志设置为true
  let unmatch = false; // æ·»åŠ ä¸€ä¸ªæ ‡å¿—æ¥åˆ¤æ–­æ˜¯å¦æœ‰æœªåŒ¹é…é¡¹
  values.value.forEach((item) => {
    if (!props.options.some((v) => v.value === item)) {
      unmatch = true; // å¦‚果有未匹配项,将标志设置为true
    }
  })
  return false // è¿”回标志的值
  });
  return unmatch; // è¿”回标志的值
});
const unmatchArray = computed(() => {
// è®°å½•未匹配的项
  // è®°å½•未匹配的项
  const itemUnmatchArray: Array<string | number> = [];
  if (props.value !== '' && props.value !== null && typeof props.value !== "undefined") {
    values.value.forEach(item => {
      if (!props.options.some(v => v.value === item)) {
  if (props.value !== '' && props.value !== null && typeof props.value !== 'undefined') {
    values.value.forEach((item) => {
      if (!props.options.some((v) => v.value === item)) {
        itemUnmatchArray.push(item);
      }
    })
    });
  }
  // æ²¡æœ‰value不显示
  return handleArray(itemUnmatchArray);
});
const handleArray = (array: Array<string | number>) => {
  if (array.length === 0) return "";
  if (array.length === 0) return '';
  return array.reduce((pre, cur) => {
    return pre + " " + cur;
    return pre + ' ' + cur;
  });
}
};
</script>
<style scoped>
src/components/Editor/index.vue
@@ -1,6 +1,7 @@
<template>
  <div>
    <el-upload
      v-if="type === 'url'"
      :action="upload.url"
      :before-upload="handleBeforeUpload"
      :on-success="handleUploadSuccess"
@@ -9,28 +10,30 @@
      name="file"
      :show-file-list="false"
      :headers="upload.headers"
      ref="uploadRef"
      v-if="type === 'url'"
    >
      <i ref="uploadRef"></i>
    </el-upload>
    <div class="editor">
      <quill-editor
        ref="quillEditorRef"
        v-model:content="content"
        contentType="html"
        @textChange="(e: any) => $emit('update:modelValue', content)"
        :options="options"
        :style="styles"
      />
    </div>
  </div>
  <div class="editor">
    <quill-editor
      ref="quillEditorRef"
      v-model:content="content"
      content-type="html"
      :options="options"
      :style="styles"
      @text-change="(e: any) => $emit('update:modelValue', content)"
    />
  </div>
</template>
<script setup lang="ts">
import { QuillEditor, Quill } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { QuillEditor, Quill } from '@vueup/vue-quill';
import { propTypes } from '@/utils/propTypes';
import { globalHeaders } from "@/utils/request";
import { globalHeaders } from '@/utils/request';
defineEmits(['update:modelValue']);
const props = defineProps({
  /* ç¼–辑器的内容 */
@@ -52,42 +55,43 @@
const upload = reactive<UploadOption>({
  headers: globalHeaders(),
  url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
})
});
const quillEditorRef = ref();
const uploadRef = ref<HTMLDivElement>();
const options = ref({
  theme: "snow",
const options = ref<any>({
  theme: 'snow',
  bounds: document.body,
  debug: "warn",
  debug: 'warn',
  modules: {
    // å·¥å…·æ é…ç½®
    toolbar: {
      container: [
        ["bold", "italic", "underline", "strike"],       // åŠ ç²— æ–œä½“ ä¸‹åˆ’线 åˆ é™¤çº¿
        ["blockquote", "code-block"],                    // å¼•用  ä»£ç å—
        [{ list: "ordered" }, { list: "bullet" }],       // æœ‰åºã€æ— åºåˆ—表
        [{ indent: "-1" }, { indent: "+1" }],            // ç¼©è¿›
        [{ size: ["small", false, "large", "huge"] }],   // å­—体大小
        [{ header: [1, 2, 3, 4, 5, 6, false] }],         // æ ‡é¢˜
        [{ color: [] }, { background: [] }],             // å­—体颜色、字体背景颜色
        [{ align: [] }],                                 // å¯¹é½æ–¹å¼
        ["clean"],                                       // æ¸…除文本格式
        ["link", "image", "video"]                       // é“¾æŽ¥ã€å›¾ç‰‡ã€è§†é¢‘
        ['bold', 'italic', 'underline', 'strike'], // åŠ ç²— æ–œä½“ ä¸‹åˆ’线 åˆ é™¤çº¿
        ['blockquote', 'code-block'], // å¼•用  ä»£ç å—
        [{ list: 'ordered' }, { list: 'bullet' }], // æœ‰åºã€æ— åºåˆ—表
        [{ indent: '-1' }, { indent: '+1' }], // ç¼©è¿›
        [{ size: ['small', false, 'large', 'huge'] }], // å­—体大小
        [{ header: [1, 2, 3, 4, 5, 6, false] }], // æ ‡é¢˜
        [{ color: [] }, { background: [] }], // å­—体颜色、字体背景颜色
        [{ align: [] }], // å¯¹é½æ–¹å¼
        ['clean'], // æ¸…除文本格式
        ['link', 'image', 'video'] // é“¾æŽ¥ã€å›¾ç‰‡ã€è§†é¢‘
      ],
      handlers: {
        image: function (value: any) {
        image: (value: boolean) => {
          if (value) {
            // è°ƒç”¨element图片上传
            (document.querySelector(".editor-img-uploader>.el-upload") as HTMLDivElement)?.click();
            uploadRef.value.click();
          } else {
            Quill.format("image", true);
            Quill.format('image', true);
          }
        },
      },
        }
      }
    }
  },
  placeholder: "请输入内容",
  readOnly: props.readOnly,
  placeholder: '请输入内容',
  readOnly: props.readOnly
});
const styles = computed(() => {
@@ -99,14 +103,18 @@
    style.height = `${props.height}px`;
  }
  return style;
})
});
const content = ref("");
watch(() => props.modelValue, (v) => {
  if (v !== content.value) {
    content.value = v === undefined ? "<p></p>" : v;
  }
}, { immediate: true });
const content = ref('');
watch(
  () => props.modelValue,
  (v: string) => {
    if (v !== content.value) {
      content.value = v === undefined ? '<p></p>' : v;
    }
  },
  { immediate: true }
);
// å›¾ç‰‡ä¸Šä¼ æˆåŠŸè¿”å›žå›¾ç‰‡åœ°å€
const handleUploadSuccess = (res: any) => {
@@ -117,19 +125,19 @@
    // èŽ·å–å…‰æ ‡ä½ç½®
    let length = quill.selection.savedRange.index;
    // æ’入图片,res为服务器返回的图片链接地址
    quill.insertEmbed(length, "image", res.data.url);
    quill.insertEmbed(length, 'image', res.data.url);
    // è°ƒæ•´å…‰æ ‡åˆ°æœ€åŽ
    quill.setSelection(length + 1);
    proxy?.$modal.closeLoading();
  } else {
    proxy?.$modal.loading(res.msg);
    proxy?.$modal.msgError('图片插入失败');
    proxy?.$modal.closeLoading();
  }
}
};
// å›¾ç‰‡ä¸Šä¼ å‰æ‹¦æˆª
const handleBeforeUpload = (file: any) => {
  const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
  const type = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg'];
  const isJPG = type.includes(file.type);
  //检验文件格式
  if (!isJPG) {
@@ -146,13 +154,12 @@
  }
  proxy?.$modal.loading('正在上传文件,请稍候...');
  return true;
}
};
// å›¾ç‰‡å¤±è´¥æ‹¦æˆª
const handleUploadError = (err: any) => {
  console.error(err);
  proxy?.$modal.msgError('上传文件失败');
}
};
</script>
<style>
@@ -167,71 +174,71 @@
.quill-img {
  display: none;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {
  content: "请输入链接地址:";
.ql-snow .ql-tooltip[data-mode='link']::before {
  content: '请输入链接地址:';
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  border-right: 0;
  content: "保存";
  content: '保存';
  padding-right: 0;
}
.ql-snow .ql-tooltip[data-mode="video"]::before {
  content: "请输入视频地址:";
.ql-snow .ql-tooltip[data-mode='video']::before {
  content: '请输入视频地址:';
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
  content: "14px";
  content: '14px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
  content: "10px";
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
  content: '10px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
  content: "18px";
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
  content: '18px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
  content: "32px";
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
  content: '32px';
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
  content: "文本";
  content: '文本';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
  content: "标题1";
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
  content: '标题1';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  content: "标题2";
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
  content: '标题2';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  content: "标题3";
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
  content: '标题3';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
  content: "标题4";
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
  content: '标题4';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
  content: "标题5";
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
  content: '标题5';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
  content: "标题6";
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
  content: '标题6';
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
  content: "标准字体";
  content: '标准字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
  content: "衬线字体";
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
  content: '衬线字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
  content: "等宽字体";
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
  content: '等宽字体';
}
</style>
src/components/FileUpload/index.vue
@@ -1,6 +1,7 @@
<template>
  <div class="upload-file">
    <el-upload
      ref="fileUploadRef"
      multiple
      :action="uploadFileUrl"
      :before-upload="handleBeforeUpload"
@@ -12,30 +13,29 @@
      :show-file-list="false"
      :headers="headers"
      class="upload-file-uploader"
      ref="fileUploadRef"
    >
      <!-- ä¸Šä¼ æŒ‰é’® -->
      <el-button type="primary">选取文件</el-button>
    </el-upload>
    <!-- ä¸Šä¼ æç¤º -->
    <div class="el-upload__tip" v-if="showTip">
    <div v-if="showTip" class="el-upload__tip">
      è¯·ä¸Šä¼ 
      <template v-if="fileSize">
        å¤§å°ä¸è¶…过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
      </template>
      <template v-if="fileType">
        æ ¼å¼ä¸º <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
        æ ¼å¼ä¸º <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
      </template>
      çš„æ–‡ä»¶
    </div>
    <!-- æ–‡ä»¶åˆ—表 -->
    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
      <li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
        <el-link :href="`${file.url}`" :underline="false" target="_blank">
          <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
        </el-link>
        <div class="ele-upload-list__item-content-action">
          <el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
          <el-button type="danger" link @click="handleDelete(index)">删除</el-button>
        </div>
      </li>
    </transition-group>
@@ -43,20 +43,23 @@
</template>
<script setup lang="ts">
import { listByIds, delOss } from "@/api/system/oss";
import { propTypes } from '@/utils/propTypes';
import { globalHeaders } from "@/utils/request";
import { delOss, listByIds } from '@/api/system/oss';
import { globalHeaders } from '@/utils/request';
const props = defineProps({
    modelValue: [String, Object, Array],
    // æ•°é‡é™åˆ¶
    limit: propTypes.number.def(5),
    // å¤§å°é™åˆ¶(MB)
    fileSize: propTypes.number.def(5),
    // æ–‡ä»¶ç±»åž‹, ä¾‹å¦‚['png', 'jpg', 'jpeg']
    fileType: propTypes.array.def(["doc", "xls", "ppt", "txt", "pdf"]),
    // æ˜¯å¦æ˜¾ç¤ºæç¤º
    isShowTip: propTypes.bool.def(true),
  modelValue: {
    type: [String, Object, Array],
    default: () => []
  },
  // æ•°é‡é™åˆ¶
  limit: propTypes.number.def(5),
  // å¤§å°é™åˆ¶(MB)
  fileSize: propTypes.number.def(5),
  // æ–‡ä»¶ç±»åž‹, ä¾‹å¦‚['png', 'jpg', 'jpeg']
  fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']),
  // æ˜¯å¦æ˜¾ç¤ºæç¤º
  isShowTip: propTypes.bool.def(true)
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -65,153 +68,162 @@
const uploadList = ref<any[]>([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(baseUrl + "/resource/oss/upload"); // ä¸Šä¼ æ–‡ä»¶æœåŠ¡å™¨åœ°å€
const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // ä¸Šä¼ æ–‡ä»¶æœåŠ¡å™¨åœ°å€
const headers = ref(globalHeaders());
const fileList = ref<any[]>([]);
const showTip = computed(
    () => props.isShowTip && (props.fileType || props.fileSize)
);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
const fileUploadRef = ref<ElUploadInstance>();
watch(() => props.modelValue, async val => {
watch(
  () => props.modelValue,
  async (val) => {
    if (val) {
        let temp = 1;
        // é¦–先将值转为数组
        let list = [];
        if (Array.isArray(val)) {
            list = val;
        } else {
            const res = await listByIds(val as string)
            list = res.data.map((oss) => {
                const data = { name: oss.originalName, url: oss.url, ossId: oss.ossId };
                return data;
            });
        }
        // ç„¶åŽå°†æ•°ç»„转为对象数组
        fileList.value = list.map(item => {
            item = { name: item.name, url: item.url, ossId: item.ossId };
            item.uid = item.uid || new Date().getTime() + temp++;
            return item;
      let temp = 1;
      // é¦–先将值转为数组
      let list: any[] = [];
      if (Array.isArray(val)) {
        list = val;
      } else {
        const res = await listByIds(val);
        list = res.data.map((oss) => {
          return {
            name: oss.originalName,
            url: oss.url,
            ossId: oss.ossId
          };
        });
      }
      // ç„¶åŽå°†æ•°ç»„转为对象数组
      fileList.value = list.map((item) => {
        item = { name: item.name, url: item.url, ossId: item.ossId };
        item.uid = item.uid || new Date().getTime() + temp++;
        return item;
      });
    } else {
        fileList.value = [];
        return [];
      fileList.value = [];
      return [];
    }
}, { deep: true, immediate: true });
  },
  { deep: true, immediate: true }
);
// ä¸Šä¼ å‰æ ¡æ£€æ ¼å¼å’Œå¤§å°
const handleBeforeUpload = (file: any) => {
    // æ ¡æ£€æ–‡ä»¶ç±»åž‹
    if (props.fileType.length) {
        const fileName = file.name.split('.');
        const fileExt = fileName[fileName.length - 1];
        const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
        if (!isTypeOk) {
            proxy?.$modal.msgError(`文件格式不正确, è¯·ä¸Šä¼ ${props.fileType.join("/")}格式文件!`);
            return false;
        }
  // æ ¡æ£€æ–‡ä»¶ç±»åž‹
  if (props.fileType.length) {
    const fileName = file.name.split('.');
    const fileExt = fileName[fileName.length - 1];
    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
    if (!isTypeOk) {
      proxy?.$modal.msgError(`文件格式不正确, è¯·ä¸Šä¼ ${props.fileType.join('/')}格式文件!`);
      return false;
    }
    // æ ¡æ£€æ–‡ä»¶å¤§å°
    if (props.fileSize) {
        const isLt = file.size / 1024 / 1024 < props.fileSize;
        if (!isLt) {
            proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
            return false;
        }
  }
  // æ ¡æ£€æ–‡ä»¶å¤§å°
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
      return false;
    }
    proxy?.$modal.loading("正在上传文件,请稍候...");
    number.value++;
    return true;
}
  }
  proxy?.$modal.loading('正在上传文件,请稍候...');
  number.value++;
  return true;
};
// æ–‡ä»¶ä¸ªæ•°è¶…出
const handleExceed = () => {
    proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} ä¸ª!`);
}
  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} ä¸ª!`);
};
// ä¸Šä¼ å¤±è´¥
const handleUploadError = () => {
    proxy?.$modal.msgError("上传文件失败");
}
  proxy?.$modal.msgError('上传文件失败');
};
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
const handleUploadSuccess = (res: any, file: UploadFile) => {
    if (res.code === 200) {
        uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
        uploadedSuccessfully();
    } else {
        number.value--;
        proxy?.$modal.closeLoading();
        proxy?.$modal.msgError(res.msg);
        fileUploadRef.value?.handleRemove(file);
        uploadedSuccessfully();
    }
}
  if (res.code === 200) {
    uploadList.value.push({
      name: res.data.fileName,
      url: res.data.url,
      ossId: res.data.ossId
    });
    uploadedSuccessfully();
  } else {
    number.value--;
    proxy?.$modal.closeLoading();
    proxy?.$modal.msgError(res.msg);
    fileUploadRef.value?.handleRemove(file);
    uploadedSuccessfully();
  }
};
// åˆ é™¤æ–‡ä»¶
const handleDelete = (index: number) => {
    let ossId = fileList.value[index].ossId;
    delOss(ossId);
    fileList.value.splice(index, 1);
    emit("update:modelValue", listToString(fileList.value));
}
  let ossId = fileList.value[index].ossId;
  delOss(ossId);
  fileList.value.splice(index, 1);
  emit('update:modelValue', listToString(fileList.value));
};
// ä¸Šä¼ ç»“束处理
const uploadedSuccessfully = () => {
    if (number.value > 0 && uploadList.value.length === number.value) {
        fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
        uploadList.value = [];
        number.value = 0;
        emit("update:modelValue", listToString(fileList.value));
        proxy?.$modal.closeLoading();
    }
}
  if (number.value > 0 && uploadList.value.length === number.value) {
    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
    uploadList.value = [];
    number.value = 0;
    emit('update:modelValue', listToString(fileList.value));
    proxy?.$modal.closeLoading();
  }
};
// èŽ·å–æ–‡ä»¶åç§°
const getFileName = (name: string) => {
    // å¦‚果是url那么取最后的名字 å¦‚果不是直接返回
    if (name.lastIndexOf("/") > -1) {
        return name.slice(name.lastIndexOf("/") + 1);
    } else {
        return name;
    }
}
  // å¦‚果是url那么取最后的名字 å¦‚果不是直接返回
  if (name.lastIndexOf('/') > -1) {
    return name.slice(name.lastIndexOf('/') + 1);
  } else {
    return name;
  }
};
// å¯¹è±¡è½¬æˆæŒ‡å®šå­—符串分隔
const listToString = (list: any[], separator?: string) => {
    let strs = "";
    separator = separator || ",";
    list.forEach(item => {
        if (item.ossId) {
            strs += item.ossId + separator;
        }
    })
    return strs != "" ? strs.substring(0, strs.length - 1) : "";
}
  let strs = '';
  separator = separator || ',';
  list.forEach((item) => {
    if (item.ossId) {
      strs += item.ossId + separator;
    }
  });
  return strs != '' ? strs.substring(0, strs.length - 1) : '';
};
</script>
<style scoped lang="scss">
.upload-file-uploader {
    margin-bottom: 5px;
  margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
    border: 1px solid #e4e7ed;
    line-height: 2;
    margin-bottom: 10px;
    position: relative;
  border: 1px solid #e4e7ed;
  line-height: 2;
  margin-bottom: 10px;
  position: relative;
}
.upload-file-list .ele-upload-list__item-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    color: inherit;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: inherit;
}
.ele-upload-list__item-content-action .el-link {
    margin-right: 10px;
  margin-right: 10px;
}
</style>
src/components/Hamburger/index.vue
@@ -1,5 +1,5 @@
<template>
  <div style="padding: 0 15px;" @click="toggleClick">
  <div style="padding: 0 15px" @click="toggleClick">
    <svg :class="{ 'is-active': isActive }" class="hamburger" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
      <path
        d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
@@ -13,12 +13,12 @@
defineProps({
  isActive: propTypes.bool.def(false)
})
});
const emit = defineEmits(['toggleClick'])
const emit = defineEmits(['toggleClick']);
const toggleClick = () => {
  emit('toggleClick');
}
};
</script>
<style scoped>
src/components/HeaderSearch/index.vue
@@ -1,6 +1,6 @@
<template>
  <div :class="{ 'show': show }" class="header-search">
    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click"/>
  <div :class="{ show: show }" class="header-search">
    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
    <el-select
      ref="headerSearchSelectRef"
      v-model="search"
@@ -12,23 +12,22 @@
      class="header-search-select"
      @change="change"
    >
      <el-option v-for="option in options" :key="option.item.path" :value="option.item"
                 :label="option.item.title.join(' > ')"/>
      <el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
    </el-select>
  </div>
</template>
<script setup lang="ts" name="HeaderSearch">
import Fuse from 'fuse.js';
import {getNormalPath} from '@/utils/ruoyi';
import {isHttp} from '@/utils/validate';
import { getNormalPath } from '@/utils/ruoyi';
import { isHttp } from '@/utils/validate';
import usePermissionStore from '@/store/modules/permission';
import {RouteOption} from 'vue-router';
import { RouteRecordRaw } from 'vue-router';
type Router = Array<{
  path: string;
  title: string[];
}>
}>;
const search = ref('');
const options = ref<any>([]);
@@ -37,39 +36,39 @@
const fuse = ref();
const headerSearchSelectRef = ref<ElSelectInstance>();
const router = useRouter();
const routes = computed(() => usePermissionStore().routes);
const routes = computed(() => usePermissionStore().getRoutes());
const click = () => {
  show.value = !show.value
  show.value = !show.value;
  if (show.value) {
    headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
    headerSearchSelectRef.value && headerSearchSelectRef.value.focus();
  }
};
const close = () => {
  headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
  options.value = []
  show.value = false
}
  headerSearchSelectRef.value && headerSearchSelectRef.value.blur();
  options.value = [];
  show.value = false;
};
const change = (val: any) => {
  const path = val.path;
  const query = val.query;
  if (isHttp(path)) {
    // http(s):// è·¯å¾„新窗口打开
    const pindex = path.indexOf("http");
    window.open(path.substr(pindex, path.length), "_blank");
    const pindex = path.indexOf('http');
    window.open(path.substr(pindex, path.length), '_blank');
  } else {
    if (query) {
      router.push({ path: path, query: JSON.parse(query) });
    } else {
      router.push(path)
      router.push(path);
    }
  }
  search.value = ''
  options.value = []
  search.value = '';
  options.value = [];
  nextTick(() => {
    show.value = false
  })
}
    show.value = false;
  });
};
const initFuse = (list: Router) => {
  fuse.value = new Fuse(list, {
    shouldSort: true,
@@ -77,20 +76,23 @@
    location: 0,
    distance: 100,
    minMatchCharLength: 1,
    keys: [{
      name: 'title',
      weight: 0.7
    }, {
      name: 'path',
      weight: 0.3
    }]
  })
}
    keys: [
      {
        name: 'title',
        weight: 0.7
      },
      {
        name: 'path',
        weight: 0.3
      }
    ]
  });
};
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: string[] = []) => {
  let res: Router = []
  routes.forEach(r => {
const generateRoutes = (routes: RouteRecordRaw[], basePath = '', prefixTitle: string[] = []) => {
  let res: Router = [];
  routes.forEach((r) => {
    // skip hidden router
    if (!r.hidden) {
      const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
@@ -98,7 +100,7 @@
        path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
        title: [...prefixTitle],
        query: ''
      }
      };
      if (r.meta && r.meta.title) {
        data.title = [...data.title, r.meta.title];
        if (r.redirect !== 'noRedirect') {
@@ -109,7 +111,7 @@
      }
      if (r.query) {
        data.query = r.query
        data.query = r.query;
      }
      // recursive child routes
@@ -120,20 +122,20 @@
        }
      }
    }
  })
  });
  return res;
}
};
const querySearch = (query: string) => {
  if (query !== '') {
    options.value = fuse.value.search(query)
    options.value = fuse.value.search(query);
  } else {
    options.value = []
    options.value = [];
  }
}
};
onMounted(() => {
  searchPool.value = generateRoutes(routes.value);
})
});
// watchEffect(() => {
//     searchPool.value = generateRoutes(routes.value)
@@ -141,15 +143,15 @@
watch(show, (value) => {
  if (value) {
    document.body.addEventListener('click', close)
    document.body.addEventListener('click', close);
  } else {
    document.body.removeEventListener('click', close)
    document.body.removeEventListener('click', close);
  }
})
});
watch(searchPool, (list) => {
  initFuse(list)
})
watch(searchPool, (list: Router) => {
  initFuse(list);
});
</script>
<style lang="scss" scoped>
src/components/IconSelect/index.vue
@@ -1,6 +1,6 @@
<template>
  <div class="relative" :style="{ width: width }">
    <el-input v-model="modelValue" readonly @click="visible = !visible" placeholder="点击选择图标">
  <div class="relative" :style="{ 'width': width }">
    <el-input v-model="modelValue" readonly placeholder="点击选择图标" @click="visible = !visible">
      <template #prepend>
        <svg-icon :icon-class="modelValue" />
      </template>
@@ -8,18 +8,18 @@
    <el-popover shadow="none" :visible="visible" placement="bottom-end" trigger="click" :width="450">
      <template #reference>
        <div @click="visible = !visible" class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]">
        <div class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]" @click="visible = !visible">
          <i-ep-caret-top v-show="visible"></i-ep-caret-top>
          <i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
        </div>
      </template>
      <el-input class="p-2" v-model="filterValue" placeholder="搜索图标" clearable @input="filterIcons" />
      <el-input v-model="filterValue" class="p-2" placeholder="搜索图标" clearable @input="filterIcons" />
      <el-scrollbar height="w-[200px]">
        <ul class="icon-list">
          <el-tooltip v-for="(iconName, index) in iconNames" :key="index" :content="iconName" placement="bottom" effect="light">
            <li :class="['icon-item', {active: modelValue == iconName}]" @click="selectedIcon(iconName)">
            <li :class="['icon-item', { active: modelValue == iconName }]" @click="selectedIcon(iconName)">
              <svg-icon color="var(--el-text-color-regular)" :icon-class="iconName" />
            </li>
          </el-tooltip>
@@ -50,13 +50,11 @@
 */
const filterIcons = () => {
  if (filterValue.value) {
    iconNames.value = icons.filter(iconName =>
      iconName.includes(filterValue.value)
    );
    iconNames.value = icons.filter((iconName) => iconName.includes(filterValue.value));
  } else {
    iconNames.value = icons;
  }
}
};
/**
 * é€‰æ‹©å›¾æ ‡
 * @param iconName é€‰æ‹©çš„图标名称
@@ -64,12 +62,12 @@
const selectedIcon = (iconName: string) => {
  emit('update:modelValue', iconName);
  visible.value = false;
}
};
</script>
<style scoped lang="scss">
.el-scrollbar {
  max-height: calc(50vh - 100px)!important;
  max-height: calc(50vh - 100px) !important;
  overflow-y: auto;
}
.el-divider--horizontal {
@@ -99,8 +97,8 @@
    }
  }
  .active {
      border-color: var(--el-color-primary);
      color: var(--el-color-primary);
    }
    border-color: var(--el-color-primary);
    color: var(--el-color-primary);
  }
}
</style>
src/components/ImagePreview/index.vue
@@ -15,11 +15,11 @@
  src: propTypes.string.def(''),
  width: {
    type: [Number, String],
    default: ""
    default: ''
  },
  height: {
    type: [Number, String],
    default: ""
    default: ''
  }
});
@@ -27,29 +27,28 @@
  if (!props.src) {
    return;
  }
  let real_src = props.src.split(",")[0];
  let real_src = props.src.split(',')[0];
  return real_src;
});
const realSrcList = computed(() => {
  if (!props.src) {
    return;
    return [];
  }
  let real_src_list = props.src.split(",");
  let real_src_list = props.src.split(',');
  let srcList: string[] = [];
  real_src_list.forEach(item => {
  real_src_list.forEach((item: string) => {
    if(item.trim() === '') {
      return;
    }
    return srcList.push(item);
  });
  return srcList;
});
const realWidth = computed(() =>
  typeof props.width == "string" ? props.width : `${props.width}px`
);
const realWidth = computed(() => (typeof props.width == 'string' ? props.width : `${props.width}px`));
const realHeight = computed(() =>
  typeof props.height == "string" ? props.height : `${props.height}px`
);
const realHeight = computed(() => (typeof props.height == 'string' ? props.height : `${props.height}px`));
</script>
<style lang="scss" scoped>
src/components/ImageUpload/index.vue
@@ -1,6 +1,7 @@
<template>
  <div class="component-upload-image">
    <el-upload
      ref="imageUpload"
      multiple
      :action="uploadImgUrl"
      list-type="picture-card"
@@ -9,7 +10,6 @@
      :limit="limit"
      :on-error="handleUploadError"
      :on-exceed="handleExceed"
      ref="imageUpload"
      :before-remove="handleDelete"
      :show-file-list="true"
      :headers="headers"
@@ -22,13 +22,13 @@
      </el-icon>
    </el-upload>
    <!-- ä¸Šä¼ æç¤º -->
    <div class="el-upload__tip" v-if="showTip">
    <div v-if="showTip" class="el-upload__tip">
      è¯·ä¸Šä¼ 
      <template v-if="fileSize">
        å¤§å°ä¸è¶…过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
      </template>
      <template v-if="fileType">
        æ ¼å¼ä¸º <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
        æ ¼å¼ä¸º <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
      </template>
      çš„æ–‡ä»¶
    </div>
@@ -40,177 +40,195 @@
</template>
<script setup lang="ts">
import { listByIds, delOss } from "@/api/system/oss";
import { ComponentInternalInstance } from "vue";
import { OssVO } from "@/api/system/oss/types";
import { listByIds, delOss } from '@/api/system/oss';
import { OssVO } from '@/api/system/oss/types';
import { propTypes } from '@/utils/propTypes';
import {globalHeaders} from "@/utils/request";
import { globalHeaders } from '@/utils/request';
import { compressAccurately } from 'image-conversion';
const props = defineProps({
    modelValue: [String, Object, Array],
    // å›¾ç‰‡æ•°é‡é™åˆ¶
    limit: propTypes.number.def(5),
    // å¤§å°é™åˆ¶(MB)
    fileSize: propTypes.number.def(5),
    // æ–‡ä»¶ç±»åž‹, ä¾‹å¦‚['png', 'jpg', 'jpeg']
    fileType: propTypes.array.def(["png", "jpg", "jpeg"]),
    // æ˜¯å¦æ˜¾ç¤ºæç¤º
    isShowTip: {
        type: Boolean,
        default: true
    },
  modelValue: {
    type: [String, Object, Array],
    default: () => []
  },
  // å›¾ç‰‡æ•°é‡é™åˆ¶
  limit: propTypes.number.def(5),
  // å¤§å°é™åˆ¶(MB)
  fileSize: propTypes.number.def(5),
  // æ–‡ä»¶ç±»åž‹, ä¾‹å¦‚['png', 'jpg', 'jpeg']
  fileType: propTypes.array.def(['png', 'jpg', 'jpeg']),
  // æ˜¯å¦æ˜¾ç¤ºæç¤º
  isShowTip: {
    type: Boolean,
    default: true
  },
  // æ˜¯å¦æ”¯æŒåŽ‹ç¼©ï¼Œé»˜è®¤å¦
  compressSupport: {
    type: Boolean,
    default: false
  },
  // åŽ‹ç¼©ç›®æ ‡å¤§å°ï¼Œå•ä½KB。默认300KB以上文件才压缩,并压缩至300KB以内
  compressTargetSize: propTypes.number.def(300)
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue']);
const number = ref(0);
const uploadList = ref<any[]>([]);
const dialogImageUrl = ref("");
const dialogImageUrl = ref('');
const dialogVisible = ref(false);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(baseUrl + "/resource/oss/upload"); // ä¸Šä¼ çš„图片服务器地址
const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // ä¸Šä¼ çš„图片服务器地址
const headers = ref(globalHeaders());
const fileList = ref<any[]>([]);
const showTip = computed(
    () => props.isShowTip && (props.fileType || props.fileSize)
);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
const imageUploadRef = ref<ElUploadInstance>();
watch(() => props.modelValue, async val => {
watch(
  () => props.modelValue,
  async (val: string) => {
    if (val) {
        // é¦–先将值转为数组
        let list: OssVO[] = [];
        if (Array.isArray(val)) {
            list = val as OssVO[];
      // é¦–先将值转为数组
      let list: OssVO[] = [];
      if (Array.isArray(val)) {
        list = val as OssVO[];
      } else {
        const res = await listByIds(val);
        list = res.data;
      }
      // ç„¶åŽå°†æ•°ç»„转为对象数组
      fileList.value = list.map((item) => {
        // å­—符串回显处理 å¦‚果此处存的是url可直接回显 å¦‚果存的是id需要调用接口查出来
        let itemData;
        if (typeof item === 'string') {
          itemData = { name: item, url: item };
        } else {
            const res = await listByIds(val as string)
            list = res.data
          // æ­¤å¤„name使用ossId é˜²æ­¢åˆ é™¤å‡ºçŽ°é‡å
          itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
        }
        // ç„¶åŽå°†æ•°ç»„转为对象数组
        fileList.value = list.map(item => {
            // å­—符串回显处理 å¦‚果此处存的是url可直接回显 å¦‚果存的是id需要调用接口查出来
            let itemData;
            if (typeof item === "string") {
                itemData = { name: item, url: item };
            } else {
                // æ­¤å¤„name使用ossId é˜²æ­¢åˆ é™¤å‡ºçŽ°é‡å
                itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
            }
            return itemData;
        });
        return itemData;
      });
    } else {
        fileList.value = [];
        return [];
      fileList.value = [];
      return [];
    }
}, { deep: true, immediate: true });
  },
  { deep: true, immediate: true }
);
/** ä¸Šä¼ å‰loading加载 */
const handleBeforeUpload = (file: any) => {
    let isImg = false;
    if (props.fileType.length) {
        let fileExtension = "";
        if (file.name.lastIndexOf(".") > -1) {
            fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
        }
        isImg = props.fileType.some((type: any) => {
            if (file.type.indexOf(type) > -1) return true;
            if (fileExtension && fileExtension.indexOf(type) > -1) return true;
            return false;
        });
    } else {
        isImg = file.type.indexOf("image") > -1;
  let isImg = false;
  if (props.fileType.length) {
    let fileExtension = '';
    if (file.name.lastIndexOf('.') > -1) {
      fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
    }
    if (!isImg) {
        proxy?.$modal.msgError(
            `文件格式不正确, è¯·ä¸Šä¼ ${props.fileType.join("/")}图片格式文件!`
        );
        return false;
    isImg = props.fileType.some((type: any) => {
      if (file.type.indexOf(type) > -1) return true;
      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
      return false;
    });
  } else {
    isImg = file.type.indexOf('image') > -1;
  }
  if (!isImg) {
    proxy?.$modal.msgError(`文件格式不正确, è¯·ä¸Šä¼ ${props.fileType.join('/')}图片格式文件!`);
    return false;
  }
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
      return false;
    }
    if (props.fileSize) {
        const isLt = file.size / 1024 / 1024 < props.fileSize;
        if (!isLt) {
            proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
            return false;
        }
    }
    proxy?.$modal.loading("正在上传图片,请稍候...");
  }
  //压缩图片,开启压缩并且大于指定的压缩大小时才压缩
  if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
    proxy?.$modal.loading('正在上传图片,请稍候...');
    number.value++;
}
    return compressAccurately(file, props.compressTargetSize);
  } else {
    proxy?.$modal.loading('正在上传图片,请稍候...');
    number.value++;
  }
};
// æ–‡ä»¶ä¸ªæ•°è¶…出
const handleExceed = () => {
    proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} ä¸ª!`);
}
  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} ä¸ª!`);
};
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
const handleUploadSuccess = (res: any, file: UploadFile) => {
    if (res.code === 200) {
        uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
        uploadedSuccessfully();
    } else {
        number.value--;
        proxy?.$modal.closeLoading();
        proxy?.$modal.msgError(res.msg);
        imageUploadRef.value?.handleRemove(file);
        uploadedSuccessfully();
    }
}
  if (res.code === 200) {
    uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
    uploadedSuccessfully();
  } else {
    number.value--;
    proxy?.$modal.closeLoading();
    proxy?.$modal.msgError(res.msg);
    imageUploadRef.value?.handleRemove(file);
    uploadedSuccessfully();
  }
};
// åˆ é™¤å›¾ç‰‡
const handleDelete = (file: UploadFile): boolean => {
    const findex = fileList.value.map(f => f.name).indexOf(file.name);
    if (findex > -1 && uploadList.value.length === number.value) {
        let ossId = fileList.value[findex].ossId;
        delOss(ossId);
        fileList.value.splice(findex, 1);
        emit("update:modelValue", listToString(fileList.value));
        return false;
    }
    return true;
}
  const findex = fileList.value.map((f) => f.name).indexOf(file.name);
  if (findex > -1 && uploadList.value.length === number.value) {
    let ossId = fileList.value[findex].ossId;
    delOss(ossId);
    fileList.value.splice(findex, 1);
    emit('update:modelValue', listToString(fileList.value));
    return false;
  }
  return true;
};
// ä¸Šä¼ ç»“束处理
const uploadedSuccessfully = () => {
    if (number.value > 0 && uploadList.value.length === number.value) {
        fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
        uploadList.value = [];
        number.value = 0;
        emit("update:modelValue", listToString(fileList.value));
        proxy?.$modal.closeLoading();
    }
}
  if (number.value > 0 && uploadList.value.length === number.value) {
    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
    uploadList.value = [];
    number.value = 0;
    emit('update:modelValue', listToString(fileList.value));
    proxy?.$modal.closeLoading();
  }
};
// ä¸Šä¼ å¤±è´¥
const handleUploadError = () => {
    proxy?.$modal.msgError("上传图片失败");
    proxy?.$modal.closeLoading();
}
  proxy?.$modal.msgError('上传图片失败');
  proxy?.$modal.closeLoading();
};
// é¢„览
const handlePictureCardPreview = (file: any) => {
    dialogImageUrl.value = file.url;
    dialogVisible.value = true;
}
  dialogImageUrl.value = file.url;
  dialogVisible.value = true;
};
// å¯¹è±¡è½¬æˆæŒ‡å®šå­—符串分隔
const listToString = (list: any[], separator?: string) => {
    let strs = "";
    separator = separator || ",";
    for (let i in list) {
        if (undefined !== list[i].ossId && list[i].url.indexOf("blob:") !== 0) {
            strs += list[i].ossId + separator;
        }
  let strs = '';
  separator = separator || ',';
  for (let i in list) {
    if (undefined !== list[i].ossId && list[i].url.indexOf('blob:') !== 0) {
      strs += list[i].ossId + separator;
    }
    return strs != "" ? strs.substring(0, strs.length - 1) : "";
}
  }
  return strs != '' ? strs.substring(0, strs.length - 1) : '';
};
</script>
<style scoped lang="scss">
// .el-upload--picture-card æŽ§åˆ¶åŠ å·éƒ¨åˆ†
:deep(.hide .el-upload--picture-card) {
    display: none;
  display: none;
}
</style>
src/components/LangSelect/index.vue
@@ -14,22 +14,21 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useAppStore } from '@/store/modules/app';
import SvgIcon from '@/components/SvgIcon/index.vue';
const appStore = useAppStore();
const { locale } = useI18n();
const message: any = {
  zh_CN: '切换语言成功!',
  en_US: 'Switch Language Successful!',
}
const handleLanguageChange = (lang: string) => {
  en_US: 'Switch Language Successful!'
};
const handleLanguageChange = (lang: any) => {
  locale.value = lang;
  appStore.changeLanguage(lang);
  ElMessage.success(message[lang] || '切换语言成功!');
}
};
</script>
<style lang="scss" scoped>
src/components/Pagination/index.vue
@@ -1,9 +1,9 @@
<template>
  <div :class="{ 'hidden': hidden }" class="pagination-container">
  <div :class="{ hidden: hidden }" class="pagination-container">
    <el-pagination
      :background="background"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :background="background"
      :layout="layout"
      :page-sizes="pageSizes"
      :pager-count="pagerCount"
@@ -16,69 +16,69 @@
<script lang="ts">
export default {
    name: 'Pagination'
}
  name: 'Pagination'
};
</script>
<script setup lang="ts">
import { scrollTo } from '@/utils/scroll-to'
import { propTypes } from "@/utils/propTypes";
import { scrollTo } from '@/utils/scroll-to';
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
    total: propTypes.number,
    page: propTypes.number.def(1),
    limit: propTypes.number.def(20),
    pageSizes: {
      type: Array as PropType<number[]>,
      default: () => [10, 20, 30, 50]
    },
    // ç§»åŠ¨ç«¯é¡µç æŒ‰é’®çš„æ•°é‡ç«¯é»˜è®¤å€¼5
    pagerCount: propTypes.number.def(document.body.clientWidth < 992 ? 5 : 7),
    layout: propTypes.string.def('total, sizes, prev, pager, next, jumper'),
    background: propTypes.bool.def(true),
    autoScroll: propTypes.bool.def(true),
    hidden: propTypes.bool.def(false),
    float: propTypes.string.def('right')
})
  total: propTypes.number,
  page: propTypes.number.def(1),
  limit: propTypes.number.def(20),
  pageSizes: {
    type: Array,
    default: () => [10, 20, 30, 50]
  },
  // ç§»åŠ¨ç«¯é¡µç æŒ‰é’®çš„æ•°é‡ç«¯é»˜è®¤å€¼5
  pagerCount: propTypes.number.def(document.body.clientWidth < 992 ? 5 : 7),
  layout: propTypes.string.def('total, sizes, prev, pager, next, jumper'),
  background: propTypes.bool.def(true),
  autoScroll: propTypes.bool.def(true),
  hidden: propTypes.bool.def(false),
  float: propTypes.string.def('right')
});
const emit = defineEmits(['update:page', 'update:limit', 'pagination']);
const currentPage = computed({
    get() {
        return props.page
    },
    set(val) {
        emit('update:page', val)
    }
})
  get() {
    return props.page;
  },
  set(val) {
    emit('update:page', val);
  }
});
const pageSize = computed({
    get() {
        return props.limit
    },
    set(val){
        emit('update:limit', val)
    }
})
  get() {
    return props.limit;
  },
  set(val) {
    emit('update:limit', val);
  }
});
function handleSizeChange(val: number) {
    if (currentPage.value * val > props.total) {
        currentPage.value = 1
    }
    emit('pagination', { page: currentPage.value, limit: val })
    if (props.autoScroll) {
        scrollTo(0, 800)
    }
  if (currentPage.value * val > props.total) {
    currentPage.value = 1;
  }
  emit('pagination', { page: currentPage.value, limit: val });
  if (props.autoScroll) {
    scrollTo(0, 800);
  }
}
function handleCurrentChange(val: number) {
    emit('pagination', { page: val, limit: pageSize.value })
    if (props.autoScroll) {
        scrollTo(0, 800)
    }
  emit('pagination', { page: val, limit: pageSize.value });
  if (props.autoScroll) {
    scrollTo(0, 800);
  }
}
</script>
<style lang="scss" scoped>
.pagination-container {
  padding: 32px 16px;
  .el-pagination{
  .el-pagination {
    float: v-bind(float);
  }
}
src/components/Process/approvalRecord.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
<template>
  <div class="container">
    <el-dialog v-model="visible" draggable title="审批记录" :width="props.width" :height="props.height" :close-on-click-modal="false">
      <el-tabs v-model="tabActiveName" class="demo-tabs">
        <el-tab-pane label="流程图" name="bpmn">
          <BpmnView ref="bpmnViewRef"></BpmnView>
        </el-tab-pane>
        <el-tab-pane v-loading="loading" label="审批信息" name="info">
          <div>
            <el-table :data="historyList" style="width: 100%" border fit>
              <el-table-column type="index" label="序号" align="center" width="60"></el-table-column>
              <el-table-column prop="name" label="任务名称" sortable align="center"></el-table-column>
              <el-table-column prop="nickName" :show-overflow-tooltip="true" label="办理人" sortable align="center">
                <template #default="scope">
                  <el-tag type="success">{{ scope.row.nickName || '无' }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="状态" sortable align="center">
                <template #default="scope">
                  <el-tag type="success">{{ scope.row.statusName }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column prop="comment" label="审批意见" sortable align="center"></el-table-column>
              <el-table-column prop="startTime" label="开始时间" sortable align="center"></el-table-column>
              <el-table-column prop="endTime" label="结束时间" sortable align="center"></el-table-column>
              <el-table-column prop="runDuration" label="运行时长" sortable align="center"></el-table-column>
              <el-table-column prop="attachmentList" label="附件" sortable align="center">
                <template #default="scope">
                  <el-popover v-if="scope.row.attachmentList && scope.row.attachmentList.length > 0" placement="right" :width="310" trigger="click">
                    <template #reference>
                      <el-button style="margin-right: 16px">附件</el-button>
                    </template>
                    <el-table border :data="scope.row.attachmentList">
                      <el-table-column prop="name" width="202" :show-overflow-tooltip="true" label="附件名称"></el-table-column>
                      <el-table-column prop="name" width="80" align="center" :show-overflow-tooltip="true" label="操作">
                        <template #default="tool">
                          <el-button type="text" @click="handleDownload(tool.row.contentId)">下载</el-button>
                        </template>
                      </el-table-column>
                    </el-table>
                  </el-popover>
                </template>
              </el-table-column>
            </el-table>
          </div>
        </el-tab-pane>
      </el-tabs>
    </el-dialog>
  </div>
</template>
<script lang="ts" setup>
import BpmnView from '@/components/BpmnView/index.vue';
import processApi from '@/api/workflow/processInstance';
import { propTypes } from '@/utils/propTypes';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({
  width: propTypes.string.def('70%'),
  height: propTypes.string.def('100%')
});
const loading = ref(false);
const visible = ref(false);
const historyList = ref<Array<any>>([]);
const tabActiveName = ref('bpmn');
const bpmnViewRef = ref<BpmnView>();
//初始化查询审批记录
const init = async (instanceId: string) => {
  visible.value = true;
  loading.value = true;
  tabActiveName.value = 'bpmn';
  historyList.value = [];
  processApi.getHistoryRecord(instanceId).then((resp) => {
    historyList.value = resp.data;
    loading.value = false;
  });
  await nextTick(() => {
    bpmnViewRef.value.init(instanceId);
  });
};
/** ä¸‹è½½æŒ‰é’®æ“ä½œ */
const handleDownload = (ossId: string) => {
  proxy?.$download.oss(ossId);
};
/**
 * å¯¹å¤–暴露子组件方法
 */
defineExpose({
  init
});
</script>
<style lang="scss" scoped>
.triangle {
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  border-radius: 6px;
}
.triangle::after {
  content: ' ';
  position: absolute;
  top: 8em;
  right: 215px;
  border: 15px solid;
  border-color: transparent #fff transparent transparent;
}
.container {
  :deep(.el-dialog .el-dialog__body) {
    max-height: calc(100vh - 170px) !important;
    min-height: calc(100vh - 170px) !important;
  }
}
</style>
src/components/Process/multiInstanceUser.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,368 @@
<template>
  <el-dialog v-model="visible" draggable :title="title" :width="width" :height="height" append-to-body
    :close-on-click-modal="false">
    <div class="p-2" v-if="multiInstance === 'add'">
      <el-row :gutter="20">
        <!-- éƒ¨é—¨æ ‘ -->
        <el-col :lg="4" :xs="24" style="">
          <el-card shadow="hover">
            <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
            <el-tree class="mt-2" ref="deptTreeRef" node-key="id" :data="deptOptions"
              :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false"
              :filter-node-method="filterNode" highlight-current default-expand-all
              @node-click="handleNodeClick"></el-tree>
          </el-card>
        </el-col>
        <el-col :lg="20" :xs="24">
          <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
            :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
              <el-form ref="queryFormRef" :model="queryParams" :inline="true">
                <el-form-item label="用户名称" prop="userName">
                  <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
                </el-form-item>
                <el-form-item label="手机号码" prop="phonenumber">
                  <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
                </el-form-item>
                <el-form-item>
                  <el-button type="primary" @click="handleQuery" icon="Search">搜索</el-button>
                  <el-button @click="resetQuery" icon="Refresh">重置</el-button>
                </el-form-item>
              </el-form>
            </div>
          </transition>
          <el-card shadow="hover">
            <template #header>
              <el-row :gutter="10">
                <right-toolbar v-model:showSearch="showSearch" @queryTable="handleQuery" :search="true"></right-toolbar>
              </el-row>
            </template>
            <el-table v-loading="loading" :data="userList" ref="multipleTableRef" row-key="userId"
              @selection-change="handleSelectionChange">
              <el-table-column type="selection" width="50" align="center" />
              <el-table-column label="用户编号" align="center" key="userId" prop="userId" />
              <el-table-column label="用户名称" align="center" key="userName" prop="userName" :show-overflow-tooltip="true" />
              <el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" :show-overflow-tooltip="true" />
              <el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" width="120" />
              <el-table-column label="创建时间" align="center" prop="createTime" width="160">
                <template #default="scope">
                  <span>{{ scope.row.createTime }}</span>
                </template>
              </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
              v-model:limit="queryParams.pageSize" @pagination="handleQuery" />
          </el-card>
          <el-card shadow="hover">
            <el-tag v-for="(user, index) in chooseUserList" :key="user.userId" style="margin:2px" closable
              @close="handleCloseTag(user, index)">{{ user.userName }}
            </el-tag>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <div class="p-2" v-if="multiInstance === 'delete'">
      <el-table v-loading="loading" :data="taskList" @selection-change="handleTaskSelection">
        <el-table-column type="selection" width="55" />
        <el-table-column prop="name" label="任务名称" />
        <el-table-column prop="assigneeName" label="办理人" />
      </el-table>
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button type="primary" @click="submitFileForm">ç¡® å®š</el-button>
        <el-button @click="visible = false">取 æ¶ˆ</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup name="User" lang="ts">
import { deptTreeSelect, listUser, optionSelect } from '@/api/system/user';
import {
  addMultiInstanceExecution,
  deleteMultiInstanceExecution,
  getTaskUserIdsByAddMultiInstance,
  getListByDeleteMultiInstance
} from '@/api/workflow/task';
import { UserVO } from '@/api/system/user/types';
import { DeptVO } from '@/api/system/dept/types';
import { ComponentInternalInstance } from 'vue';
import { ElTree, ElTable } from 'element-plus';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({
  // å®½
  width: {
    type: String,
    default: '70%'
  },
  // é«˜
  height: {
    type: String,
    default: '100%'
  },
  // æ ‡é¢˜
  title: {
    type: String,
    default: '加签人员'
  },
  //是否多选
  multiple: {
    type: Boolean,
    default: true
  },
  //回显用户id
  userIdList: {
    type: Array,
    default: []
  }
});
const deptTreeRef = ref(ElTree);
const multipleTableRef = ref(ElTable);
const userList = ref<UserVO[]>();
const taskList = ref<Array<any>[]>();
const loading = ref(true);
const showSearch = ref(true);
const selectionTask = ref<Array<any>[]>();
const visible = ref(false);
const total = ref(0);
const deptName = ref('');
const deptOptions = ref<DeptVO[]>([]);
const chooseUserList = ref(ref<UserVO[]>());
const userIds = ref<Array<number | string>>([]);
//加签或者减签
const multiInstance = ref('');
const queryParams = ref<Record<string, any>>({
  pageNum: 1,
  pageSize: 10,
  userName: '',
  nickName: '',
  taskId: ''
});
/** æŸ¥è¯¢ç”¨æˆ·åˆ—表 */
const getAddMultiInstanceList = async (taskId: string, userIdList: Array<number | string>) => {
  deptOptions.value = [];
  getTreeSelect();
  multiInstance.value = 'add';
  userIds.value = userIdList;
  visible.value = true;
  queryParams.value.taskId = taskId;
  loading.value = true;
  const res1 = await getTaskUserIdsByAddMultiInstance(taskId);
  queryParams.value.excludeUserIds = res1.data;
  const res = await listUser(queryParams.value);
  loading.value = false;
  userList.value = res.rows;
  total.value = res.total;
  if (userList.value && userIds.value.length > 0) {
    const data = await optionSelect(userIds.value);
    if (data.data && data.data.length > 0) {
      chooseUserList.value = data.data;
      data.data.forEach((user: UserVO) => {
        multipleTableRef.value!.toggleRowSelection(
          userList.value.find((item) => {
            return item.userId == user.userId;
          }),
          true
        );
      });
    }
  }
};
const getList = async () => {
  loading.value = true;
  const res1 = await getTaskUserIdsByAddMultiInstance(queryParams.value.taskId);
  queryParams.value.excludeUserIds = res1.data;
  const res = await listUser(queryParams.value);
  loading.value = false;
  userList.value = res.rows;
  total.value = res.total;
  if (userList.value && userIds.value.length > 0) {
    const data = await optionSelect(userIds.value);
    if (data.data && data.data.length > 0) {
      chooseUserList.value = data.data;
      data.data.forEach((user: UserVO) => {
        multipleTableRef.value!.toggleRowSelection(
          userList.value.find((item) => {
            return item.userId == user.userId;
          }),
          true
        );
      });
    }
  }
};
const getDeleteMultiInstanceList = async (taskId: string) => {
  deptOptions.value = [];
  loading.value = true;
  queryParams.value.taskId = taskId;
  multiInstance.value = 'delete';
  visible.value = true;
  const res = await getListByDeleteMultiInstance(taskId);
  taskList.value = res.data;
  loading.value = false;
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getAddMultiInstanceList(queryParams.value.taskId, userIds.value);
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryParams.value.pageNum = 1;
  queryParams.value.deptId = undefined;
  queryParams.value.userName = undefined;
  queryParams.value.nickName = undefined;
  deptTreeRef.value.setCurrentKey(null);
  handleQuery();
};
/** é€‰æ‹©æ¡æ•°  */
const handleSelectionChange = (selection: UserVO[]) => {
  if (props.multiple) {
    chooseUserList.value = selection.filter((element, index, self) => {
      return self.findIndex((x) => x.userId === element.userId) === index;
    });
    selection.forEach((u) => {
      if (chooseUserList.value && !chooseUserList.value.includes(u)) {
        multipleTableRef.value!.toggleRowSelection(u, undefined);
      }
    });
    userIds.value = chooseUserList.value.map((item) => {
      return item.userId;
    });
  } else {
    chooseUserList.value = selection;
    if (selection.length > 1) {
      let delRow = selection.shift();
      multipleTableRef.value!.toggleRowSelection(delRow, undefined);
    }
    if (selection.length === 0) {
      chooseUserList.value = [];
    }
  }
};
/** é€‰æ‹©æ¡æ•°  */
const handleTaskSelection = (selection: any) => {
  selectionTask.value = selection;
};
/** æŸ¥è¯¢éƒ¨é—¨ä¸‹æ‹‰æ ‘结构 */
const getTreeSelect = async () => {
  const res = await deptTreeSelect();
  deptOptions.value = res.data;
};
/** é€šè¿‡æ¡ä»¶è¿‡æ»¤èŠ‚ç‚¹  */
const filterNode = (value: string, data: any) => {
  if (!value) return true;
  return data.label.indexOf(value) !== -1;
};
/** æ ¹æ®åç§°ç­›é€‰éƒ¨é—¨æ ‘ */
watchEffect(
  () => {
    if (visible.value && deptOptions.value && deptOptions.value.length > 0) {
      deptTreeRef.value.filter(deptName.value);
    }
  },
  {
    flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
  }
);
/** èŠ‚ç‚¹å•å‡»äº‹ä»¶ */
const handleNodeClick = (data: DeptVO) => {
  queryParams.value.deptId = data.id;
  getList();
};
//删除tag
const handleCloseTag = (user: UserVO, index: any) => {
  if (multipleTableRef.value.selection && multipleTableRef.value.selection.length > 0) {
    multipleTableRef.value.selection.forEach((u: UserVO, i: Number) => {
      if (user.userId === u.userId) {
        multipleTableRef.value.selection.splice(i, 1);
      }
    });
  }
  if (chooseUserList.value && chooseUserList.value.length > 0) {
    chooseUserList.value.splice(index, 1);
  }
  multipleTableRef.value.toggleRowSelection(user, undefined);
  if (userIds.value && userIds.value.length > 0) {
    userIds.value.forEach((userId, i) => {
      if (userId === user.userId) {
        userIds.value.splice(i, 1);
      }
    });
  }
};
const submitFileForm = async () => {
  if (multiInstance.value === 'add') {
    if (chooseUserList.value && chooseUserList.value.length > 0) {
      loading.value = true;
      let userIds = chooseUserList.value.map((item) => {
        return item.userId;
      });
      let nickNames = chooseUserList.value.map((item) => {
        return item.nickName;
      });
      let params = {
        taskId: queryParams.value.taskId,
        assignees: userIds,
        assigneeNames: nickNames
      };
      await addMultiInstanceExecution(params);
      emits('submitCallback');
      loading.value = false;
      proxy?.$modal.msgSuccess('操作成功');
      visible.value = false;
    }
  } else {
    if (selectionTask.value && selectionTask.value.length > 0) {
      loading.value = true;
      let taskIds = selectionTask.value.map((item: any) => {
        return item.id;
      });
      let executionIds = selectionTask.value.map((item: any) => {
        return item.executionId;
      });
      let assigneeIds = selectionTask.value.map((item: any) => {
        return item.assignee;
      });
      let assigneeNames = selectionTask.value.map((item: any) => {
        return item.assigneeName;
      });
      let params = {
        taskId: queryParams.value.taskId,
        taskIds: taskIds,
        executionIds: executionIds,
        assigneeIds: assigneeIds,
        assigneeNames: assigneeNames
      };
      await deleteMultiInstanceExecution(params);
      emits('submitCallback');
      loading.value = false;
      proxy?.$modal.msgSuccess('操作成功');
      visible.value = false;
    }
  }
};
//事件
const emits = defineEmits(['submitCallback']);
/**
 * å¯¹å¤–暴露子组件方法
 */
defineExpose({
  getAddMultiInstanceList,
  getDeleteMultiInstanceList
});
</script>
src/components/Process/submitVerify.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,353 @@
<template>
  <el-dialog v-model="dialog.visible" :title="dialog.title" width="50%" draggable :before-close="cancel" center :close-on-click-modal="false">
    <el-form v-loading="loading" :model="form" label-width="120px">
      <el-form-item label="消息提醒">
        <el-checkbox-group v-model="form.messageType">
          <el-checkbox label="1" name="type" disabled>站内信</el-checkbox>
          <el-checkbox label="2" name="type">邮件</el-checkbox>
          <el-checkbox label="3" name="type">短信</el-checkbox>
        </el-checkbox-group>
      </el-form-item>
      <el-form-item label="附件" v-if="task.businessStatus === 'waiting'">
        <fileUpload v-model="form.fileId" :fileType="['doc', 'xls', 'ppt', 'txt', 'pdf', 'xlsx', 'docx', 'zip']" :fileSize="'20'"/>
      </el-form-item>
      <el-form-item label="抄送">
        <el-button type="primary" @click="openUserSelectCopy" icon="Plus" circle />
          <el-tag v-for="user in selectCopyUserList" :key="user.userId" closable style="margin: 2px" @close="handleCopyCloseTag(user)">
            {{ user.userName }}
          </el-tag>
      </el-form-item>
      <el-form-item label="审批意见" v-if="task.businessStatus === 'waiting'">
        <el-input v-model="form.message" type="textarea" resize="none" />
      </el-form-item>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button v-loading="buttonLoading" type="primary" @click="handleCompleteTask"> æäº¤ </el-button>
        <el-button v-if="task.businessStatus === 'waiting'" v-loading="buttonLoading" type="primary" @click="openDelegateTask"> å§”托 </el-button>
        <el-button v-if="task.businessStatus === 'waiting'" v-loading="buttonLoading" type="primary" @click="openTransferTask"> è½¬åŠž </el-button>
        <el-button v-if="task.businessStatus === 'waiting' && task.multiInstance" v-loading="buttonLoading" type="primary" @click="addMultiInstanceUser"> åŠ ç­¾ </el-button>
        <el-button v-if="task.businessStatus === 'waiting' && task.multiInstance" v-loading="buttonLoading" type="primary" @click="deleteMultiInstanceUser"> å‡ç­¾ </el-button>
        <el-button v-if="task.businessStatus === 'waiting'" v-loading="buttonLoading" type="danger" @click="handleTerminationTask"> ç»ˆæ­¢ </el-button>
        <el-button v-if="task.businessStatus === 'waiting'" v-loading="buttonLoading" type="danger" @click="handleBackProcessOpen"> é€€å›ž </el-button>
        <el-button v-loading="buttonLoading" @click="cancel">取消</el-button>
      </span>
    </template>
    <!-- æŠ„送 -->
    <UserSelect ref="userSelectCopyRef" :multiple="true" :data="selectCopyUserIds" @confirm-call-back="userSelectCopyCallBack"></UserSelect>
    <!-- è½¬åŠž -->
    <UserSelect ref="transferTaskRef" :multiple="false" @confirm-call-back="handleTransferTask"></UserSelect>
    <!-- å§”托 -->
    <UserSelect ref="delegateTaskRef" :multiple="false" @confirm-call-back="handleDelegateTask"></UserSelect>
    <!-- åŠ ç­¾ç»„ä»¶ -->
    <multiInstanceUser ref="multiInstanceUserRef" :title="title" @submit-callback='closeDialog' />
    <!-- é©³å›žå¼€å§‹ -->
    <el-dialog v-model="backVisible" draggable title="驳回" width="40%" :close-on-click-modal="false">
      <el-form v-loading="backLoading" :model="backForm" label-width="120px" v-if="task.businessStatus === 'waiting'">
        <el-form-item label="驳回节点">
          <el-select clearable placeholder="请选择" v-model="backForm.targetActivityId" style="width: 300px">
            <el-option
              v-for="item in taskNodeList"
              :key="item.nodeId"
              :label="item.nodeName"
              :value="item.nodeId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="消息提醒">
          <el-checkbox-group v-model="backForm.messageType">
            <el-checkbox label="1" name="type" disabled>站内信</el-checkbox>
            <el-checkbox label="2" name="type">邮件</el-checkbox>
            <el-checkbox label="3" name="type">短信</el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        <el-form-item label="审批意见">
          <el-input v-model="backForm.message" type="textarea" resize="none" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer" style="float: right;padding-bottom: 20px;">
          <el-button type="primary" v-loading="backLoading" @click="handleBackProcess">确认</el-button>
          <el-button v-loading="backLoading" @click="backVisible = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é©³å›žç»“束 -->
  </el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ComponentInternalInstance } from 'vue';
import { ElForm } from 'element-plus';
import { completeTask, backProcess, getTaskById,transferTask,terminationTask,getTaskNodeList,delegateTask } from '@/api/workflow/task';
import UserSelect from '@/components/UserSelect';
import MultiInstanceUser from '@/components/Process/multiInstanceUser.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { UserVO } from '@/api/system/user/types';
import { TaskVO } from '@/api/workflow/task/types';
const userSelectCopyRef = ref<InstanceType<typeof UserSelect>>();
const transferTaskRef = ref<InstanceType<typeof UserSelect>>();
const delegateTaskRef = ref<InstanceType<typeof UserSelect>>();
  //加签组件
const multiInstanceUserRef = ref<InstanceType<typeof MultiInstanceUser>>();
const props = defineProps({
  taskVariables: {
    type: Object as () => Record<string, any>,
    default: {}
  }
});
//遮罩层
const loading = ref(true);
//按钮
const buttonLoading = ref(true);
//任务id
const taskId = ref<string>('');
//抄送人
const selectCopyUserList = ref<UserVO[]>([]);
//抄送人id
const selectCopyUserIds = ref<string>(undefined);
// é©³å›žæ˜¯å¦æ˜¾ç¤º
const backVisible = ref(false);
const backLoading = ref(true);
// å¯é©³å›žå¾—任务节点
const taskNodeList = ref([]);
//任务
const task = ref<TaskVO>({
  id: undefined,
  name: undefined,
  description: undefined,
  priority: undefined,
  owner: undefined,
  assignee: undefined,
  assigneeName: undefined,
  processInstanceId: undefined,
  executionId: undefined,
  taskDefinitionId: undefined,
  processDefinitionId: undefined,
  endTime: undefined,
  taskDefinitionKey: undefined,
  dueDate: undefined,
  category: undefined,
  parentTaskId: undefined,
  tenantId: undefined,
  claimTime: undefined,
  businessStatus: undefined,
  businessStatusName: undefined,
  processDefinitionName: undefined,
  processDefinitionKey: undefined,
  participantVo: undefined,
  multiInstance: undefined,
  businessKey: undefined,
  wfNodeConfigVo: undefined
});
//加签 å‡ç­¾æ ‡é¢˜
const title = ref('');
const dialog = reactive<DialogOption>({
  visible: false,
  title: '提示'
});
const form = ref<Record<string, any>>({
  taskId: undefined,
  message: undefined,
  variables: {},
  messageType: ['1'],
  wfCopyList: []
});
const backForm = ref<Record<string, any>>({
  taskId: undefined,
  targetActivityId: undefined,
  message: undefined,
  variables: {},
  messageType: ['1']
});
const closeDialog = () => {
  dialog.visible = false
}
//打开弹窗
const openDialog = (id?: string) => {
  selectCopyUserIds.value = undefined
  selectCopyUserList.value = []
  form.value.fileId = undefined
  taskId.value = id;
  form.value.message = undefined;
  dialog.visible = true;
  loading.value = true;
  buttonLoading.value = true;
  nextTick(() => {
    getTaskById(taskId.value).then((response) => {
      task.value = response.data;
      loading.value = false;
      buttonLoading.value = false;
    });
  });
};
onMounted(() => {});
const emits = defineEmits(['submitCallback', 'cancelCallback']);
/** åŠžç†æµç¨‹ */
const handleCompleteTask = async () => {
  form.value.taskId = taskId.value;
  form.value.taskVariables = props.taskVariables;
  if(selectCopyUserList && selectCopyUserList.value.length > 0){
    let wfCopyList = []
    selectCopyUserList.value.forEach( e=> {
      let copyUser = {
        userId: e.userId,
        userName: e.nickName
      }
      wfCopyList.push(copyUser)
    })
    form.value.wfCopyList = wfCopyList
  }
  await proxy?.$modal.confirm('是否确认提交?');
  loading.value = true;
  buttonLoading.value = true;
  try {
    await completeTask(form.value);
    dialog.visible = false;
    emits('submitCallback');
    proxy?.$modal.msgSuccess('操作成功');
  } finally {
    loading.value = false
    buttonLoading.value = false
  }
};
/** é©³å›žå¼¹çª—打开 */
const handleBackProcessOpen = async () => {
  backForm.value = {}
  backForm.value.messageType = ['1']
  backVisible.value = true
  backLoading.value = true
  let data = await getTaskNodeList(task.value.processInstanceId)
  taskNodeList.value = data.data
  backLoading.value = false
  backForm.value.targetActivityId = taskNodeList.value[0].nodeId
}
/** é©³å›žæµç¨‹ */
const handleBackProcess = async () => {
  backForm.value.taskId = taskId.value;
  await proxy?.$modal.confirm('是否确认驳回到申请人?');
  loading.value = true;
  backLoading.value = true;
  await backProcess(backForm.value).finally(() => (loading.value = false));
  dialog.visible = false;
  backLoading.value = false
  emits('submitCallback');
  proxy?.$modal.msgSuccess('操作成功');
};
//取消
const cancel = async () => {
  dialog.visible = false;
  buttonLoading.value = false;
  emits('cancelCallback');
};
//打开抄送人员
const openUserSelectCopy = () => {
  userSelectCopyRef.value.open();
};
//确认抄送人员
const userSelectCopyCallBack = (data: UserVO[]) => {
  if(data && data.length > 0){
    selectCopyUserList.value = data
    selectCopyUserIds.value = selectCopyUserList.value.map((item) => item.userId).join(',');
  }
}
//删除抄送人员
const handleCopyCloseTag = (user: UserVO) => {
  const userId = user.userId;
  // ä½¿ç”¨split删除用户
  const index = selectCopyUserList.value.findIndex((item) => item.userId === userId);
  selectCopyUserList.value.splice(index, 1);
  selectCopyUserIds.value = selectCopyUserList.value.map((item) => item.userId).join(',');
};
//加签
const addMultiInstanceUser = () => {
  if (multiInstanceUserRef.value) {
    title.value = '加签人员';
    multiInstanceUserRef.value.getAddMultiInstanceList(taskId.value, []);
  }
};
//减签
const deleteMultiInstanceUser = () => {
  if (multiInstanceUserRef.value) {
    title.value = '减签人员';
    multiInstanceUserRef.value.getDeleteMultiInstanceList(taskId.value);
  }
};
//打开转办
const openTransferTask = () => {
  transferTaskRef.value.open();
};
//转办
const handleTransferTask  = async (data) => {
  if(data && data.length > 0){
    let params = {
      taskId: taskId.value,
      userId: data[0].userId,
      comment: form.value.message
    }
    await proxy?.$modal.confirm('是否确认提交?');
    loading.value = true;
    buttonLoading.value = true;
    await transferTask(params).finally(() => (loading.value = false));
    dialog.visible = false;
    emits('submitCallback');
    proxy?.$modal.msgSuccess('操作成功');
  }else{
    proxy?.$modal.msgWarning('请选择用户!');
  }
}
//打开委托
const openDelegateTask = () => {
  delegateTaskRef.value.open();
};
//委托
const handleDelegateTask  = async (data) => {
  if(data && data.length > 0){
    let params = {
      taskId: taskId.value,
      userId: data[0].userId,
      nickName: data[0].nickName
    }
    await proxy?.$modal.confirm('是否确认提交?');
    loading.value = true;
    buttonLoading.value = true;
    await delegateTask(params).finally(() => (loading.value = false));
    dialog.visible = false;
    emits('submitCallback');
    proxy?.$modal.msgSuccess('操作成功');
  }else{
    proxy?.$modal.msgWarning('请选择用户!');
  }
}
//终止任务
const handleTerminationTask  = async (data) => {
    let params = {
      taskId: taskId.value,
      comment: form.value.message
    }
    await proxy?.$modal.confirm('是否确认终止?');
    loading.value = true;
    buttonLoading.value = true;
    await terminationTask(params).finally(() => (loading.value = false));
    dialog.visible = false;
    emits('submitCallback');
    proxy?.$modal.msgSuccess('操作成功');
}
/**
 * å¯¹å¤–暴露子组件方法
 */
defineExpose({
  openDialog
});
</script>
src/components/RightToolbar/index.vue
@@ -1,13 +1,13 @@
<template>
  <div class="top-right-btn" :style="style">
    <el-row>
      <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
      <el-tooltip v-if="search" class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top">
        <el-button circle icon="Search" @click="toggleSearch()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="刷新" placement="top">
        <el-button circle icon="Refresh" @click="refresh()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="显示/隐藏列" placement="top" v-if="columns">
      <el-tooltip v-if="columns" class="item" effect="dark" content="显示/隐藏列" placement="top">
        <div class="show-btn">
          <el-popover placement="bottom" trigger="click">
            <div class="tree-header">显示/隐藏列</div>
@@ -15,9 +15,9 @@
              ref="columnRef"
              :data="columns"
              show-checkbox
              @check="columnChange"
              node-key="key"
              :props="{ label: 'label', children: 'children' }"
              @check="columnChange"
            ></el-tree>
            <template #reference>
              <el-button circle icon="Menu" />
@@ -33,51 +33,49 @@
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
    showSearch: propTypes.bool.def(true),
    columns: {
        type: Array as PropType<FieldOption[]>,
    },
    search: propTypes.bool.def(true),
    gutter: propTypes.number.def(10),
})
  showSearch: propTypes.bool.def(true),
  columns: propTypes.fieldOption,
  search: propTypes.bool.def(true),
  gutter: propTypes.number.def(10)
});
const columnRef = ref<ElTreeInstance>();
const emits = defineEmits(['update:showSearch', 'queryTable']);
const style = computed(() => {
    const ret: any = {};
    if (props.gutter) {
        ret.marginRight = `${props.gutter / 2}px`;
    }
    return ret;
  const ret: any = {};
  if (props.gutter) {
    ret.marginRight = `${props.gutter / 2}px`;
  }
  return ret;
});
// æœç´¢
function toggleSearch() {
    emits("update:showSearch", !props.showSearch);
  emits('update:showSearch', !props.showSearch);
}
// åˆ·æ–°
function refresh() {
    emits("queryTable");
  emits('queryTable');
}
// æ›´æ”¹æ•°æ®åˆ—的显示和隐藏
function columnChange(...args: any[]) {
  props.columns?.forEach((item) => {
    item.visible = args[1].checkedKeys.includes(item.key);
  })
  });
}
// æ˜¾éšåˆ—初始默认隐藏列
onMounted(() => {
    props.columns?.forEach((item) => {
        if (item.visible) {
          columnRef.value?.setChecked(item.key, true, false);
            // value.value.push(item.key);
        }
    })
})
  props.columns?.forEach((item) => {
    if (item.visible) {
      columnRef.value?.setChecked(item.key, true, false);
      // value.value.push(item.key);
    }
  });
});
</script>
<style lang="scss" scoped>
@@ -93,7 +91,7 @@
.my-el-transfer {
  text-align: center;
}
.tree-header{
.tree-header {
  width: 100%;
  line-height: 24px;
  text-align: center;
src/components/RoleSelect/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,250 @@
<template>
  <div>
    <el-dialog v-model="roleDialog.visible.value" :title="roleDialog.title.value" width="80%" append-to-body>
      <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
        <div v-show="showSearch" class="mb-[10px]">
          <el-card shadow="hover">
            <el-form ref="queryFormRef" :model="queryParams" :inline="true">
              <el-form-item label="角色名称" prop="roleName">
                <el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable @keyup.enter="handleQuery" />
              </el-form-item>
              <el-form-item label="权限字符" prop="roleKey">
                <el-input v-model="queryParams.roleKey" placeholder="请输入权限字符" clearable @keyup.enter="handleQuery" />
              </el-form-item>
              <el-form-item>
                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
              </el-form-item>
            </el-form>
          </el-card>
        </div>
      </transition>
      <el-card shadow="hover">
        <template #header>
          <el-tag v-for="role in selectRoleList" :key="role.roleId" closable style="margin: 2px" @close="handleCloseTag(role)">
            {{ role.roleName }}
          </el-tag>
        </template>
        <vxe-table
          ref="tableRef"
          height="400px"
          border
          show-overflow
          :data="roleList"
          :loading="loading"
          :row-config="{ keyField: 'roleId' }"
          :checkbox-config="{ reserve: true, checkRowKeys: defaultSelectRoleIds }"
          highlight-current-row
          @checkbox-all="handleCheckboxAll"
          @checkbox-change="handleCheckboxChange"
        >
          <vxe-column type="checkbox" width="50" align="center" />
          <vxe-column v-if="false" key="roleId" label="角色编号" />
          <vxe-column field="roleName" title="角色名称" />
          <vxe-column field="roleKey" title="权限字符" />
          <vxe-column field="roleSort" title="显示顺序" width="100" />
          <vxe-column title="状态" align="center" width="100">
            <template #default="scope">
              <dict-tag :options="sys_normal_disable" :value="scope.row.status"></dict-tag>
            </template>
          </vxe-column>
          <vxe-column field="createTime" title="创建时间" align="center">
            <template #default="scope">
              <span>{{ parseTime(scope.row.createTime) }}</span>
            </template>
          </vxe-column>
        </vxe-table>
        <pagination
          v-if="total > 0"
          v-model:total="total"
          v-model:page="queryParams.pageNum"
          v-model:limit="queryParams.pageSize"
          @pagination="pageList"
        />
      </el-card>
      <template #footer>
        <el-button @click="close">取消</el-button>
        <el-button type="primary" @click="confirm">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import { RoleVO, RoleQuery } from '@/api/system/role/types';
import { VxeTableInstance } from 'vxe-table';
import useDialog from '@/hooks/useDialog';
import api from '@/api/system/role';
interface PropType {
  modelValue?: RoleVO[] | RoleVO | undefined;
  multiple?: boolean;
  data?: string | number | (string | number)[];
}
const prop = withDefaults(defineProps<PropType>(), {
  multiple: true,
  modelValue: undefined,
  data: undefined
});
const emit = defineEmits(['update:modelValue', 'confirmCallBack']);
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const roleList = ref<RoleVO[]>();
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const selectRoleList = ref<RoleVO[]>([]);
const roleDialog = useDialog({
  title: '角色选择'
});
const queryFormRef = ref<ElFormInstance>();
const tableRef = ref<VxeTableInstance<RoleVO>>();
const queryParams = ref<RoleQuery>({
  pageNum: 1,
  pageSize: 10,
  roleName: '',
  roleKey: '',
  status: ''
});
const defaultSelectRoleIds = computed(() => computedIds(prop.data));
const confirm = () => {
  emit('update:modelValue', selectRoleList.value);
  emit('confirmCallBack', selectRoleList.value);
  roleDialog.closeDialog();
};
const computedIds = (data) => {
  if (data instanceof Array) {
    return [...data];
  } else if (typeof data === 'string') {
    return data.split(',');
  } else if (typeof data === 'number') {
    return [data];
  } else {
    console.warn('<RoleSelect> The data type of data should be array or string or number, but I received other');
    return [];
  }
};
/**
 * æŸ¥è¯¢è§’色列表
 */
const getList = () => {
  loading.value = true;
  api.listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
    roleList.value = res.rows;
    total.value = res.total;
    loading.value = false;
  });
};
const pageList = async () => {
  await getList();
  const roles = roleList.value.filter((item) => {
    return selectRoleList.value.some((role) => role.roleId === item.roleId);
  });
  await tableRef.value.setCheckboxRow(roles, true);
};
/**
 * æœç´¢æŒ‰é’®æ“ä½œ
 */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
};
/** é‡ç½® */
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value?.resetFields();
  handleQuery();
};
const handleCheckboxChange = (checked) => {
  if (!prop.multiple && checked.checked) {
    tableRef.value.setCheckboxRow(selectRoleList.value, false);
    selectRoleList.value = [];
  }
  const row = checked.row;
  if (checked.checked) {
    selectRoleList.value.push(row);
  } else {
    selectRoleList.value = selectRoleList.value.filter((item) => {
      return item.roleId !== row.roleId;
    });
  }
};
const handleCheckboxAll = (checked) => {
  const rows = roleList.value;
  if (checked.checked) {
    rows.forEach((row) => {
      if (!selectRoleList.value.some((item) => item.roleId === row.roleId)) {
        selectRoleList.value.push(row);
      }
    });
  } else {
    selectRoleList.value = selectRoleList.value.filter((item) => {
      return !rows.some((row) => row.roleId === item.roleId);
    });
  }
};
const handleCloseTag = (user: RoleVO) => {
  const roleId = user.roleId;
  // ä½¿ç”¨split删除用户
  const index = selectRoleList.value.findIndex((item) => item.roleId === roleId);
  const rows = selectRoleList.value[index];
  tableRef.value?.setCheckboxRow(rows, false);
  selectRoleList.value.splice(index, 1);
};
/**
 * åˆå§‹åŒ–选中数据
 */
const initSelectRole = async () => {
  if (defaultSelectRoleIds.value.length > 0) {
    const { data } = await api.optionSelect(defaultSelectRoleIds.value);
    selectRoleList.value = data;
    const users = roleList.value.filter((item) => {
      return defaultSelectRoleIds.value.includes(String(item.roleId));
    });
    await nextTick(() => {
      tableRef.value.setCheckboxRow(users, true);
    });
  }
};
const close = () => {
  roleDialog.closeDialog();
};
watch(
  () => roleDialog.visible.value,
  (newValue: boolean) => {
    if (newValue) {
      initSelectRole();
    } else {
      tableRef.value.clearCheckboxReserve();
      tableRef.value.clearCheckboxRow();
      resetQuery();
      selectRoleList.value = [];
    }
  }
);
onMounted(() => {
  getList(); // åˆå§‹åŒ–列表数据
});
defineExpose({
  open: roleDialog.openDialog,
  close: roleDialog.closeDialog
});
</script>
src/components/RuoYiDoc/index.vue
@@ -8,6 +8,6 @@
const url = ref('https://plus-doc.dromara.org/');
function goto() {
  window.open(url.value)
  window.open(url.value);
}
</script>
src/components/RuoYiGit/index.vue
@@ -8,6 +8,6 @@
const url = ref('https://gitee.com/dromara/RuoYi-Vue-Plus');
function goto() {
  window.open(url.value)
  window.open(url.value);
}
</script>
src/components/SizeSelect/index.vue
@@ -16,20 +16,20 @@
</template>
<script setup lang="ts">
import useAppStore from "@/store/modules/app";
import useAppStore from '@/store/modules/app';
const appStore = useAppStore();
const size = computed(() => appStore.size);
const sizeOptions = ref([
    { label: "较大", value: "large" },
    { label: "默认", value: "default" },
    { label: "稍小", value: "small" },
  { label: '较大', value: 'large' },
  { label: '默认', value: 'default' },
  { label: '稍小', value: 'small' }
]);
const handleSetSize = (size: string) => {
    appStore.setSize(size);
}
const handleSetSize = (size: 'large' | 'default' | 'small') => {
  appStore.setSize(size);
};
</script>
<style lang="scss" scoped>
src/components/SvgIcon/index.vue
@@ -8,17 +8,17 @@
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
    iconClass: propTypes.string.isRequired,
    className: propTypes.string.def(''),
    color: propTypes.string.def(''),
})
const iconName =  computed(() => `#icon-${props.iconClass}`);
  iconClass: propTypes.string.isRequired,
  className: propTypes.string.def(''),
  color: propTypes.string.def('')
});
const iconName = computed(() => `#icon-${props.iconClass}`);
const svgClass = computed(() => {
    if (props.className) {
        return `svg-icon ${props.className}`
    }
    return 'svg-icon'
})
  if (props.className) {
    return `svg-icon ${props.className}`;
  }
  return 'svg-icon';
});
</script>
<style scope lang="scss">
src/components/TopNav/index.vue
@@ -1,19 +1,18 @@
<template>
  <el-menu :default-active="activeMenu" mode="horizontal" @select="handleSelect" :ellipsis="false">
  <el-menu :default-active="activeMenu" mode="horizontal" :ellipsis="false" @select="handleSelect">
    <template v-for="(item, index) in topMenus">
      <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
        ><svg-icon
        v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
        :icon-class="item.meta ? item.meta.icon : '' " /> {{ item.meta?.title }}</el-menu-item
      <el-menu-item v-if="index < visibleNumber" :key="index" :style="{ '--theme': theme }" :index="item.path"
        ><svg-icon v-if="item.meta && item.meta.icon && item.meta.icon !== '#'" :icon-class="item.meta ? item.meta.icon : ''" />
        {{ item.meta?.title }}</el-menu-item
      >
    </template>
    <!-- é¡¶éƒ¨èœå•超出数量折叠 -->
    <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
    <el-sub-menu v-if="topMenus.length > visibleNumber" :style="{ '--theme': theme }" index="more">
      <template #title>更多菜单</template>
      <template v-for="(item, index) in topMenus">
        <el-menu-item :index="item.path" :key="index" v-if="index >= visibleNumber"
          ><svg-icon :icon-class="item.meta ? item.meta.icon : '' " /> {{ item.meta?.title }}</el-menu-item
        <el-menu-item v-if="index >= visibleNumber" :key="index" :index="item.path"
          ><svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> {{ item.meta?.title }}</el-menu-item
        >
      </template>
    </el-sub-menu>
@@ -26,7 +25,7 @@
import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
import { RouteOption } from 'vue-router';
import { RouteRecordRaw } from 'vue-router';
// é¡¶éƒ¨æ åˆå§‹æ•°
const visibleNumber = ref<number>(-1);
@@ -35,86 +34,89 @@
// éšè—ä¾§è¾¹æ è·¯ç”±
const hideList = ['/index', '/user/profile'];
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const route = useRoute();
const router = useRouter();
// ä¸»é¢˜é¢œè‰²
const theme = computed(() => settingsStore.theme);
// æ‰€æœ‰çš„路由信息
const routers = computed(() => permissionStore.topbarRouters);
const routers = computed(() => permissionStore.getTopbarRoutes());
// é¡¶éƒ¨æ˜¾ç¤ºèœå•
const topMenus = computed(() => {
  let topMenus:RouteOption[] = [];
  let topMenus: RouteRecordRaw[] = [];
  routers.value.map((menu) => {
    if (menu.hidden !== true) {
      // å…¼å®¹é¡¶éƒ¨æ ä¸€çº§èœå•内部跳转
      if (menu.path === "/") {
          topMenus.push(menu.children? menu.children[0] : menu);
      if (menu.path === '/') {
        topMenus.push(menu.children ? menu.children[0] : menu);
      } else {
          topMenus.push(menu);
        topMenus.push(menu);
      }
    }
  })
  });
  return topMenus;
})
});
// è®¾ç½®å­è·¯ç”±
const childrenMenus = computed(() => {
  let childrenMenus:RouteOption[] = [];
  let childrenMenus: RouteRecordRaw[] = [];
  routers.value.map((router) => {
    router.children?.forEach((item) => {
      if (item.parentPath === undefined) {
        if(router.path === "/") {
          item.path = "/" + item.path;
        if (router.path === '/') {
          item.path = '/' + item.path;
        } else {
          if(!isHttp(item.path)) {
            item.path = router.path + "/" + item.path;
          if (!isHttp(item.path)) {
            item.path = router.path + '/' + item.path;
          }
        }
        item.parentPath = router.path;
      }
      childrenMenus.push(item);
    })
  })
    });
  });
  return constantRoutes.concat(childrenMenus);
})
});
// é»˜è®¤æ¿€æ´»çš„菜单
const activeMenu = computed(() => {
  const path = route.path;
  let path = route.path;
  if (path === '/index') {
    path = '/system/user';
  }
  let activePath = path;
  if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
  if (path !== undefined && path.lastIndexOf('/') > 0 && hideList.indexOf(path) === -1) {
    const tmpPath = path.substring(1, path.length);
    activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
    activePath = '/' + tmpPath.substring(0, tmpPath.indexOf('/'));
    if (!route.meta.link) {
        appStore.toggleSideBarHide(false);
      appStore.toggleSideBarHide(false);
    }
  } else if(!route.children) {
  } else if (!route.children) {
    activePath = path;
    appStore.toggleSideBarHide(true);
  }
  activeRoutes(activePath);
  return activePath;
})
});
const setVisibleNumber = () => {
  const width = document.body.getBoundingClientRect().width / 3;
  visibleNumber.value = parseInt(String(width / 85));
}
};
const handleSelect = (key: string) => {
  currentIndex.value = key;
  const route = routers.value.find(item => item.path === key);
  const route = routers.value.find((item) => item.path === key);
  if (isHttp(key)) {
    // http(s):// è·¯å¾„新窗口打开
    window.open(key, "_blank");
    window.open(key, '_blank');
  } else if (!route || !route.children) {
    // æ²¡æœ‰å­è·¯ç”±è·¯å¾„内部打开
    const routeMenu = childrenMenus.value.find(item => item.path === key);
    const routeMenu = childrenMenus.value.find((item) => item.path === key);
    if (routeMenu && routeMenu.query) {
      let query = JSON.parse(routeMenu.query);
      router.push({ path: key, query: query });
@@ -127,35 +129,35 @@
    activeRoutes(key);
    appStore.toggleSideBarHide(false);
  }
}
};
const activeRoutes = (key: string) => {
  let routes:RouteOption[] = [];
  let routes: RouteRecordRaw[] = [];
  if (childrenMenus.value && childrenMenus.value.length > 0) {
    childrenMenus.value.map((item) => {
      if (key == item.parentPath || (key == "index" && "" == item.path)) {
      if (key == item.parentPath || (key == 'index' && '' == item.path)) {
        routes.push(item);
      }
    });
  }
  if(routes.length > 0) {
  if (routes.length > 0) {
    permissionStore.setSidebarRouters(routes);
  } else {
    appStore.toggleSideBarHide(true);
  }
  return routes;
}
};
onMounted(() => {
  window.addEventListener('resize', setVisibleNumber)
})
  window.addEventListener('resize', setVisibleNumber);
});
onBeforeUnmount(() => {
  window.removeEventListener('resize', setVisibleNumber)
})
  window.removeEventListener('resize', setVisibleNumber);
});
onMounted(() => {
  setVisibleNumber()
})
  setVisibleNumber();
});
</script>
<style lang="scss">
@@ -168,7 +170,8 @@
  margin: 0 10px !important;
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active,
.el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
  border-bottom: 2px solid #{'var(--theme)'} !important;
  color: #303133;
}
@@ -184,7 +187,9 @@
}
/* èƒŒæ™¯è‰²éšè— */
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
.topmenu-container.el-menu--horizontal > .el-menu-item:not(.is-disabled):focus,
.topmenu-container.el-menu--horizontal > .el-menu-item:not(.is-disabled):hover,
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title:hover {
  background-color: #ffffff !important;
}
src/components/TreeSelect/index.vue
@@ -1,14 +1,14 @@
<template>
  <div class="el-tree-select">
    <el-select
      style="width: 100%"
      v-model="valueId"
      ref="treeSelect"
      v-model="valueId"
      style="width: 100%"
      :filterable="true"
      :clearable="true"
      @clear="clearHandle"
      :filter-method="selectFilterData"
      :placeholder="placeholder"
      @clear="clearHandle"
    >
      <el-option :value="valueId" :label="valueTitle">
        <el-tree
@@ -29,43 +29,32 @@
</template>
<script setup lang="ts">
interface ObjMap {
  value: string;
  label: string;
  children: string;
}
interface Props {
  objMap: ObjMap;
  accordion: boolean;
  value: string | number;
  options: any[];
  placeholder: string;
}
const props = defineProps({
  /* é…ç½®é¡¹ */
  objMap: {
    type: Object,
    default: () => {
      return {
        value: 'id', // ID字段名
        label: 'label', // æ˜¾ç¤ºåç§°
        children: 'children' // å­çº§å­—段名
      }
    }
const props = withDefaults(defineProps<Props>(), {
  objMap: () => {
    return {
      value: 'id',
      label: 'label',
      children: 'children'
    };
  },
  /* è‡ªåŠ¨æ”¶èµ· */
  accordion: {
    type: Boolean,
    default: () => {
      return false
    }
  },
  /**当前双向数据绑定的值 */
  value: {
    type: [String, Number],
    default: ''
  },
  /**当前的数据 */
  options: {
    type: Array,
    default: () => []
  },
  /**输入框内部的文字 */
  placeholder: {
    type: String,
    default: ''
  }
})
  accordion: false,
  value: '',
  options: () => [],
  placeholder: ''
});
const selectTree = ref<ElTreeSelectInstance>();
@@ -74,7 +63,7 @@
const valueId = computed({
  get: () => props.value,
  set: (val) => {
    emit('update:value', val)
    emit('update:value', val);
  }
});
const valueTitle = ref('');
@@ -83,54 +72,54 @@
const initHandle = () => {
  nextTick(() => {
    const selectedValue = valueId.value;
    if (selectedValue !== null && typeof (selectedValue) !== 'undefined') {
      const node = selectTree.value?.getNode(selectedValue)
    if (selectedValue !== null && typeof selectedValue !== 'undefined') {
      const node = selectTree.value?.getNode(selectedValue);
      if (node) {
        valueTitle.value = node.data[props.objMap.label]
        selectTree.value?.setCurrentKey(selectedValue) // è®¾ç½®é»˜è®¤é€‰ä¸­
        defaultExpandedKey.value = [selectedValue] // è®¾ç½®é»˜è®¤å±•å¼€
        valueTitle.value = node.data[props.objMap.label];
        selectTree.value?.setCurrentKey(selectedValue); // è®¾ç½®é»˜è®¤é€‰ä¸­
        defaultExpandedKey.value = [selectedValue]; // è®¾ç½®é»˜è®¤å±•å¼€
      }
    } else {
      clearHandle()
      clearHandle();
    }
  })
}
  });
};
const handleNodeClick = (node: any) => {
  valueTitle.value = node[props.objMap.label]
  valueTitle.value = node[props.objMap.label];
  valueId.value = node[props.objMap.value];
  defaultExpandedKey.value = [];
  selectTree.value?.blur()
  selectFilterData('')
}
  selectTree.value?.blur();
  selectFilterData('');
};
const selectFilterData = (val: any) => {
  selectTree.value?.filter(val)
}
  selectTree.value?.filter(val);
};
const filterNode = (value: any, data: any) => {
  if (!value) return true
  return data[props.objMap['label']].indexOf(value) !== -1
}
  if (!value) return true;
  return data[props.objMap['label']].indexOf(value) !== -1;
};
const clearHandle = () => {
  valueTitle.value = ''
  valueId.value = ''
  valueTitle.value = '';
  valueId.value = '';
  defaultExpandedKey.value = [];
  clearSelected()
}
  clearSelected();
};
const clearSelected = () => {
  const allNode = document.querySelectorAll('#tree-option .el-tree-node')
  allNode.forEach((element) => element.classList.remove('is-current'))
}
  const allNode = document.querySelectorAll('#tree-option .el-tree-node');
  allNode.forEach((element) => element.classList.remove('is-current'));
};
onMounted(() => {
  initHandle()
})
  initHandle();
});
watch(valueId, () => {
  initHandle();
})
});
</script>
<style lang="scss" scoped>
@import "@/assets/styles/variables.module.scss";
@import '@/assets/styles/variables.module.scss';
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
  padding: 0;
src/components/UserSelect/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,314 @@
<template>
  <div>
    <el-dialog v-model="userDialog.visible.value" :title="userDialog.title.value" width="80%" append-to-body>
      <el-row :gutter="20">
        <!-- éƒ¨é—¨æ ‘ -->
        <el-col :lg="4" :xs="24" style="">
          <el-card shadow="hover">
            <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
            <el-tree
              ref="deptTreeRef"
              class="mt-2"
              node-key="id"
              :data="deptOptions"
              :props="{ label: 'label', children: 'children' }"
              :expand-on-click-node="false"
              :filter-node-method="filterNode"
              highlight-current
              default-expand-all
              @node-click="handleNodeClick"
            />
          </el-card>
        </el-col>
        <el-col :lg="20" :xs="24">
          <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div v-show="showSearch" class="mb-[10px]">
              <el-card shadow="hover">
                <el-form ref="queryFormRef" :model="queryParams" :inline="true">
                  <el-form-item label="用户名称" prop="userName">
                    <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
                  </el-form-item>
                  <el-form-item label="手机号码" prop="phonenumber">
                    <el-input
                      v-model="queryParams.phonenumber"
                      placeholder="请输入手机号码"
                      clearable
                      @keyup.enter="handleQuery"
                    />
                  </el-form-item>
                  <el-form-item>
                    <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                    <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                  </el-form-item>
                </el-form>
              </el-card>
            </div>
          </transition>
          <el-card shadow="hover">
            <template v-if="prop.multiple" #header>
              <el-tag v-for="user in selectUserList" :key="user.userId" closable style="margin: 2px" @close="handleCloseTag(user)">
                {{ user.userName }}
              </el-tag>
            </template>
            <vxe-table
              ref="tableRef"
              height="400px"
              border
              show-overflow
              :data="userList"
              :loading="loading"
              :row-config="{ keyField: 'userId', isHover: true }"
              :checkbox-config="{ reserve: true, trigger: 'row', highlight: true, showHeader: prop.multiple }"
              @checkbox-all="handleCheckboxAll"
              @checkbox-change="handleCheckboxChange"
            >
              <vxe-column type="checkbox" width="50" align="center" />
              <vxe-column key="userId" title="用户编号" align="center" field="userId" />
              <vxe-column key="userName" title="用户名称" align="center" field="userName" />
              <vxe-column key="nickName" title="用户昵称" align="center" field="nickName" />
              <vxe-column key="deptName" title="部门" align="center" field="deptName" />
              <vxe-column key="phonenumber" title="手机号码" align="center" field="phonenumber" width="120" />
              <vxe-column key="status" title="状态" align="center">
                <template #default="scope">
                  <dict-tag :options="sys_normal_disable" :value="scope.row.status"></dict-tag>
                </template>
              </vxe-column>
              <vxe-column title="创建时间" align="center" width="160">
                <template #default="scope">
                  <span>{{ scope.row.createTime }}</span>
                </template>
              </vxe-column>
            </vxe-table>
            <pagination
              v-show="total > 0"
              v-model:page="queryParams.pageNum"
              v-model:limit="queryParams.pageSize"
              :total="total"
              @pagination="pageList"
            />
          </el-card>
        </el-col>
      </el-row>
      <template #footer>
        <el-button @click="close">取消</el-button>
        <el-button type="primary" @click="confirm">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import api from '@/api/system/user';
import { UserQuery, UserVO } from '@/api/system/user/types';
import { DeptVO } from '@/api/system/dept/types';
import { VxeTableInstance } from 'vxe-table';
import useDialog from '@/hooks/useDialog';
interface PropType {
  modelValue?: UserVO[] | UserVO | undefined;
  multiple?: boolean;
  data?: string | number | (string | number)[];
}
const prop = withDefaults(defineProps<PropType>(), {
  multiple: true,
  modelValue: undefined,
  data: undefined
});
const emit = defineEmits(['update:modelValue', 'confirmCallBack']);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const userList = ref<UserVO[]>();
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const deptName = ref('');
const deptOptions = ref<DeptVO[]>([]);
const selectUserList = ref<UserVO[]>([]);
const deptTreeRef = ref<ElTreeInstance>();
const queryFormRef = ref<ElFormInstance>();
const tableRef = ref<VxeTableInstance<UserVO>>();
const userDialog = useDialog({
  title: '用户选择'
});
const queryParams = ref<UserQuery>({
  pageNum: 1,
  pageSize: 10,
  userName: '',
  phonenumber: '',
  status: '',
  deptId: '',
  roleId: ''
});
const defaultSelectUserIds = computed(() => computedIds(prop.data));
/** æ ¹æ®åç§°ç­›é€‰éƒ¨é—¨æ ‘ */
watchEffect(
  () => {
    deptTreeRef.value?.filter(deptName.value);
  },
  {
    flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
  }
);
const confirm = () => {
  emit('update:modelValue', selectUserList.value);
  emit('confirmCallBack', selectUserList.value);
  userDialog.closeDialog();
};
const computedIds = (data) => {
  if (data instanceof Array) {
    return [...data];
  } else if (typeof data === 'string') {
    return data.split(',');
  } else if (typeof data === 'number') {
    return [data];
  } else {
    console.warn('<UserSelect> The data type of data should be array or string or number, but I received other');
    return [];
  }
};
/** é€šè¿‡æ¡ä»¶è¿‡æ»¤èŠ‚ç‚¹  */
const filterNode = (value: string, data: any) => {
  if (!value) return true;
  return data.label.indexOf(value) !== -1;
};
/** æŸ¥è¯¢éƒ¨é—¨ä¸‹æ‹‰æ ‘结构 */
const getTreeSelect = async () => {
  const res = await api.deptTreeSelect();
  deptOptions.value = res.data;
};
/** æŸ¥è¯¢ç”¨æˆ·åˆ—表 */
const getList = async () => {
  loading.value = true;
  const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
  loading.value = false;
  userList.value = res.rows;
  total.value = res.total;
};
const pageList = async () => {
  await getList();
  const users = userList.value.filter((item) => {
    return selectUserList.value.some((user) => user.userId === item.userId);
  });
  await tableRef.value.setCheckboxRow(users, true);
};
/** èŠ‚ç‚¹å•å‡»äº‹ä»¶ */
const handleNodeClick = (data: DeptVO) => {
  queryParams.value.deptId = data.id;
  handleQuery();
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value?.resetFields();
  queryParams.value.pageNum = 1;
  queryParams.value.deptId = undefined;
  deptTreeRef.value?.setCurrentKey(undefined);
  handleQuery();
};
const handleCheckboxChange = (checked) => {
  if (!prop.multiple && checked.checked) {
    tableRef.value.setCheckboxRow(selectUserList.value, false);
    selectUserList.value = [];
  }
  const row = checked.row;
  if (checked.checked) {
    selectUserList.value.push(row);
  } else {
    selectUserList.value = selectUserList.value.filter((item) => {
      return item.userId !== row.userId;
    });
  }
};
const handleCheckboxAll = (checked) => {
  const rows = userList.value;
  if (checked.checked) {
    rows.forEach((row) => {
      if (!selectUserList.value.some((item) => item.userId === row.userId)) {
        selectUserList.value.push(row);
      }
    });
  } else {
    selectUserList.value = selectUserList.value.filter((item) => {
      return !rows.some((row) => row.userId === item.userId);
    });
  }
};
const handleCloseTag = (user: UserVO) => {
  const userId = user.userId;
  // ä½¿ç”¨split删除用户
  const index = selectUserList.value.findIndex((item) => item.userId === userId);
  const rows = selectUserList.value[index];
  tableRef.value?.setCheckboxRow(rows, false);
  selectUserList.value.splice(index, 1);
};
const initSelectUser = async () => {
  if (defaultSelectUserIds.value.length > 0) {
    const { data } = await api.optionSelect(defaultSelectUserIds.value);
    selectUserList.value = data;
    const users = userList.value.filter((item) => {
      return defaultSelectUserIds.value.includes(String(item.userId));
    });
    await nextTick(() => {
      tableRef.value.setCheckboxRow(users, true);
    });
  }
};
const close = () => {
  userDialog.closeDialog();
};
watch(
  () => userDialog.visible.value,
  (newValue: boolean) => {
    if (newValue) {
      initSelectUser();
    } else {
      tableRef.value.clearCheckboxReserve();
      tableRef.value.clearCheckboxRow();
      resetQuery();
      selectUserList.value = [];
    }
  }
);
onMounted(() => {
  getTreeSelect(); // åˆå§‹åŒ–部门数据
  getList(); // åˆå§‹åŒ–列表数据
});
defineExpose({
  open: userDialog.openDialog,
  close: userDialog.closeDialog
});
</script>
<style lang="scss" scoped></style>
src/components/iFrame/index.vue
@@ -9,18 +9,18 @@
const props = defineProps({
  src: propTypes.string.isRequired
})
});
const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
const loading = ref(true)
const url = computed(() => props.src)
const height = ref(document.documentElement.clientHeight - 94.5 + 'px;');
const loading = ref(true);
const url = computed(() => props.src);
onMounted(() => {
  setTimeout(() => {
    loading.value = false;
  }, 300);
  window.onresize = function temp() {
    height.value = document.documentElement.clientHeight - 94.5 + "px;";
    height.value = document.documentElement.clientHeight - 94.5 + 'px;';
  };
})
});
</script>
src/directive/common/copyText.ts
@@ -2,9 +2,10 @@
 * v-copyText å¤åˆ¶æ–‡æœ¬å†…容
 * Copyright (c) 2022 ruoyi
 */
import { DirectiveBinding } from 'vue';
export default {
  beforeMount(el: any, { value, arg }: any) {
  beforeMount(el: any, { value, arg }: DirectiveBinding) {
    if (arg === 'callback') {
      el.$copyCallback = value;
    } else {
src/directive/permission/index.ts
@@ -9,7 +9,7 @@
    // ã€Œå…¶ä»–角色」按钮权限校验
    const { value } = binding;
    if (value && value instanceof Array && value.length > 0) {
      const hasPermission = permissions.some((permi) => {
      const hasPermission = permissions.some((permi: string) => {
        return permi === '*:*:*' || value.includes(permi);
      });
      if (!hasPermission) {
@@ -30,7 +30,7 @@
    const { value } = binding;
    const { roles } = useUserStore();
    if (value && value instanceof Array && value.length > 0) {
      const hasRole = roles.some((role) => {
      const hasRole = roles.some((role: string) => {
        return role === 'admin' || value.includes(role);
      });
      if (!hasRole) {
src/enums/LanguageEnum.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
export enum LanguageEnum {
  zh_CN = 'zh_CN',
  en_US = 'en_US'
}
src/enums/bpmn/IndexEnums.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
export enum AllocationTypeEnum {
  USER = 'user',
  CANDIDATE = 'candidate',
  YOURSELF = 'yourself',
  SPECIFY = 'specify'
}
export enum SpecifyDescEnum {
  SPECIFY_MULTIPLE = 'specifyMultiple',
  SPECIFY_SINGLE = 'specifySingle'
}
export enum MultiInstanceTypeEnum {
  SERIAL = 'serial',
  PARALLEL = 'parallel',
  NONE = 'none'
}
src/hooks/useDialog.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
import { Ref } from 'vue';
interface Options {
  title?: string;
}
interface Return {
  title: Ref<string>;
  visible: Ref<boolean>;
  openDialog: () => void;
  closeDialog: () => void;
}
export default (ops?: Options): Return => {
  const visible = ref(false);
  const title = ref(ops.title || '');
  const openDialog = () => {
    visible.value = true;
  };
  const closeDialog = () => {
    visible.value = false;
  };
  return {
    title,
    visible,
    openDialog,
    closeDialog
  };
};
src/lang/en_US.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
{
  "route": {
    "dashboard": "Dashboard",
    "document": "Document"
  },
  "login": {
    "username": "Username",
    "password": "Password",
    "login": "Login",
    "code": "Verification Code",
    "copyright": ""
  },
  "navbar": {
    "full": "Full Screen",
    "language": "Language",
    "dashboard": "Dashboard",
    "document": "Document",
    "message": "Message",
    "layoutSize": "Layout Size",
    "selectTenant": "Select Tenant",
    "layoutSetting": "Layout Setting",
    "personalCenter": "Personal Center",
    "logout": "Logout"
  }
}
src/lang/index.ts
@@ -1,32 +1,24 @@
// è‡ªå®šä¹‰å›½é™…化配置
import { createI18n } from 'vue-i18n';
// æœ¬åœ°è¯­è¨€åŒ…
import enUSLocale from './en_US';
import zhCNLocale from './zh_CN';
const messages = {
  zh_CN: {
    ...zhCNLocale
  },
  en_US: {
    ...enUSLocale
  }
};
import { LanguageEnum } from '@/enums/LanguageEnum';
import messages from '@intlify/unplugin-vue-i18n/messages';
/**
 * èŽ·å–å½“å‰è¯­è¨€
 * @returns zh-cn|en ...
 */
export const getLanguage = () => {
  const language = useStorage('language', 'zh_CN');
export const getLanguage = (): LanguageEnum => {
  const language = useStorage<LanguageEnum>('language', LanguageEnum.zh_CN);
  if (language.value) {
    return language.value;
  }
  return 'zh_CN';
  return LanguageEnum.zh_CN;
};
const i18n = createI18n({
  globalInjection: true,
  allowComposition: true,
  legacy: false,
  locale: getLanguage(),
  messages
src/lang/zh_CN.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
{
  "route": {
    "dashboard": "首页",
    "document": "项目文档"
  },
  "login": {
    "username": "用户名",
    "password": "密码",
    "login": "登 å½•",
    "code": "请输入验证码",
    "copyright": ""
  },
  "navbar": {
    "full": "全屏",
    "language": "语言",
    "dashboard": "首页",
    "document": "项目文档",
    "message": "消息",
    "layoutSize": "布局大小",
    "selectTenant": "选择租户",
    "layoutSetting": "布局设置",
    "personalCenter": "个人中心",
    "logout": "退出登录"
  }
}
src/layout/components/AppMain.vue
@@ -2,9 +2,12 @@
  <section class="app-main">
    <router-view v-slot="{ Component, route }">
      <transition :enter-active-class="animante" mode="out-in">
        <keep-alive :include="tagsViewStore.cachedViews">
          <component v-if="!route.meta.link" :is="Component" :key="route.path" />
        </keep-alive>
        <div>
          <keep-alive :include="tagsViewStore.cachedViews" v-if="!route.meta.noCache">
            <component v-if="!route.meta.link" :is="Component" :key="route.path" />
          </keep-alive>
          <component v-if="!route.meta.link && route.meta.noCache" :is="Component" :key="route.path" />
        </div>
      </transition>
    </router-view>
    <iframe-toggle />
@@ -12,24 +15,28 @@
</template>
<script setup name="AppMain" lang="ts">
import useTagsViewStore from '@/store/modules/tagsView';
import useSettingsStore from '@/store/modules/settings';
import IframeToggle  from './IframeToggle/index.vue'
import { ComponentInternalInstance } from "vue";
import useTagsViewStore from '@/store/modules/tagsView';
import IframeToggle from './IframeToggle/index.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const tagsViewStore = useTagsViewStore();
// éšæœºåŠ¨ç”»é›†åˆ
const animante = ref<string>('');
const animationEnable = ref(useSettingsStore().animationEnable);
watch(()=> useSettingsStore().animationEnable, (val) => {
watch(
  () => useSettingsStore().animationEnable,
  (val: boolean) => {
    animationEnable.value = val;
    if (val) {
        animante.value = proxy?.animate.animateList[Math.round(Math.random() * proxy?.animate.animateList.length)] as string;
      animante.value = proxy?.animate.animateList[Math.round(Math.random() * proxy?.animate.animateList.length)] as string;
    } else {
        animante.value = proxy?.animate.defaultAnimate as string;
      animante.value = proxy?.animate.defaultAnimate as string;
    }
}, { immediate: true });
  },
  { immediate: true }
);
</script>
<style lang="scss" scoped>
@@ -41,7 +48,7 @@
  overflow: hidden;
}
.fixed-header+.app-main {
.fixed-header + .app-main {
  padding-top: 50px;
}
@@ -51,7 +58,7 @@
    min-height: calc(100vh - 84px);
  }
  .fixed-header+.app-main {
  .fixed-header + .app-main {
    padding-top: 84px;
  }
}
src/layout/components/IframeToggle/index.vue
@@ -1,26 +1,27 @@
<template>
  <transition-group name="fade-transform" mode="out-in">
    <inner-link
      v-for="(item, index) in tagsViewStore.iframeViews"
      :key="item.path"
      :iframeId="'iframe' + index"
      v-show="route.path === item.path"
      :src="iframeUrl(item.meta ? item.meta.link : '', item.query)"
    ></inner-link>
  </transition-group>
  <inner-link
    v-for="(item, index) in tagsViewStore.iframeViews"
    v-show="route.path === item.path"
    :key="item.path"
    :iframe-id="'iframe' + index"
    :src="iframeUrl(item.meta ? item.meta.link : '', item.query)"
  ></inner-link>
</template>
<script setup lang="ts">
import InnerLink from "../InnerLink/index.vue";
import InnerLink from '../InnerLink/index.vue';
import useTagsViewStore from '@/store/modules/tagsView';
const route = useRoute();
const tagsViewStore = useTagsViewStore();
function iframeUrl(url: string, query: any) {
function iframeUrl(url: string | undefined, query: any) {
  if (Object.keys(query).length > 0) {
    let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
    return url + "?" + params;
    let params = Object.keys(query)
      .map((key) => key + '=' + query[key])
      .join('&');
    return url + '?' + params;
  }
  return url;
}
src/layout/components/InnerLink/index.vue
@@ -1,18 +1,15 @@
<template>
  <div :style="'height:' + height">
    <iframe :id="iframeId" style="width: 100%; height: 100%" :src="src" frameborder="no"></iframe>
    <iframe :id="iframeId" style="width: 100%; height: 100%; border: 0" :src="src"></iframe>
  </div>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
    src: {
        type: String,
        default: "/"
    },
    iframeId: {
        type: String
    }
  src: propTypes.string.def('/'),
  iframeId: propTypes.string.isRequired
});
const height = ref(document.documentElement.clientHeight - 94.5 + "px");
const height = ref(document.documentElement.clientHeight - 94.5 + 'px');
</script>
src/layout/components/Navbar.vue
@@ -1,18 +1,19 @@
<template>
  <div class="navbar">
    <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" />
    <top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
    <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggle-click="toggleSideBar" />
    <breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
    <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
    <div class="right-menu flex align-center">
      <template v-if="appStore.device !== 'mobile'">
        <el-select
          v-if="userId === 1 && tenantEnabled"
          v-model="companyName"
          class="min-w-244px"
          clearable
          filterable
          reserve-keyword
          :placeholder="$t('navbar.selectTenant')"
          v-if="userId === 1 && tenantEnabled"
          @change="dynamicTenantEvent"
          @clear="dynamicClearEvent"
        >
@@ -63,17 +64,17 @@
        </el-tooltip>
      </template>
      <div class="avatar-container">
        <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
        <el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
          <div class="avatar-wrapper">
            <img :src="userStore.avatar" class="user-avatar" />
            <el-icon><caret-bottom /></el-icon>
          </div>
          <template #dropdown>
            <el-dropdown-menu>
              <router-link to="/user/profile" v-if="!dynamic">
              <router-link v-if="!dynamic" to="/user/profile">
                <el-dropdown-item>{{ $t('navbar.personalCenter') }}</el-dropdown-item>
              </router-link>
              <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
              <el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">
                <span>{{ $t('navbar.layoutSetting') }}</span>
              </el-dropdown-item>
              <el-dropdown-item divided command="logout">
@@ -92,12 +93,11 @@
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import useSettingsStore from '@/store/modules/settings';
import { getTenantList } from "@/api/login";
import { dynamicClear, dynamicTenant } from "@/api/system/tenant";
import { ComponentInternalInstance } from "vue";
import { TenantVO } from "@/api/types";
import notice from './notice/index.vue';
import useNoticeStore from '@/store/modules/notice';
import { getTenantList } from '@/api/login';
import { dynamicClear, dynamicTenant } from '@/api/system/tenant';
import { TenantVO } from '@/api/types';
import notice from './notice/index.vue';
const appStore = useAppStore();
const userStore = useUserStore();
@@ -119,7 +119,7 @@
const openSearchMenu = () => {
  searchMenuRef.value?.openSearch();
}
};
// åŠ¨æ€åˆ‡æ¢
const dynamicTenantEvent = async (tenantId: string) => {
@@ -129,14 +129,14 @@
    proxy?.$tab.closeAllPage();
    proxy?.$router.push('/');
  }
}
};
const dynamicClearEvent = async () => {
  await dynamicClear();
  dynamic.value = false;
  proxy?.$tab.closeAllPage();
  proxy?.$router.push('/');
}
};
/** ç§Ÿæˆ·åˆ—表 */
const initTenantList = async () => {
@@ -145,56 +145,58 @@
  if (tenantEnabled.value) {
    tenantList.value = data.voList;
  }
}
};
defineExpose({
  initTenantList,
})
  initTenantList
});
const toggleSideBar = () => {
  appStore.toggleSideBar(false);
}
};
const logout = async () => {
    await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
    await userStore.logout()
    location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
}
  await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  });
  await userStore.logout();
  location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
};
const emits = defineEmits(['setLayout'])
const emits = defineEmits(['setLayout']);
const setLayout = () => {
    emits('setLayout');
}
  emits('setLayout');
};
// å®šä¹‰Command方法对象 é€šè¿‡key直接调用方法
const commandMap: {[key: string]: any} = {
    setLayout,
    logout
const commandMap: { [key: string]: any } = {
  setLayout,
  logout
};
const handleCommand = (command: string) => {
    // åˆ¤æ–­æ˜¯å¦å­˜åœ¨è¯¥æ–¹æ³•
    if (commandMap[command]) {
        commandMap[command]();
    }
}
  // åˆ¤æ–­æ˜¯å¦å­˜åœ¨è¯¥æ–¹æ³•
  if (commandMap[command]) {
    commandMap[command]();
  }
};
//用深度监听 æ¶ˆæ¯
watch(() => noticeStore.state.value.notices, (newVal, oldVal) => {
  newNotice.value = newVal.filter((item: any) => !item.read).length;
}, { deep: true });
watch(
  () => noticeStore.state.value.notices,
  (newVal) => {
    newNotice.value = newVal.filter((item: any) => !item.read).length;
  },
  { deep: true }
);
</script>
<style lang="scss" scoped>
:deep(.el-select .el-input__wrapper) {
  height:30px;
  height: 30px;
}
:deep(.el-badge__content.is-fixed){
    top: 12px;
:deep(.el-badge__content.is-fixed) {
  top: 12px;
}
.flex {
src/layout/components/Settings/index.vue
@@ -1,11 +1,11 @@
<template>
  <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px" close-on-click-modal>
  <el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal>
    <h3 class="drawer-title">主题风格设置</h3>
    <div class="setting-drawer-block-checbox">
      <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.DARK)">
        <img src="@/assets/images/dark.svg" alt="dark" />
        <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
        <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
          <i aria-label="图标: check" class="anticon anticon-check">
            <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
              <path
@@ -17,7 +17,7 @@
      </div>
      <div class="setting-drawer-block-checbox-item" @click="handleTheme(SideThemeEnum.LIGHT)">
        <img src="@/assets/images/light.svg" alt="light" />
        <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
        <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
          <i aria-label="图标: check" class="anticon anticon-check">
            <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
              <path
@@ -37,7 +37,7 @@
    <div class="drawer-item">
      <span>深色模式</span>
      <span class="comp-style">
        <el-switch v-model="isDark" @change="toggleDark" class="drawer-switch" />
        <el-switch v-model="isDark" class="drawer-switch" @change="toggleDark" />
      </span>
    </div>
@@ -48,35 +48,35 @@
    <div class="drawer-item">
      <span>开启 TopNav</span>
      <span class="comp-style">
        <el-switch v-model="topNav" class="drawer-switch" />
        <el-switch v-model="settingsStore.topNav" class="drawer-switch" @change="topNavChange" />
      </span>
    </div>
    <div class="drawer-item">
      <span>开启 Tags-Views</span>
      <span class="comp-style">
        <el-switch v-model="tagsView" class="drawer-switch" />
        <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>固定 Header</span>
      <span class="comp-style">
        <el-switch v-model="fixedHeader" class="drawer-switch" />
        <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>显示 Logo</span>
      <span class="comp-style">
        <el-switch v-model="sidebarLogo" class="drawer-switch" />
        <el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>动态标题</span>
      <span class="comp-style">
        <el-switch v-model="dynamicTitle" class="drawer-switch" />
        <el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" @change="dynamicTitleChange" />
      </span>
    </div>
@@ -88,126 +88,92 @@
</template>
<script setup lang="ts">
import { useDynamicTitle } from '@/utils/dynamicTitle'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { handleThemeStyle } from '@/utils/theme'
import { ComponentInternalInstance } from "vue";
import { SettingTypeEnum } from "@/enums/SettingTypeEnum";
import { SideThemeEnum } from "@/enums/SideThemeEnum";
import { useDynamicTitle } from '@/utils/dynamicTitle';
import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
import { handleThemeStyle } from '@/utils/theme';
import { SideThemeEnum } from '@/enums/SideThemeEnum';
import defaultSettings from '@/settings';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const showSettings = ref(false);
const theme = ref(settingsStore.theme);
const sideTheme = ref(settingsStore.sideTheme);
const storeSettings = computed(() => settingsStore);
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
const predefineColors = ref(['#409EFF', '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585']);
// æ˜¯å¦æš—黑模式
const isDark = useDark({
  storageKey: 'useDarkKey',
  valueDark: 'dark',
  valueLight: 'light',
  valueLight: 'light'
});
watch(isDark, ()=> {
// åŒ¹é…èœå•颜色
watch(isDark, () => {
  if (isDark.value) {
    settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: SideThemeEnum.DARK })
    settingsStore.sideTheme = SideThemeEnum.DARK;
  } else {
    settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: sideTheme.value })
    settingsStore.sideTheme = sideTheme.value;
  }
})
});
const toggleDark = () => useToggle(isDark);
/** æ˜¯å¦éœ€è¦topNav */
const topNav = computed({
    get: () => storeSettings.value.topNav,
    set: (val) => {
        settingsStore.changeSetting({ key: SettingTypeEnum.TOP_NAV, value: val })
        if (!val) {
            appStore.toggleSideBarHide(false);
            permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
        }
    }
})
/** æ˜¯å¦éœ€è¦tagview */
const tagsView = computed({
    get: () => storeSettings.value.tagsView,
    set: (val) => {
        settingsStore.changeSetting({ key: SettingTypeEnum.TAGS_VIEW, value: val })
    }
})
/**是否需要固定头部 */
const fixedHeader = computed({
    get: () => storeSettings.value.fixedHeader,
    set: (val) => {
        settingsStore.changeSetting({ key: SettingTypeEnum.FIXED_HEADER, value: val })
    }
})
/**是否需要侧边栏的logo */
const sidebarLogo = computed({
    get: () => storeSettings.value.sidebarLogo,
    set: (val) => {
        settingsStore.changeSetting({ key: SettingTypeEnum.SIDEBAR_LOGO, value: val })
    }
})
/**是否需要侧边栏的动态网页的title */
const dynamicTitle = computed({
    get: () => storeSettings.value.dynamicTitle,
    set: (val) => {
        settingsStore.changeSetting({ key: SettingTypeEnum.DYNAMIC_TITLE, value: val })
        // åŠ¨æ€è®¾ç½®ç½‘é¡µæ ‡é¢˜
        useDynamicTitle()
    }
})
const topNavChange = (val: any) => {
  if (!val) {
    appStore.toggleSideBarHide(false);
    permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
  }
};
const themeChange = (val: string | null) => {
    settingsStore.changeSetting({ key: SettingTypeEnum.THEME, value: val })
    theme.value = val;
    if (val) {
        handleThemeStyle(val);
    }
}
const dynamicTitleChange = () => {
  // åŠ¨æ€è®¾ç½®ç½‘é¡µæ ‡é¢˜
  useDynamicTitle();
};
const themeChange = (val: string) => {
  settingsStore.theme = val;
  handleThemeStyle(val);
};
const handleTheme = (val: string) => {
    sideTheme.value = val;
    if (isDark.value && val === SideThemeEnum.LIGHT) {
      // æš—黑模式颜色不变
      settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: SideThemeEnum.DARK })
      return
    }
    settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: val })
}
  sideTheme.value = val;
  if (isDark.value && val === SideThemeEnum.LIGHT) {
    // æš—黑模式颜色不变
    settingsStore.sideTheme = SideThemeEnum.DARK;
    return;
  }
  settingsStore.sideTheme = val;
};
const saveSetting = () => {
    proxy?.$modal.loading("正在保存到本地,请稍候...");
    let layoutSetting = {
        "topNav": storeSettings.value.topNav,
        "tagsView": storeSettings.value.tagsView,
        "fixedHeader": storeSettings.value.fixedHeader,
        "sidebarLogo": storeSettings.value.sidebarLogo,
        "dynamicTitle": storeSettings.value.dynamicTitle,
        "sideTheme": storeSettings.value.sideTheme,
        "theme": storeSettings.value.theme
    };
    localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
    setTimeout(() => {proxy?.$modal.closeLoading()}, 1000)
}
  proxy?.$modal.loading('正在保存到本地,请稍候...');
  const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
  settings.value.topNav = storeSettings.value.topNav;
  settings.value.tagsView = storeSettings.value.tagsView;
  settings.value.fixedHeader = storeSettings.value.fixedHeader;
  settings.value.sidebarLogo = storeSettings.value.sidebarLogo;
  settings.value.dynamicTitle = storeSettings.value.dynamicTitle;
  settings.value.sideTheme = storeSettings.value.sideTheme;
  settings.value.theme = storeSettings.value.theme;
  setTimeout(() => {
    proxy?.$modal.closeLoading();
  }, 1000);
};
const resetSetting = () => {
    proxy?.$modal.loading("正在清除设置缓存并刷新,请稍候...");
    localStorage.removeItem("layout-setting")
    setTimeout("window.location.reload()", 1000)
}
  proxy?.$modal.loading('正在清除设置缓存并刷新,请稍候...');
  useStorage<any>('layout-setting', null).value = null;
  setTimeout('window.location.reload()', 1000);
};
const openSetting = () => {
    showSettings.value = true;
}
  showSettings.value = true;
};
defineExpose({
    openSetting,
})
  openSetting
});
</script>
<style lang="scss" scoped>
src/layout/components/Sidebar/Link.vue
@@ -5,36 +5,36 @@
</template>
<script setup lang="ts">
import { isExternal } from '@/utils/validate'
import { isExternal } from '@/utils/validate';
const props = defineProps({
    to: {
        type: [String, Object],
        required: true
    }
})
  to: {
    type: [String, Object],
    required: true
  }
});
const isExt = computed(() => {
    return isExternal(props.to as string)
})
  return isExternal(props.to as string);
});
const type = computed(() => {
    if (isExt.value) {
        return 'a'
    }
    return 'router-link'
})
  if (isExt.value) {
    return 'a';
  }
  return 'router-link';
});
function linkProps() {
    if (isExt.value) {
        return {
            href: props.to,
            target: '_blank',
            rel: 'noopener'
        }
    }
  if (isExt.value) {
    return {
        to: props.to
    }
      href: props.to,
      target: '_blank',
      rel: 'noopener'
    };
  }
  return {
    to: props.to
  };
}
</script>
src/layout/components/Sidebar/Logo.vue
@@ -1,7 +1,7 @@
<template>
  <div
    class="sidebar-logo-container"
    :class="{ 'collapse': collapse }"
    :class="{ collapse: collapse }"
    :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
  >
    <transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
@@ -22,18 +22,17 @@
</template>
<script setup lang="ts">
import variables from '@/assets/styles/variables.module.scss'
import logo from '@/assets/logo/logo.png'
import useSettingsStore from '@/store/modules/settings'
import { ComponentInternalInstance } from "vue";
import variables from '@/assets/styles/variables.module.scss';
import logo from '@/assets/logo/logo.png';
import useSettingsStore from '@/store/modules/settings';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
defineProps({
    collapse: {
        type: Boolean,
        required: true
    }
})
  collapse: {
    type: Boolean,
    required: true
  }
});
const title = ref('RuoYi-Vue-Plus');
const settingsStore = useSettingsStore();
@@ -77,7 +76,12 @@
      font-weight: 600;
      line-height: 50px;
      font-size: 14px;
      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
      font-family:
        Avenir,
        Helvetica Neue,
        Arial,
        Helvetica,
        sans-serif;
      vertical-align: middle;
    }
  }
src/layout/components/Sidebar/SidebarItem.vue
@@ -13,7 +13,7 @@
    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
      <template v-if="item.meta" #title>
        <svg-icon :icon-class="item.meta ? item.meta.icon : '' " />
        <svg-icon :icon-class="item.meta ? item.meta.icon : ''" />
        <span class="menu-title" :title="hasTitle(item.meta?.title)">{{ item.meta?.title }}</span>
      </template>
@@ -30,79 +30,74 @@
</template>
<script setup lang="ts">
import { isExternal } from '@/utils/validate'
import AppLink from './Link.vue'
import { getNormalPath } from '@/utils/ruoyi'
import { RouteOption } from "vue-router";
import { isExternal } from '@/utils/validate';
import AppLink from './Link.vue';
import { getNormalPath } from '@/utils/ruoyi';
import { RouteRecordRaw } from 'vue-router';
const props = defineProps({
    // route object
    item: {
        type: Object as PropType<RouteOption>,
        required: true
    },
    isNest: {
        type: Boolean,
        default: false
    },
    basePath: {
        type: String,
        default: ''
    }
})
  item: {
    type: Object as PropType<RouteRecordRaw>,
    required: true
  },
  isNest: {
    type: Boolean,
    default: false
  },
  basePath: {
    type: String,
    default: ''
  }
});
const onlyOneChild = ref<any>({});
const hasOneShowingChild = (parent: RouteOption, children?:RouteOption[]) => {
    if (!children) {
        children = [];
const hasOneShowingChild = (parent: RouteRecordRaw, children?: RouteRecordRaw[]) => {
  if (!children) {
    children = [];
  }
  const showingChildren = children.filter((item) => {
    if (item.hidden) {
      return false;
    } else {
      // Temp set(will be used if only has one showing child)
      onlyOneChild.value = item;
      return true;
    }
    const showingChildren = children.filter(item => {
        if (item.hidden) {
            return false
        } else {
            // Temp set(will be used if only has one showing child)
            onlyOneChild.value = item
            return true
        }
    })
  });
    // When there is only one child router, the child router is displayed by default
    if (showingChildren.length === 1) {
        return true
    }
  // When there is only one child router, the child router is displayed by default
  if (showingChildren.length === 1) {
    return true;
  }
    // Show parent if there are no child router to display
    if (showingChildren.length === 0) {
        onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
        if (parent.name === '2222') {
          console.log(onlyOneChild.value)
        }
        return true
    }
  // Show parent if there are no child router to display
  if (showingChildren.length === 0) {
    onlyOneChild.value = { ...parent, path: '', noShowingChildren: true };
    return true;
  }
    return false
  return false;
};
const resolvePath = (routePath:string, routeQuery?:string): any => {
    if (isExternal(routePath)) {
        return routePath
    }
    if (isExternal(props.basePath)) {
        return props.basePath
    }
    if (routeQuery) {
        let query = JSON.parse(routeQuery);
        return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
    }
    return getNormalPath(props.basePath + '/' + routePath)
}
const resolvePath = (routePath: string, routeQuery?: string): any => {
  if (isExternal(routePath)) {
    return routePath;
  }
  if (isExternal(props.basePath as string)) {
    return props.basePath;
  }
  if (routeQuery) {
    let query = JSON.parse(routeQuery);
    return { path: getNormalPath(props.basePath + '/' + routePath), query: query };
  }
  return getNormalPath(props.basePath + '/' + routePath);
};
const hasTitle = (title: string | undefined): string => {
    if(!title || title.length <= 5) {
        return "";
    }
    return title;
}
  if (!title || title.length <= 5) {
    return '';
  }
  return title;
};
</script>
src/layout/components/Sidebar/index.vue
@@ -13,7 +13,7 @@
          :collapse-transition="false"
          mode="vertical"
        >
          <sidebar-item v-for="(route, index) in sidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" />
          <sidebar-item v-for="(r, index) in sidebarRouters" :key="r.path + index" :item="r" :base-path="r.path" />
        </el-menu>
      </transition>
    </el-scrollbar>
@@ -21,35 +21,35 @@
</template>
<script setup lang="ts">
import Logo from './Logo.vue'
import SidebarItem from './SidebarItem.vue'
import variables from '@/assets/styles/variables.module.scss'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { RouteOption } from "vue-router";
import Logo from './Logo.vue';
import SidebarItem from './SidebarItem.vue';
import variables from '@/assets/styles/variables.module.scss';
import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
import { RouteRecordRaw } from 'vue-router';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const sidebarRouters =  computed<RouteOption[]>(() => permissionStore.sidebarRouters);
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const sidebarRouters = computed<RouteRecordRaw[]>(() => permissionStore.getSidebarRoutes());
const showLogo = computed(() => settingsStore.sidebarLogo);
const sideTheme = computed(() => settingsStore.sideTheme);
const theme = computed(() => settingsStore.theme);
const isCollapse = computed(() => !appStore.sidebar.opened);
const activeMenu = computed(() => {
    const { meta, path } = route;
    // if set path, the sidebar will highlight the path you set
    if (meta.activeMenu) {
        return meta.activeMenu;
    }
    return path;
})
  const { meta, path } = route;
  // if set path, the sidebar will highlight the path you set
  if (meta.activeMenu) {
    return meta.activeMenu;
  }
  return path;
});
const bgColor = computed(() => sideTheme.value === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground);
const textColor = computed(() => sideTheme.value === 'theme-dark' ? variables.menuColor : variables.menuLightColor);
const bgColor = computed(() => (sideTheme.value === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground));
const textColor = computed(() => (sideTheme.value === 'theme-dark' ? variables.menuColor : variables.menuLightColor));
</script>
src/layout/components/SocialCallback/index.vue
@@ -10,7 +10,6 @@
const route = useRoute();
const loading = ref(true);
/**
 * æŽ¥æ”¶Route传递的参数
 * @param {Object} route.query.
@@ -18,8 +17,7 @@
const code = route.query.code as string;
const state = route.query.state as string;
const source = route.query.source as string;
const tenantId = localStorage.getItem("tenantId") ? localStorage.getItem("tenantId") as string : '000000';
const tenantId = localStorage.getItem('tenantId') ? (localStorage.getItem('tenantId') as string) : '000000';
const processResponse = async (res: any) => {
  if (res.code !== 200) {
@@ -52,7 +50,6 @@
};
const loginByCode = async (data: LoginData) => {
  console.log(2)
  try {
    const res = await login(data);
    await processResponse(res);
src/layout/components/TagsView/ScrollPane.vue
@@ -5,84 +5,85 @@
</template>
<script setup lang="ts">
import useTagsViewStore from '@/store/modules/tagsView'
import { TagView } from 'vue-router'
import { RouteLocationNormalized } from 'vue-router';
import useTagsViewStore from '@/store/modules/tagsView';
const tagAndTagSpacing = ref(4);
const scrollContainerRef = ref<ElScrollbarInstance>()
const scrollWrapper = computed(() => scrollContainerRef.value?.$refs.wrapRef as any);
const scrollContainerRef = ref<ElScrollbarInstance>();
const scrollWrapper = computed(() => scrollContainerRef.value?.$refs.wrapRef);
onMounted(() => {
    scrollWrapper.value?.addEventListener('scroll', emitScroll, true)
})
  scrollWrapper.value?.addEventListener('scroll', emitScroll, true);
});
onBeforeUnmount(() => {
    scrollWrapper.value?.removeEventListener('scroll', emitScroll)
})
  scrollWrapper.value?.removeEventListener('scroll', emitScroll);
});
const handleScroll = (e: WheelEvent) => {
    const eventDelta = (e as any).wheelDelta || - e.deltaY * 40
    const $scrollWrapper = scrollWrapper.value;
    $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
}
const emits = defineEmits(['scroll'])
  const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
  const $scrollWrapper = scrollWrapper.value;
  $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4;
};
const emits = defineEmits(['scroll']);
const emitScroll = () => {
    emits('scroll')
}
  emits('scroll');
};
const tagsViewStore = useTagsViewStore()
const tagsViewStore = useTagsViewStore();
const visitedViews = computed(() => tagsViewStore.visitedViews);
const moveToTarget = (currentTag: TagView) => {
    const $container = scrollContainerRef.value?.$el
    const $containerWidth = $container.offsetWidth
    const $scrollWrapper = scrollWrapper.value;
const moveToTarget = (currentTag: RouteLocationNormalized) => {
  const $container = scrollContainerRef.value?.$el;
  const $containerWidth = $container.offsetWidth;
  const $scrollWrapper = scrollWrapper.value;
    let firstTag = null
    let lastTag = null
  let firstTag = null;
  let lastTag = null;
    // find first tag and last tag
    if (visitedViews.value.length > 0) {
        firstTag = visitedViews.value[0]
        lastTag = visitedViews.value[visitedViews.value.length - 1]
  // find first tag and last tag
  if (visitedViews.value.length > 0) {
    firstTag = visitedViews.value[0];
    lastTag = visitedViews.value[visitedViews.value.length - 1];
  }
  if (firstTag === currentTag) {
    $scrollWrapper.scrollLeft = 0;
  } else if (lastTag === currentTag) {
    $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
  } else {
    const tagListDom: any = document.getElementsByClassName('tags-view-item');
    const currentIndex = visitedViews.value.findIndex((item) => item === currentTag);
    let prevTag = null;
    let nextTag = null;
    for (const k in tagListDom) {
      if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
        if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
          prevTag = tagListDom[k];
        }
        if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
          nextTag = tagListDom[k];
        }
      }
    }
    if (firstTag === currentTag) {
        $scrollWrapper.scrollLeft = 0
    } else if (lastTag === currentTag) {
        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
    } else {
        const tagListDom: any = document.getElementsByClassName('tags-view-item');
        const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
        let prevTag = null
        let nextTag = null
    // the tag's offsetLeft after of nextTag
    const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value;
        for (const k in tagListDom) {
            if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
                if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
                    prevTag = tagListDom[k];
                }
                if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
                    nextTag = tagListDom[k];
                }
            }
        }
        // the tag's offsetLeft after of nextTag
        const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
        // the tag's offsetLeft before of prevTag
        const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
            $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
            $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
        }
    // the tag's offsetLeft before of prevTag
    const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value;
    if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
      $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
    } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
      $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
    }
}
  }
};
defineExpose({
    moveToTarget,
})
  moveToTarget
});
</script>
<style lang="scss" scoped>
src/layout/components/TagsView/index.vue
@@ -14,226 +14,230 @@
      >
        {{ tag.title }}
        <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
          <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
          <close class="el-icon-close" style="width: 1em; height: 1em; vertical-align: middle" />
        </span>
      </router-link>
    </scroll-pane>
    <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
      <li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em;" /> åˆ·æ–°é¡µé¢</li>
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em;" /> å…³é—­å½“前</li>
      <li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em;" /> å…³é—­å…¶ä»–</li>
      <li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em;" /> å…³é—­å·¦ä¾§</li>
      <li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em;" /> å…³é—­å³ä¾§</li>
      <li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em;" /> å…¨éƒ¨å…³é—­</li>
      <li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em" /> åˆ·æ–°é¡µé¢</li>
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em" /> å…³é—­å½“前</li>
      <li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em" /> å…³é—­å…¶ä»–</li>
      <li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em" /> å…³é—­å·¦ä¾§</li>
      <li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em" /> å…³é—­å³ä¾§</li>
      <li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em" /> å…¨éƒ¨å…³é—­</li>
    </ul>
  </div>
</template>
<script setup lang="ts">
import ScrollPane from './ScrollPane.vue'
import { getNormalPath } from '@/utils/ruoyi'
import useTagsViewStore from "@/store/modules/tagsView";
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { ComponentInternalInstance } from "vue";
import { RouteOption, TagView, RouteLocationRaw } from "vue-router";
import ScrollPane from './ScrollPane.vue';
import { getNormalPath } from '@/utils/ruoyi';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
import useTagsViewStore from '@/store/modules/tagsView';
import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router';
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref<TagView>({});
const affixTags = ref<TagView[]>([]);
const scrollPaneRef = ref(ScrollPane);
const selectedTag = ref<RouteLocationNormalized>();
const affixTags = ref<RouteLocationNormalized[]>([]);
const scrollPaneRef = ref<InstanceType<typeof ScrollPane>>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const visitedViews = computed(() => useTagsViewStore().visitedViews);
const routes = computed(() => usePermissionStore().routes);
const visitedViews = computed(() => useTagsViewStore().getVisitedViews());
const routes = computed(() => usePermissionStore().getRoutes());
const theme = computed(() => useSettingsStore().theme);
watch(route, () => {
    addTags();
    moveToCurrentTag();
})
  addTags();
  moveToCurrentTag();
});
watch(visible, (value) => {
    if (value) {
        document.body.addEventListener('click', closeMenu);
    } else {
        document.body.removeEventListener('click', closeMenu);
    }
})
  if (value) {
    document.body.addEventListener('click', closeMenu);
  } else {
    document.body.removeEventListener('click', closeMenu);
  }
});
const isActive = (r: TagView): boolean => {
    return r.path === route.path;
}
const activeStyle = (tag: TagView) => {
    if (!isActive(tag)) return {};
    return {
        "background-color": theme.value,
        "border-color": theme.value
    };
}
const isAffix = (tag: TagView) => {
    return tag.meta && tag.meta.affix;
}
const isActive = (r: RouteLocationNormalized): boolean => {
  return r.path === route.path;
};
const activeStyle = (tag: RouteLocationNormalized) => {
  if (!isActive(tag)) return {};
  return {
    'background-color': theme.value,
    'border-color': theme.value
  };
};
const isAffix = (tag: RouteLocationNormalized) => {
  return tag?.meta && tag?.meta?.affix;
};
const isFirstView = () => {
    try {
        return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath;
    } catch (err) {
        return false;
    }
}
  try {
    return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath;
  } catch (err) {
    return false;
  }
};
const isLastView = () => {
    try {
        return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath;
    } catch (err) {
        return false;
  try {
    return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath;
  } catch (err) {
    return false;
  }
};
const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
  let tags: RouteLocationNormalized[] = [];
  routes.forEach((route) => {
    if (route.meta && route.meta.affix) {
      const tagPath = getNormalPath(basePath + '/' + route.path);
      tags.push({
        hash: '',
        matched: [],
        params: undefined,
        query: undefined,
        redirectedFrom: undefined,
        fullPath: tagPath,
        path: tagPath,
        name: route.name as string,
        meta: { ...route.meta }
      });
    }
}
const filterAffixTags = (routes:RouteOption [], basePath = '') => {
    let tags:TagView[] = []
    routes.forEach(route => {
        if (route.meta && route.meta.affix) {
            const tagPath = getNormalPath(basePath + '/' + route.path);
            tags.push({
                fullPath: tagPath,
                path: tagPath,
                name: route.name,
                meta: { ...route.meta }
            })
        }
        if (route.children) {
            const tempTags = filterAffixTags(route.children, route.path);
            if (tempTags.length >= 1) {
                tags = [...tags, ...tempTags];
            }
        }
    })
    return tags
}
    if (route.children) {
      const tempTags = filterAffixTags(route.children, route.path);
      if (tempTags.length >= 1) {
        tags = [...tags, ...tempTags];
      }
    }
  });
  return tags;
};
const initTags = () => {
    const res = filterAffixTags(routes.value);
    affixTags.value = res;
    for (const tag of res) {
        // Must have tag name
        if (tag.name) {
            useTagsViewStore().addVisitedView(tag);
        }
  const res = filterAffixTags(routes.value);
  affixTags.value = res;
  for (const tag of res) {
    // Must have tag name
    if (tag.name) {
      useTagsViewStore().addVisitedView(tag);
    }
}
  }
};
const addTags = () => {
    const { name } = route;
    if(route.query.title) {
        route.meta.title = route.query.title;
    }
    if (name) {
        useTagsViewStore().addView(route);
        if (route.meta.link) {
            useTagsViewStore().addIframeView(route);
        }
    }
    return false
}
const moveToCurrentTag = () => {
    nextTick(() => {
        for (const r of visitedViews.value) {
            if (r.path === route.path) {
                scrollPaneRef.value.moveToTarget(r);
                // when query is different then update
                if (r.fullPath !== route.fullPath) {
                    useTagsViewStore().updateVisitedView(route);
                }
            }
        }
    })
}
const refreshSelectedTag = (view: TagView) => {
    proxy?.$tab.refreshPage(view);
  const { name } = route;
  if (route.query.title) {
    route.meta.title = route.query.title as string;
  }
  if (name) {
    useTagsViewStore().addView(route as any);
    if (route.meta.link) {
        useTagsViewStore().delIframeView(route);
      useTagsViewStore().addIframeView(route as any);
    }
}
const closeSelectedTag = (view: TagView) => {
    proxy?.$tab.closePage(view).then(({ visitedViews }: any) => {
        if (isActive(view)) {
            toLastView(visitedViews, view);
  }
  return false;
};
const moveToCurrentTag = () => {
  nextTick(() => {
    for (const r of visitedViews.value) {
      if (r.path === route.path) {
        scrollPaneRef.value?.moveToTarget(r);
        // when query is different then update
        if (r.fullPath !== route.fullPath) {
          useTagsViewStore().updateVisitedView(route);
        }
    })
}
      }
    }
  });
};
const refreshSelectedTag = (view: RouteLocationNormalized) => {
  proxy?.$tab.refreshPage(view);
  if (route.meta.link) {
    useTagsViewStore().delIframeView(route);
  }
};
const closeSelectedTag = (view: RouteLocationNormalized) => {
  proxy?.$tab.closePage(view).then(({ visitedViews }: any) => {
    if (isActive(view)) {
      toLastView(visitedViews, view);
    }
  });
};
const closeRightTags = () => {
    proxy?.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
        if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
            toLastView(visitedViews);
        }
    })
}
  proxy?.$tab.closeRightPage(selectedTag.value).then((visitedViews: RouteLocationNormalized[]) => {
    if (!visitedViews.find((i: RouteLocationNormalized) => i.fullPath === route.fullPath)) {
      toLastView(visitedViews);
    }
  });
};
const closeLeftTags = () => {
    proxy?.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
        if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
            toLastView(visitedViews);
        }
    })
}
  proxy?.$tab.closeLeftPage(selectedTag.value).then((visitedViews: RouteLocationNormalized[]) => {
    if (!visitedViews.find((i: RouteLocationNormalized) => i.fullPath === route.fullPath)) {
      toLastView(visitedViews);
    }
  });
};
const closeOthersTags = () => {
    router.push(selectedTag.value as RouteLocationRaw).catch(() => { });
    proxy?.$tab.closeOtherPage(selectedTag.value).then(() => {
        moveToCurrentTag();
    })
}
const closeAllTags = (view: TagView) => {
    proxy?.$tab.closeAllPage().then(({ visitedViews }) => {
        if (affixTags.value.some(tag => tag.path === route.path)) {
            return;
        }
        toLastView(visitedViews, view);
    })
}
const toLastView = (visitedViews:TagView[], view?: TagView) => {
    const latestView = visitedViews.slice(-1)[0];
    if (latestView) {
        router.push(latestView.fullPath as string);
    } else {
        // now the default is to redirect to the home page if there is no tags-view,
        // you can adjust it according to your needs.
        if (view?.name === 'Dashboard') {
            // to reload home page
            router.replace({ path: '/redirect' + view?.fullPath });
        } else {
            router.push('/');
        }
  router.push(selectedTag.value).catch(() => {});
  proxy?.$tab.closeOtherPage(selectedTag.value).then(() => {
    moveToCurrentTag();
  });
};
const closeAllTags = (view: RouteLocationNormalized) => {
  proxy?.$tab.closeAllPage().then(({ visitedViews }) => {
    if (affixTags.value.some((tag) => tag.path === route.path)) {
      return;
    }
}
const openMenu = (tag: TagView, e: MouseEvent) => {
    const menuMinWidth = 105;
    const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
    const offsetWidth = proxy?.$el.offsetWidth; // container width
    const maxLeft = offsetWidth - menuMinWidth; // left boundary
    const l = e.clientX - offsetLeft + 15; // 15: margin right
    if (l > maxLeft) {
        left.value = maxLeft;
    toLastView(visitedViews, view);
  });
};
const toLastView = (visitedViews: RouteLocationNormalized[], view?: RouteLocationNormalized) => {
  const latestView = visitedViews.slice(-1)[0];
  if (latestView) {
    router.push(latestView.fullPath as string);
  } else {
    // now the default is to redirect to the home page if there is no tags-view,
    // you can adjust it according to your needs.
    if (view?.name === 'Dashboard') {
      // to reload home page
      router.replace({ path: '/redirect' + view?.fullPath });
    } else {
        left.value = l;
      router.push('/');
    }
  }
};
const openMenu = (tag: RouteLocationNormalized, e: MouseEvent) => {
  const menuMinWidth = 105;
  const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
  const offsetWidth = proxy?.$el.offsetWidth; // container width
  const maxLeft = offsetWidth - menuMinWidth; // left boundary
  const l = e.clientX - offsetLeft + 15; // 15: margin right
    top.value = e.clientY
    visible.value = true;
    selectedTag.value = tag;
}
  if (l > maxLeft) {
    left.value = maxLeft;
  } else {
    left.value = l;
  }
  top.value = e.clientY;
  visible.value = true;
  selectedTag.value = tag;
};
const closeMenu = () => {
    visible.value = false;
}
  visible.value = false;
};
const handleScroll = () => {
    closeMenu();
}
  closeMenu();
};
onMounted(() => {
    initTags();
    addTags();
})
  initTags();
  addTags();
});
</script>
<style lang="scss" scoped>
@@ -242,7 +246,9 @@
  width: 100%;
  background-color: var(--el-bg-color);
  border: 1px solid var(--el-border-color-light);
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
  box-shadow:
    0 1px 3px 0 rgba(0, 0, 0, 0.12),
    0 0 3px 0 rgba(0, 0, 0, 0.04);
  .tags-view-wrapper {
    .tags-view-item {
      display: inline-block;
@@ -271,7 +277,7 @@
        color: #fff;
        border-color: #42b983;
        &::before {
          content: "";
          content: '';
          background: #fff;
          display: inline-block;
          width: 8px;
src/layout/components/TopBar/search.vue
@@ -3,12 +3,12 @@
    <el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false">
      <template #footer>
        <el-autocomplete
          ref="layoutMenuAutocompleteRef"
          v-model="state.menuQuery"
          :fetch-suggestions="menuSearch"
          placeholder="搜索"
          ref="layoutMenuAutocompleteRef"
          @select="onHandleSelect"
          :fit-input-width="true"
          @select="onHandleSelect"
        >
          <template #prefix>
            <svg-icon class-name="search-icon" icon-class="search" />
@@ -29,130 +29,130 @@
import { getNormalPath } from '@/utils/ruoyi';
import { isHttp } from '@/utils/validate';
import usePermissionStore from '@/store/modules/permission';
import { RouteOption } from 'vue-router';
import { RouteRecordRaw } from 'vue-router';
type Router = Array<{
    path: string;
    icon: string;
    title: string[];
}>
  path: string;
  icon: string;
  title: string[];
}>;
type SearchState<T = any> = {
    isShowSearch: boolean;
    menuQuery: string;
    menuList: T[];
  isShowSearch: boolean;
  menuQuery: string;
  menuList: T[];
};
// å®šä¹‰å˜é‡å†…容
const layoutMenuAutocompleteRef = ref();
const router = useRouter();
const routes = computed(() => usePermissionStore().routes);
const state = reactive<SearchState>({
    isShowSearch: false,
    menuQuery: '',
    menuList: [],
  isShowSearch: false,
  menuQuery: '',
  menuList: []
});
// æœç´¢å¼¹çª—打开
const openSearch = () => {
    state.menuQuery = '';
    state.isShowSearch = true;
    state.menuList = generateRoutes(routes.value);
    nextTick(() => {
        setTimeout(() => {
            layoutMenuAutocompleteRef.value.focus();
        });
    });
  state.menuQuery = '';
  state.isShowSearch = true;
  state.menuList = generateRoutes(routes.value as any);
  nextTick(() => {
    setTimeout(() => {
      layoutMenuAutocompleteRef.value.focus();
    });
  });
};
// æœç´¢å¼¹çª—关闭
const closeSearch = () => {
    state.isShowSearch = false;
  state.isShowSearch = false;
};
// èœå•搜索数据过滤
const menuSearch = (queryString: string, cb: Function) => {
    let options = state.menuList.filter((item) => {
        return item.title.indexOf(queryString) > -1;
    });
    cb(options);
  let options = state.menuList.filter((item) => {
    return item.title.indexOf(queryString) > -1;
  });
  cb(options);
};
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: string[] = []) => {
    let res: Router = []
    routes.forEach(r => {
        // skip hidden router
        if (!r.hidden) {
            const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
                const data: any = {
                    path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
                    icon: r.meta?.icon,
                    title: [...prefixTitle]
                }
                if (r.meta && r.meta.title) {
                    data.title = [...data.title, r.meta.title];
                    if (r.redirect !== 'noRedirect') {
                        // only push the routes with title
                        // special case: need to exclude parent router without redirect
            res.push(data);
                    }
                }
                // recursive child routes
                if (r.children) {
                        const tempRoutes = generateRoutes(r.children, data.path, data.title);
                        if (tempRoutes.length >= 1) {
                                res = [...res, ...tempRoutes];
                        }
                }
        }
    })
    res.forEach((item: any) => {
        if (item.title instanceof Array) {
            item.title = item.title.join('/');
        }
    });
    return res;
}
const generateRoutes = (routes: RouteRecordRaw[], basePath = '', prefixTitle: string[] = []) => {
  let res: Router = [];
  routes.forEach((r) => {
    // skip hidden router
    if (!r.hidden) {
      const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
      const data: any = {
        path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
        icon: r.meta?.icon,
        title: [...prefixTitle]
      };
      if (r.meta && r.meta.title) {
        data.title = [...data.title, r.meta.title];
        if (r.redirect !== 'noRedirect') {
          // only push the routes with title
          // special case: need to exclude parent router without redirect
          res.push(data);
        }
      }
      // recursive child routes
      if (r.children) {
        const tempRoutes = generateRoutes(r.children, data.path, data.title);
        if (tempRoutes.length >= 1) {
          res = [...res, ...tempRoutes];
        }
      }
    }
  });
  res.forEach((item: any) => {
    if (item.title instanceof Array) {
      item.title = item.title.join('/');
    }
  });
  return res;
};
// å½“前菜单选中时
const onHandleSelect = (val: any) => {
    const paths = val.path;
    if (isHttp(paths)) {
        // http(s):// è·¯å¾„新窗口打开
        const pindex = paths.indexOf("http");
        window.open(paths.substring(pindex, paths.length), "_blank");
    } else {
        router.push(paths);
    }
    state.menuQuery = ''
    closeSearch();
  const paths = val.path;
  if (isHttp(paths)) {
    // http(s):// è·¯å¾„新窗口打开
    const pindex = paths.indexOf('http');
    window.open(paths.substring(pindex, paths.length), '_blank');
  } else {
    router.push(paths);
  }
  state.menuQuery = '';
  closeSearch();
};
// æš´éœ²å˜é‡
defineExpose({
    openSearch
  openSearch
});
</script>
<style scoped lang="scss">
.layout-search-dialog {
    position: relative;
    :deep(.el-dialog) {
        .el-dialog__header,
        .el-dialog__body {
            display: none;
        }
        .el-dialog__footer {
            width: 100%;
            position: absolute;
            left: 50%;
            transform: translateX(-50%);
            top: -53vh;
        }
    }
    :deep(.el-autocomplete) {
        width: 560px;
        position: absolute;
        top: 150px;
        left: 50%;
        transform: translateX(-50%);
    }
  position: relative;
  :deep(.el-dialog) {
    padding: 0;
    .el-dialog__header,
    .el-dialog__body {
      display: none;
    }
    .el-dialog__footer {
      width: 100%;
      position: absolute;
      left: 50%;
      transform: translateX(-50%);
      top: -53vh;
    }
  }
  :deep(.el-autocomplete) {
    width: 560px;
    position: absolute;
    top: 150px;
    left: 50%;
    transform: translateX(-50%);
  }
}
</style>
src/layout/components/notice/index.vue
@@ -1,12 +1,12 @@
<template>
  <div class="layout-navbars-breadcrumb-user-news" v-loading="state.loading">
  <div v-loading="state.loading" class="layout-navbars-breadcrumb-user-news">
    <div class="head-box">
      <div class="head-box-title">通知公告</div>
      <div class="head-box-btn" @click="readAll">全部已读</div>
    </div>
    <div class="content-box" v-loading="state.loading">
    <div v-loading="state.loading" class="content-box">
      <template v-if="newsList.length > 0">
        <div class="content-box-item" v-for="(v, k) in newsList" :key="k" @click="onNewsClick(k)">
        <div v-for="(v, k) in newsList" :key="k" class="content-box-item" @click="onNewsClick(k)">
          <div class="item-conten">
            <div>{{ v.message }}</div>
            <div class="content-box-msg"></div>
@@ -17,26 +17,24 @@
          <span v-else class="el-tag el-tag--danger el-tag--mini read">未读</span>
        </div>
      </template>
      <el-empty :description="'消息为空'" v-else></el-empty>
      <el-empty v-else :description="'消息为空'"></el-empty>
    </div>
    <div class="foot-box" @click="onGoToGiteeClick" v-if="newsList.length > 0">前往gitee</div>
    <div v-if="newsList.length > 0" class="foot-box" @click="onGoToGiteeClick">前往gitee</div>
  </div>
</template>
<script setup lang="ts" name="layoutBreadcrumbUserNews">
import { ref } from "vue";
import { storeToRefs } from 'pinia'
import { nextTick, onMounted, reactive } from "vue";
import { storeToRefs } from 'pinia';
import useNoticeStore from '@/store/modules/notice';
const noticeStore = storeToRefs(useNoticeStore());
const {readAll} = useNoticeStore();
const { readAll } = useNoticeStore();
// å®šä¹‰å˜é‡å†…容
const state = reactive({
  loading: false,
  loading: false
});
const newsList =ref([]) as any;
const newsList = ref([]) as any;
/**
 * åˆå§‹åŒ–数据
@@ -48,7 +46,6 @@
  state.loading = false;
};
//点击消息,写入已读
const onNewsClick = (item: any) => {
  newsList.value[item].read = true;
@@ -58,7 +55,7 @@
// å‰å¾€é€šçŸ¥ä¸­å¿ƒç‚¹å‡»
const onGoToGiteeClick = () => {
  window.open("https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/");
  window.open('https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/');
};
onMounted(() => {
src/layout/index.vue
@@ -12,7 +12,7 @@
        <settings ref="settingRef" />
      </el-scrollbar> -->
      <div :class="{ 'fixed-header': fixedHeader }">
        <navbar ref="navbarRef" @setLayout="setLayout" />
        <navbar ref="navbarRef" @set-layout="setLayout" />
        <tags-view v-if="needTagsView" />
      </div>
      <app-main />
@@ -22,12 +22,13 @@
</template>
<script setup lang="ts">
import SideBar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import SideBar from './components/Sidebar/index.vue';
import { AppMain, Navbar, Settings, TagsView } from './components';
import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import { initWebSocket } from '@/utils/websocket';
const settingsStore = useSettingsStore()
const settingsStore = useSettingsStore();
const theme = computed(() => settingsStore.theme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
@@ -35,48 +36,53 @@
const fixedHeader = computed(() => settingsStore.fixedHeader);
const classObj = computed(() => ({
    hideSidebar: !sidebar.value.opened,
    openSidebar: sidebar.value.opened,
    withoutAnimation: sidebar.value.withoutAnimation,
    mobile: device.value === 'mobile'
}))
  hideSidebar: !sidebar.value.opened,
  openSidebar: sidebar.value.opened,
  withoutAnimation: sidebar.value.withoutAnimation,
  mobile: device.value === 'mobile'
}));
const { width } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
watchEffect(() => {
    if (device.value === 'mobile' && sidebar.value.opened) {
        useAppStore().closeSideBar({ withoutAnimation: false })
    }
    if (width.value - 1 < WIDTH) {
        useAppStore().toggleDevice('mobile')
        useAppStore().closeSideBar({ withoutAnimation: true })
    } else {
        useAppStore().toggleDevice('desktop')
    }
})
  if (device.value === 'mobile') {
    useAppStore().closeSideBar({ withoutAnimation: false });
  }
  if (width.value - 1 < WIDTH) {
    useAppStore().toggleDevice('mobile');
    useAppStore().closeSideBar({ withoutAnimation: true });
  } else {
    useAppStore().toggleDevice('desktop');
  }
});
const navbarRef = ref(Navbar);
const settingRef = ref(Settings);
const navbarRef = ref<InstanceType<typeof Navbar>>();
const settingRef = ref<InstanceType<typeof Settings>>();
onMounted(() => {
  nextTick(() => {
    navbarRef.value.initTenantList();
  })
})
    navbarRef.value?.initTenantList();
  });
});
onMounted(() => {
  let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
  initWebSocket(protocol + window.location.host + import.meta.env.VITE_APP_BASE_API + '/resource/websocket');
});
const handleClickOutside = () => {
  useAppStore().closeSideBar({ withoutAnimation: false })
}
  useAppStore().closeSideBar({ withoutAnimation: false });
};
const setLayout = () => {
  settingRef.value.openSetting();
}
  settingRef.value?.openSetting();
};
</script>
<style lang="scss" scoped>
  @import "@/assets/styles/mixin.scss";
  @import "@/assets/styles/variables.module.scss";
@import '@/assets/styles/mixin.scss';
@import '@/assets/styles/variables.module.scss';
.app-wrapper {
  @include clearfix;
src/main.ts
@@ -1,6 +1,6 @@
import { createApp } from 'vue';
// global css
import 'uno.css';
import 'virtual:uno.css';
import '@/assets/styles/index.scss';
import 'element-plus/theme-chalk/dark/css-vars.css';
@@ -14,10 +14,12 @@
// æ³¨å†Œæ’ä»¶
import plugins from './plugins/index'; // plugins
import { download } from '@/utils/request';
// é¢„设动画
import animate from './animate';
// é«˜äº®ç»„ä»¶
// import 'highlight.js/styles/a11y-light.css';
import 'highlight.js/styles/atom-one-dark.css';
import 'highlight.js/lib/common';
import HighLight from '@highlightjs/vue-plugin';
// svg图标
import 'virtual:svg-icons-register';
@@ -26,30 +28,28 @@
// permission control
import './permission';
import { useDict } from '@/utils/dict';
import { getConfigKey, updateConfigByKey } from '@/api/system/config';
import { parseTime, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi';
// å›½é™…化
import i18n from '@/lang/index';
const app = createApp(App);
// å…¨å±€æ–¹æ³•挂载
app.config.globalProperties.useDict = useDict;
app.config.globalProperties.getConfigKey = getConfigKey;
app.config.globalProperties.updateConfigByKey = updateConfigByKey;
app.config.globalProperties.download = download;
app.config.globalProperties.parseTime = parseTime;
app.config.globalProperties.handleTree = handleTree;
app.config.globalProperties.addDateRange = addDateRange;
app.config.globalProperties.selectDictLabel = selectDictLabel;
app.config.globalProperties.selectDictLabels = selectDictLabels;
app.config.globalProperties.animate = animate;
// vxeTable
import VXETable from 'vxe-table';
import 'vxe-table/lib/style.css';
VXETable.config({
  zIndex: 999999
});
// ä¿®æ”¹ el-dialog é»˜è®¤ç‚¹å‡»é®ç…§ä¸ºä¸å…³é—­
import { ElDialog } from 'element-plus';
ElDialog.props.closeOnClickModal.default = false;
const app = createApp(App);
app.use(HighLight);
app.use(ElementIcons);
app.use(router);
app.use(store);
app.use(i18n);
app.use(VXETable);
app.use(plugins);
// è‡ªå®šä¹‰æŒ‡ä»¤
directive(app);
src/permission.ts
@@ -15,13 +15,13 @@
router.beforeEach(async (to, from, next) => {
  NProgress.start();
  if (getToken()) {
    to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
    to.meta.title && useSettingsStore().setTitle(to.meta.title);
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' });
      NProgress.done();
    } else if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else if (whiteList.indexOf(to.path as string) !== -1) {
      next();
    } else {
      if (useUserStore().roles.length === 0) {
        isRelogin.show = true;
@@ -40,7 +40,7 @@
              router.addRoute(route); // åŠ¨æ€æ·»åŠ å¯è®¿é—®è·¯ç”±è¡¨
            }
          });
          next({ ...to, replace: true }); // hack方法 ç¡®ä¿addRoutes已完成
          next({ path: to.path, replace: true, params: to.params, query: to.query, hash: to.hash, name: to.name as string }); // hack方法 ç¡®ä¿addRoutes已完成
        }
      } else {
        next();
@@ -48,11 +48,12 @@
    }
  } else {
    // æ²¡æœ‰token
    if (whiteList.indexOf(to.path) !== -1) {
    if (whiteList.indexOf(to.path as string) !== -1) {
      // åœ¨å…ç™»å½•白名单,直接进入
      next();
    } else {
      next(`/login?redirect=${to.fullPath}`); // å¦åˆ™å…¨éƒ¨é‡å®šå‘到登录页
      const redirect = encodeURIComponent(to.fullPath || '/');
      next(`/login?redirect=${redirect}`) // å¦åˆ™å…¨éƒ¨é‡å®šå‘到登录页
      NProgress.done();
    }
  }
src/plugins/index.ts
@@ -3,6 +3,13 @@
import download from './download';
import cache from './cache';
import auth from './auth';
// é¢„设动画
import animate from '@/animate';
import { download as dl } from '@/utils/request';
import { useDict } from '@/utils/dict';
import { getConfigKey, updateConfigByKey } from '@/api/system/config';
import { parseTime, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi';
import { App } from 'vue';
@@ -21,4 +28,16 @@
  // è®¤è¯å¯¹è±¡
  app.config.globalProperties.$auth = auth;
  // å…¨å±€æ–¹æ³•挂载
  app.config.globalProperties.useDict = useDict;
  app.config.globalProperties.getConfigKey = getConfigKey;
  app.config.globalProperties.updateConfigByKey = updateConfigByKey;
  app.config.globalProperties.download = dl;
  app.config.globalProperties.parseTime = parseTime;
  app.config.globalProperties.handleTree = handleTree;
  app.config.globalProperties.addDateRange = addDateRange;
  app.config.globalProperties.selectDictLabel = selectDictLabel;
  app.config.globalProperties.selectDictLabels = selectDictLabels;
  app.config.globalProperties.animate = animate;
}
src/plugins/tab.ts
@@ -1,19 +1,29 @@
import { useTagsViewStore } from '@/store/modules/tagsView';
import router from '@/router';
import { TagView, RouteLocationRaw } from 'vue-router';
import { RouteLocationMatched, RouteLocationNormalized } from 'vue-router';
import useTagsViewStore from '@/store/modules/tagsView';
export default {
  /**
   * åˆ·æ–°å½“前tab页签
   * @param obj æ ‡ç­¾å¯¹è±¡
   */
  async refreshPage(obj?: TagView): Promise<void> {
  async refreshPage(obj?: RouteLocationNormalized): Promise<void> {
    const { path, query, matched } = router.currentRoute.value;
    if (obj === undefined) {
      matched.forEach((m) => {
      matched.forEach((m: RouteLocationMatched) => {
        if (m.components && m.components.default && m.components.default.name) {
          if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
            obj = { name: m.components.default.name, path: path, query: query };
            obj = {
              name: m.components.default.name,
              path: path,
              query: query,
              matched: undefined,
              fullPath: undefined,
              hash: undefined,
              params: undefined,
              redirectedFrom: undefined,
              meta: undefined
            };
          }
        }
      });
@@ -31,17 +41,17 @@
    });
  },
  // å…³é—­å½“前tab页签,打开新页签
  closeOpenPage(obj: RouteLocationRaw): void {
  closeOpenPage(obj: RouteLocationNormalized): void {
    useTagsViewStore().delView(router.currentRoute.value);
    if (obj !== undefined) {
      router.push(obj);
    }
  },
  // å…³é—­æŒ‡å®štab页签
  async closePage(obj?: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] } | any> {
  async closePage(obj?: RouteLocationNormalized): Promise<{ visitedViews: RouteLocationNormalized[]; cachedViews: string[] } | any> {
    if (obj === undefined) {
      // prettier-ignore
      const { visitedViews } = await useTagsViewStore().delView(router.currentRoute.value) as any
      const { visitedViews } = await useTagsViewStore().delView(router.currentRoute.value)
      const latestView = visitedViews.slice(-1)[0];
      if (latestView) {
        return router.push(latestView.fullPath);
@@ -55,15 +65,15 @@
    return useTagsViewStore().delAllViews();
  },
  // å…³é—­å·¦ä¾§tab页签
  closeLeftPage(obj?: TagView) {
  closeLeftPage(obj?: RouteLocationNormalized) {
    return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
  },
  // å…³é—­å³ä¾§tab页签
  closeRightPage(obj?: TagView) {
  closeRightPage(obj?: RouteLocationNormalized) {
    return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
  },
  // å…³é—­å…¶ä»–tab页签
  closeOtherPage(obj?: TagView) {
  closeOtherPage(obj?: RouteLocationNormalized) {
    return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
  },
  /**
@@ -80,7 +90,7 @@
   * ä¿®æ”¹tab页签
   * @param obj æ ‡ç­¾å¯¹è±¡
   */
  updatePage(obj: TagView) {
  updatePage(obj: RouteLocationNormalized) {
    return useTagsViewStore().updateVisitedView(obj);
  }
};
src/router/index.ts
@@ -1,4 +1,4 @@
import { createWebHistory, createRouter, RouteOption } from 'vue-router';
import { createWebHistory, createRouter, RouteRecordRaw } from 'vue-router';
/* Layout */
import Layout from '@/layout/index.vue';
@@ -25,7 +25,7 @@
 */
// å…¬å…±è·¯ç”±
export const constantRoutes: RouteOption[] = [
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/redirect',
    component: Layout,
@@ -92,7 +92,7 @@
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
export const dynamicRoutes: RouteOption[] = [
export const dynamicRoutes: RouteRecordRaw[] = [
  {
    path: '/system/user-auth',
    component: Layout,
@@ -162,6 +162,20 @@
        meta: { title: '修改生成配置', activeMenu: '/tool/gen', icon: '' }
      }
    ]
  },
  {
    path: '/demo/leaveEdit',
    component: Layout,
    hidden: true,
    permissions: ['demo:leave:edit'],
    children: [
      {
        path: 'index',
        component: () => import('@/views/workflow/leave/leaveEdit.vue'),
        name: 'leaveEdit',
        meta: { title: '请假申请', activeMenu: '/demo/leave', noCache: true }
      }
    ]
  }
];
src/settings.ts
@@ -1,3 +1,5 @@
import { LanguageEnum } from '@/enums/LanguageEnum';
const setting: DefaultSettings = {
  /**
   * ç½‘页标题
@@ -50,6 +52,11 @@
  animationEnable: false,
  dark: false
  dark: false,
  language: LanguageEnum.zh_CN,
  size: 'default',
  layout: ''
};
export default setting;
src/store/modules/app.ts
@@ -9,7 +9,7 @@
    hide: false
  });
  const device = ref<string>('desktop');
  const size = useStorage('size', 'default');
  const size = useStorage<'large' | 'default' | 'small'>('size', 'default');
  // è¯­è¨€
  const language = useStorage('language', 'zh_CN');
@@ -43,7 +43,7 @@
  const toggleDevice = (d: string): void => {
    device.value = d;
  };
  const setSize = (s: string): void => {
  const setSize = (s: 'large' | 'default' | 'small'): void => {
    size.value = s;
  };
  const toggleSideBarHide = (status: boolean): void => {
src/store/modules/modeler.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
import { Modeler, Modeling, Canvas, ElementRegistry, Moddle, BpmnFactory } from 'bpmn';
type ModelerStore = {
  modeler: Modeler | undefined;
  moddle: Moddle | undefined;
  modeling: Modeling | undefined;
  canvas: Canvas | undefined;
  elementRegistry: ElementRegistry | undefined;
  bpmnFactory: BpmnFactory | undefined;
  // æµç¨‹å®šä¹‰æ ¹èŠ‚ç‚¹ä¿¡æ¯
  procDefId: string | undefined;
  procDefName: string | undefined;
};
const defaultState: ModelerStore = {
  modeler: undefined,
  moddle: undefined,
  modeling: undefined,
  canvas: undefined,
  elementRegistry: undefined,
  bpmnFactory: undefined,
  procDefId: undefined,
  procDefName: undefined
};
export const useModelerStore = defineStore('modeler', () => {
  let modeler = defaultState.modeler;
  let moddle = defaultState.moddle;
  let modeling = defaultState.modeling;
  let canvas = defaultState.canvas;
  let elementRegistry = defaultState.elementRegistry;
  let bpmnFactory = defaultState.bpmnFactory;
  const procDefId = ref(defaultState.procDefId);
  const procDefName = ref(defaultState.procDefName);
  const getModeler = () => modeler;
  const getModdle = () => moddle;
  const getModeling = (): Modeling | undefined => modeling;
  const getCanvas = (): Canvas | undefined => canvas;
  const getElRegistry = (): ElementRegistry | undefined => elementRegistry;
  const getBpmnFactory = (): BpmnFactory | undefined => bpmnFactory;
  const getProcDefId = (): string | undefined => procDefId.value;
  const getProcDefName = (): string | undefined => procDefName.value;
  // è®¾ç½®æ ¹èŠ‚ç‚¹
  const setModeler = (modelers: Modeler | undefined) => {
    if (modelers) {
      modeler = modelers;
      modeling = modelers.get<Modeling>('modeling');
      moddle = modelers.get<Moddle>('moddle');
      canvas = modelers.get<Canvas>('canvas');
      bpmnFactory = modelers.get<BpmnFactory>('bpmnFactory');
      elementRegistry = modelers.get<ElementRegistry>('elementRegistry');
    } else {
      modeling = moddle = canvas = elementRegistry = bpmnFactory = undefined;
    }
  };
  // è®¾ç½®æµç¨‹å®šä¹‰æ ¹èŠ‚ç‚¹ä¿¡æ¯
  const setProcDef = (modeler: Modeler | undefined) => {
    procDefId.value = modeler.get<Canvas>('canvas').getRootElement().businessObject.get('id');
    procDefName.value = modeler.get<Canvas>('canvas').getRootElement().businessObject.get('name');
  };
  return {
    getModeler,
    getModdle,
    getModeling,
    getCanvas,
    getElRegistry,
    getBpmnFactory,
    getProcDefId,
    getProcDefName,
    setModeler,
    setProcDef
  };
});
export default useModelerStore;
src/store/modules/notice.ts
@@ -22,7 +22,7 @@
  //实现全部已读
  const readAll = () => {
    state.notices.forEach((item) => {
    state.notices.forEach((item: any) => {
      item.read = true;
    });
  };
src/store/modules/permission.ts
@@ -2,35 +2,46 @@
import router, { constantRoutes, dynamicRoutes } from '@/router';
import store from '@/store';
import { getRouters } from '@/api/menu';
import auth from '@/plugins/auth';
import { RouteRecordRaw } from 'vue-router';
import Layout from '@/layout/index.vue';
import ParentView from '@/components/ParentView/index.vue';
import InnerLink from '@/layout/components/InnerLink/index.vue';
import auth from '@/plugins/auth';
import { RouteOption } from 'vue-router';
// åŒ¹é…views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue');
export const usePermissionStore = defineStore('permission', () => {
  const routes = ref<RouteOption[]>([]);
  const addRoutes = ref<RouteOption[]>([]);
  const defaultRoutes = ref<RouteOption[]>([]);
  const topbarRouters = ref<RouteOption[]>([]);
  const sidebarRouters = ref<RouteOption[]>([]);
  const routes = ref<RouteRecordRaw[]>([]);
  const addRoutes = ref<RouteRecordRaw[]>([]);
  const defaultRoutes = ref<RouteRecordRaw[]>([]);
  const topbarRouters = ref<RouteRecordRaw[]>([]);
  const sidebarRouters = ref<RouteRecordRaw[]>([]);
  const setRoutes = (newRoutes: RouteOption[]): void => {
  const getRoutes = (): RouteRecordRaw[] => {
    return routes.value;
  };
  const getSidebarRoutes = (): RouteRecordRaw[] => {
    return sidebarRouters.value;
  };
  const getTopbarRoutes = (): RouteRecordRaw[] => {
    return topbarRouters.value;
  };
  const setRoutes = (newRoutes: RouteRecordRaw[]): void => {
    addRoutes.value = newRoutes;
    routes.value = constantRoutes.concat(newRoutes);
  };
  const setDefaultRoutes = (routes: RouteOption[]): void => {
  const setDefaultRoutes = (routes: RouteRecordRaw[]): void => {
    defaultRoutes.value = constantRoutes.concat(routes);
  };
  const setTopbarRoutes = (routes: RouteOption[]): void => {
  const setTopbarRoutes = (routes: RouteRecordRaw[]): void => {
    topbarRouters.value = routes;
  };
  const setSidebarRouters = (routes: RouteOption[]): void => {
  const setSidebarRouters = (routes: RouteRecordRaw[]): void => {
    sidebarRouters.value = routes;
  };
  const generateRoutes = async (): Promise<RouteOption[]> => {
  const generateRoutes = async (): Promise<RouteRecordRaw[]> => {
    const res = await getRouters();
    const { data } = res;
    const sdata = JSON.parse(JSON.stringify(data));
@@ -47,7 +58,7 @@
    setSidebarRouters(constantRoutes.concat(sidebarRoutes));
    setDefaultRoutes(sidebarRoutes);
    setTopbarRoutes(defaultRoutes);
    return new Promise<RouteOption[]>((resolve) => resolve(rewriteRoutes));
    return new Promise<RouteRecordRaw[]>((resolve) => resolve(rewriteRoutes));
  };
  /**
@@ -56,22 +67,20 @@
   * @param lastRouter ä¸Šä¸€çº§è·¯ç”±
   * @param type æ˜¯å¦æ˜¯é‡å†™è·¯ç”±
   */
  const filterAsyncRouter = (asyncRouterMap: RouteOption[], lastRouter?: RouteOption, type = false): RouteOption[] => {
  const filterAsyncRouter = (asyncRouterMap: RouteRecordRaw[], lastRouter?: RouteRecordRaw, type = false): RouteRecordRaw[] => {
    return asyncRouterMap.filter((route) => {
      if (type && route.children) {
        route.children = filterChildren(route.children, undefined);
      }
      if (route.component) {
        // Layout ParentView ç»„件特殊处理
        if (route.component === 'Layout') {
          route.component = Layout;
        } else if (route.component === 'ParentView') {
          route.component = ParentView;
        } else if (route.component === 'InnerLink') {
          route.component = InnerLink;
        } else {
          route.component = loadView(route.component);
        }
      // Layout ParentView ç»„件特殊处理
      if (route.component?.toString() === 'Layout') {
        route.component = Layout;
      } else if (route.component?.toString() === 'ParentView') {
        route.component = ParentView;
      } else if (route.component?.toString() === 'InnerLink') {
        route.component = InnerLink;
      } else {
        route.component = loadView(route.component);
      }
      if (route.children != null && route.children && route.children.length) {
        route.children = filterAsyncRouter(route.children, route, type);
@@ -82,11 +91,11 @@
      return true;
    });
  };
  const filterChildren = (childrenMap: RouteOption[], lastRouter?: RouteOption): RouteOption[] => {
    let children: RouteOption[] = [];
  const filterChildren = (childrenMap: RouteRecordRaw[], lastRouter?: RouteRecordRaw): RouteRecordRaw[] => {
    let children: RouteRecordRaw[] = [];
    childrenMap.forEach((el) => {
      if (el.children && el.children.length) {
        if (el.component === 'ParentView' && !lastRouter) {
        if (el.component?.toString() === 'ParentView' && !lastRouter) {
          el.children.forEach((c) => {
            c.path = el.path + '/' + c.path;
            if (c.children && c.children.length) {
@@ -101,20 +110,33 @@
      if (lastRouter) {
        el.path = lastRouter.path + '/' + el.path;
        if (el.children && el.children.length) {
          children = children.concat(filterChildren(el.children, el))
          return
          children = children.concat(filterChildren(el.children, el));
          return;
        }
      }
      children = children.concat(el);
    });
    return children;
  };
  return { routes, setRoutes, generateRoutes, setSidebarRouters, topbarRouters, sidebarRouters, defaultRoutes };
  return {
    routes,
    topbarRouters,
    sidebarRouters,
    defaultRoutes,
    getRoutes,
    getSidebarRoutes,
    getTopbarRoutes,
    setRoutes,
    generateRoutes,
    setSidebarRouters
  };
});
// åŠ¨æ€è·¯ç”±éåŽ†ï¼ŒéªŒè¯æ˜¯å¦å…·å¤‡æƒé™
export const filterDynamicRoutes = (routes: RouteOption[]) => {
  const res: RouteOption[] = [];
export const filterDynamicRoutes = (routes: RouteRecordRaw[]) => {
  const res: RouteRecordRaw[] = [];
  routes.forEach((route) => {
    if (route.permissions) {
      if (auth.hasPermiOr(route.permissions)) {
src/store/modules/settings.ts
@@ -1,35 +1,29 @@
import { defineStore } from 'pinia';
import defaultSettings from '@/settings';
import { SettingTypeEnum } from '@/enums/SettingTypeEnum';
import { useDynamicTitle } from '@/utils/dynamicTitle';
import { Ref } from 'vue';
export const useSettingsStore = defineStore('setting', () => {
  const storageSetting = JSON.parse(localStorage.getItem('layout-setting') || '{}');
  const storageSetting = useStorage<LayoutSetting>('layout-setting', {
    topNav: defaultSettings.topNav,
    tagsView: defaultSettings.tagsView,
    fixedHeader: defaultSettings.fixedHeader,
    sidebarLogo: defaultSettings.sidebarLogo,
    dynamicTitle: defaultSettings.dynamicTitle,
    sideTheme: defaultSettings.sideTheme,
    theme: defaultSettings.theme
  });
  const title = ref<string>(defaultSettings.title);
  const theme = ref<string>(storageSetting.value.theme);
  const sideTheme = ref<string>(storageSetting.value.sideTheme);
  const showSettings = ref<boolean>(defaultSettings.showSettings);
  const topNav = ref<boolean>(storageSetting.value.topNav);
  const tagsView = ref<boolean>(storageSetting.value.tagsView);
  const fixedHeader = ref<boolean>(storageSetting.value.fixedHeader);
  const sidebarLogo = ref<boolean>(storageSetting.value.sidebarLogo);
  const dynamicTitle = ref<boolean>(storageSetting.value.dynamicTitle);
  const animationEnable = ref<boolean>(defaultSettings.animationEnable);
  const dark = ref<boolean>(defaultSettings.dark);
  const prop: { [key: string]: Ref<any> } = {
    title: ref<string>(''),
    theme: ref<string>(storageSetting.theme || defaultSettings.theme),
    sideTheme: ref<string>(storageSetting.sideTheme || defaultSettings.sideTheme),
    showSettings: ref<boolean>(storageSetting.showSettings || defaultSettings.showSettings),
    topNav: ref<boolean>(storageSetting.topNav === undefined ? defaultSettings.topNav : storageSetting.topNav),
    tagsView: ref<boolean>(storageSetting.tagsView === undefined ? defaultSettings.tagsView : storageSetting.tagsView),
    fixedHeader: ref<boolean>(storageSetting.fixedHeader === undefined ? defaultSettings.fixedHeader : storageSetting.fixedHeader),
    sidebarLogo: ref<boolean>(storageSetting.sidebarLogo === undefined ? defaultSettings.sidebarLogo : storageSetting.sidebarLogo),
    dynamicTitle: ref<boolean>(storageSetting.dynamicTitle === undefined ? defaultSettings.dynamicTitle : storageSetting.dynamicTitle),
    animationEnable: ref<boolean>(storageSetting.animationEnable === undefined ? defaultSettings.animationEnable : storageSetting.animationEnable),
    dark: ref<boolean>(storageSetting.dark || defaultSettings.dark)
  };
  const { title, theme, sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle, animationEnable, dark } = prop;
  // actions
  const changeSetting = (param: { key: SettingTypeEnum; value: any }) => {
    const { key, value } = param;
    if (key in prop) {
      prop[key].value = value;
    }
  };
  const setTitle = (value: string) => {
    title.value = value;
    useDynamicTitle();
@@ -46,7 +40,6 @@
    dynamicTitle,
    animationEnable,
    dark,
    changeSetting,
    setTitle
  };
});
src/store/modules/tagsView.ts
@@ -1,38 +1,53 @@
import { TagView, RouteRecordNormalized } from 'vue-router';
import { RouteLocationNormalized } from 'vue-router';
export const useTagsViewStore = defineStore('tagsView', () => {
  const visitedViews = ref<TagView[]>([]);
  const visitedViews = ref<RouteLocationNormalized[]>([]);
  const cachedViews = ref<string[]>([]);
  const iframeViews = ref<TagView[]>([]);
  const iframeViews = ref<RouteLocationNormalized[]>([]);
  const addView = (view: TagView) => {
  const getVisitedViews = (): RouteLocationNormalized[] => {
    return visitedViews.value;
  };
  const getIframeViews = (): RouteLocationNormalized[] => {
    return iframeViews.value;
  };
  const getCachedViews = (): string[] => {
    return cachedViews.value;
  };
  const addView = (view: RouteLocationNormalized) => {
    addVisitedView(view);
    addCachedView(view);
  };
  const addIframeView = (view: TagView): void => {
    if (iframeViews.value.some((v) => v.path === view.path)) return;
  const addIframeView = (view: RouteLocationNormalized): void => {
    if (iframeViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
    iframeViews.value.push(
      Object.assign({}, view, {
        title: view.meta?.title || 'no-name'
      })
    );
  };
  const delIframeView = (view: TagView): Promise<TagView[]> => {
  const delIframeView = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
    return new Promise((resolve) => {
      iframeViews.value = iframeViews.value.filter((item) => item.path !== view.path);
      iframeViews.value = iframeViews.value.filter((item: RouteLocationNormalized) => item.path !== view.path);
      resolve([...iframeViews.value]);
    });
  };
  const addVisitedView = (view: TagView): void => {
    if (visitedViews.value.some((v) => v.path === view.path)) return;
  const addVisitedView = (view: RouteLocationNormalized): void => {
    if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
    visitedViews.value.push(
      Object.assign({}, view, {
        title: view.meta?.title || 'no-name'
      })
    );
  };
  const delView = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
  const delView = (
    view: RouteLocationNormalized
  ): Promise<{
    visitedViews: RouteLocationNormalized[];
    cachedViews: string[];
  }> => {
    return new Promise((resolve) => {
      delVisitedView(view);
      if (!isDynamicRoute(view)) {
@@ -45,7 +60,7 @@
    });
  };
  const delVisitedView = (view: TagView): Promise<TagView[]> => {
  const delVisitedView = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
    return new Promise((resolve) => {
      for (const [i, v] of visitedViews.value.entries()) {
        if (v.path === view.path) {
@@ -56,7 +71,7 @@
      resolve([...visitedViews.value]);
    });
  };
  const delCachedView = (view?: TagView): Promise<string[]> => {
  const delCachedView = (view?: RouteLocationNormalized): Promise<string[]> => {
    let viewName = '';
    if (view) {
      viewName = view.name as string;
@@ -67,7 +82,12 @@
      resolve([...cachedViews.value]);
    });
  };
  const delOthersViews = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
  const delOthersViews = (
    view: RouteLocationNormalized
  ): Promise<{
    visitedViews: RouteLocationNormalized[];
    cachedViews: string[];
  }> => {
    return new Promise((resolve) => {
      delOthersVisitedViews(view);
      delOthersCachedViews(view);
@@ -78,15 +98,15 @@
    });
  };
  const delOthersVisitedViews = (view: TagView): Promise<TagView[]> => {
  const delOthersVisitedViews = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
    return new Promise((resolve) => {
      visitedViews.value = visitedViews.value.filter((v) => {
      visitedViews.value = visitedViews.value.filter((v: RouteLocationNormalized) => {
        return v.meta?.affix || v.path === view.path;
      });
      resolve([...visitedViews.value]);
    });
  };
  const delOthersCachedViews = (view: TagView): Promise<string[]> => {
  const delOthersCachedViews = (view: RouteLocationNormalized): Promise<string[]> => {
    const viewName = view.name as string;
    return new Promise((resolve) => {
      const index = cachedViews.value.indexOf(viewName);
@@ -99,7 +119,7 @@
    });
  };
  const delAllViews = (): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
  const delAllViews = (): Promise<{ visitedViews: RouteLocationNormalized[]; cachedViews: string[] }> => {
    return new Promise((resolve) => {
      delAllVisitedViews();
      delAllCachedViews();
@@ -109,9 +129,9 @@
      });
    });
  };
  const delAllVisitedViews = (): Promise<TagView[]> => {
  const delAllVisitedViews = (): Promise<RouteLocationNormalized[]> => {
    return new Promise((resolve) => {
      visitedViews.value = visitedViews.value.filter((tag) => tag.meta?.affix);
      visitedViews.value = visitedViews.value.filter((tag: RouteLocationNormalized) => tag.meta?.affix);
      resolve([...visitedViews.value]);
    });
  };
@@ -123,7 +143,7 @@
    });
  };
  const updateVisitedView = (view: TagView): void => {
  const updateVisitedView = (view: RouteLocationNormalized): void => {
    for (let v of visitedViews.value) {
      if (v.path === view.path) {
        v = Object.assign(v, view);
@@ -131,13 +151,13 @@
      }
    }
  };
  const delRightTags = (view: TagView): Promise<TagView[]> => {
  const delRightTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
    return new Promise((resolve) => {
      const index = visitedViews.value.findIndex((v) => v.path === view.path);
      const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
      if (index === -1) {
        return;
      }
      visitedViews.value = visitedViews.value.filter((item, idx) => {
      visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
        if (idx <= index || (item.meta && item.meta.affix)) {
          return true;
        }
@@ -150,13 +170,13 @@
      resolve([...visitedViews.value]);
    });
  };
  const delLeftTags = (view: TagView): Promise<TagView[]> => {
  const delLeftTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
    return new Promise((resolve) => {
      const index = visitedViews.value.findIndex((v) => v.path === view.path);
      const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
      if (index === -1) {
        return;
      }
      visitedViews.value = visitedViews.value.filter((item, idx) => {
      visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
        if (idx >= index || (item.meta && item.meta.affix)) {
          return true;
        }
@@ -170,7 +190,7 @@
    });
  };
  const addCachedView = (view: TagView): void => {
  const addCachedView = (view: RouteLocationNormalized): void => {
    const viewName = view.name as string;
    if (!viewName) return;
    if (cachedViews.value.includes(viewName)) return;
@@ -179,15 +199,20 @@
    }
  };
  const isDynamicRoute = (view: any): boolean => {
  const isDynamicRoute = (view: RouteLocationNormalized): boolean => {
    // æ£€æŸ¥åŒ¹é…çš„路由记录中是否有动态段
    return view.matched.some((m: RouteRecordNormalized) => m.path.includes(':'));
    return view.matched.some((m) => m.path.includes(':'));
  };
  return {
    visitedViews,
    cachedViews,
    iframeViews,
    getVisitedViews,
    getIframeViews,
    getCachedViews,
    addVisitedView,
    addCachedView,
    delVisitedView,
@@ -205,5 +230,4 @@
    delIframeView
  };
});
export default useTagsViewStore;
src/store/modules/user.ts
@@ -1,9 +1,9 @@
import { to } from 'await-to-js';
import defAva from '@/assets/images/profile.jpg';
import store from '@/store';
import { getToken, removeToken, setToken } from '@/utils/auth';
import { login as loginApi, logout as logoutApi, getInfo as getUserInfo } from '@/api/login';
import { LoginData } from '@/api/types';
import defAva from '@/assets/images/profile.jpg';
import store from '@/store';
export const useUserStore = defineStore('user', () => {
  const token = ref(getToken());
src/types/axios.d.ts
@@ -1,7 +1,6 @@
import axios from 'axios';
export {};
declare module 'axios' {
  export interface AxiosResponse<T = any> {
  interface AxiosResponse<T = any> {
    code: number;
    msg: string;
    rows: T;
src/types/bpmn/editor/global.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
import { MessageApiInjection } from 'naive-ui/lib/message/src/MessageProvider';
declare global {
  interface Window {
    bpmnInstances: any;
    __messageBox: MessageApiInjection;
    URL: any;
  }
}
declare interface Window {
  bpmnInstances: any;
}
src/types/bpmn/index.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
declare module 'bpmn' {
  import type modeler from 'bpmn-js/lib/Modeler';
  import type modeling from 'bpmn-js/lib/features/modeling/Modeling';
  import type canvas from 'diagram-js/lib/core/Canvas';
  import type elementRegistry from 'diagram-js/lib/core/ElementRegistry';
  import type bpmnFactory from 'bpmn-js/lib/features/modeling/BpmnFactory';
  export type Modeler = modeler;
  export type Modeling = modeling;
  export type Canvas = canvas;
  export type ElementRegistry = elementRegistry;
  export type Moddle = import('moddle').Moddle;
  export type ModdleElement = import('moddle').ModdleElement;
  export type BpmnFactory = bpmnFactory;
}
src/types/bpmn/moddle.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
declare module 'moddle' {
  import type { Element as element } from 'bpmn-js/lib/model/Types';
  export type Element = {
    get<T>(name: string): T;
    set(name: string, value: any): void;
  } & element;
  export interface ModdleElement extends Element {
    $model: Moddle;
    readonly $type: string;
    $attrs: object | {};
    $parent: any;
    businessObject: ModdleElement;
    type: string;
    [field: string]: any;
    hasType(element: ModdleElement, type?: string): boolean;
  }
  export interface Package {
    name: string;
    prefix: string;
  }
  export interface Moddle {
    typeCache: Record<string, ModdleElement>;
    getPackage: typeof Registry.prototype.getPackage;
    getPackages: typeof Registry.prototype.getPackages;
    create(type: string, attrs?: any): ModdleElement;
  }
}
src/types/bpmn/panel.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
declare module 'bpmnDesign' {
  import { AllocationTypeEnum, SpecifyDescEnum, MultiInstanceTypeEnum } from '@/enums/bpmn/IndexEnums';
  export interface ParamVO {
    type: string;
    name: string;
    value: string;
  }
  export interface TaskListenerVO {
    event: string;
    type: string;
    name: string;
    className: string;
    params: ParamVO[];
  }
  export interface ExecutionListenerVO {
    event: string;
    type: string;
    className: string;
    params: ParamVO[];
  }
  interface BasePanel {
    id: string;
    name: string;
  }
  export interface ProcessPanel extends BasePanel {}
  export interface TaskPanel extends BasePanel {
    allocationType: AllocationTypeEnum;
    specifyDesc: SpecifyDescEnum;
    multiInstanceType: MultiInstanceTypeEnum;
    async?: boolean;
    priority?: number;
    formKey?: string;
    skipExpression?: string;
    isForCompensation?: boolean;
    triggerServiceTask?: boolean;
    autoStoreVariables?: boolean;
    ruleVariablesInput?: string;
    excludeTaskListener?: boolean;
    exclude?: boolean;
    class?: string;
    dueDate?: string;
    fixedAssignee?: string;
    candidateUsers?: string;
    assignee?: string;
    candidateGroups?: string;
    collection?: string;
    elementVariable?: string;
    completionCondition?: string;
    isSequential?: boolean;
    loopCharacteristics?: {
      collection: string;
      elementVariable: string;
      isSequential: boolean;
      completionCondition: {
        body: string;
      };
    };
  }
  export interface StartEndPanel extends BasePanel {}
  export interface GatewayPanel extends BasePanel {}
  export interface SequenceFlowPanel extends BasePanel {
    conditionExpression: {
      body: string;
    };
    conditionExpressionValue: string;
    skipExpression: string;
  }
  export interface ParticipantPanel extends BasePanel {}
  export interface SubProcessPanel extends BasePanel {
    multiInstanceType: MultiInstanceTypeEnum;
    collection?: string;
    elementVariable?: string;
    completionCondition?: string;
    loopCharacteristics?: {
      collection: string;
      elementVariable: string;
      isSequential: boolean;
      completionCondition: {
        body: string;
      };
    };
  }
}
src/types/element.d.ts
@@ -1,34 +1,35 @@
import type * as ep from 'element-plus';
declare global {
  declare type ElTagType = '' | 'success' | 'warning' | 'info' | 'danger' | 'default' | 'primary';
  declare type ElFormInstance = InstanceType<typeof ep.ElForm>;
  declare type ElTableInstance = InstanceType<typeof ep.ElTable>;
  declare type ElTagType = 'primary' | 'success' | 'info' | 'warning' | 'danger';
  declare type ElFormInstance = ep.FormInstance;
  declare type ElTableInstance = ep.TableInstance;
  declare type ElUploadInstance = ep.UploadInstance;
  declare type ElScrollbarInstance = ep.ScrollbarInstance;
  declare type ElInputInstance = ep.InputInstance;
  declare type ElInputNumberInstance = ep.InputNumberInstance;
  declare type ElRadioInstance = ep.RadioInstance;
  declare type ElRadioGroupInstance = ep.RadioGroupInstance;
  declare type ElRadioButtonInstance = ep.RadioButtonInstance;
  declare type ElCheckboxInstance = ep.CheckboxInstance;
  declare type ElSwitchInstance = ep.SwitchInstance;
  declare type ElCascaderInstance = ep.CascaderInstance;
  declare type ElColorPickerInstance = ep.ColorPickerInstance;
  declare type ElRateInstance = ep.RateInstance;
  declare type ElSliderInstance = ep.SliderInstance;
  declare type ElTreeInstance = InstanceType<typeof ep.ElTree>;
  declare type ElTreeSelectInstance = InstanceType<typeof ep.ElTreeSelect>;
  declare type ElSelectInstance = InstanceType<typeof ep.ElSelect>;
  declare type ElUploadInstance = InstanceType<typeof ep.ElUpload>;
  declare type ElCardInstance = InstanceType<typeof ep.ElCard>;
  declare type ElDialogInstance = InstanceType<typeof ep.ElDialog>;
  declare type ElInputInstance = InstanceType<typeof ep.ElInput>;
  declare type ElInputNumberInstance = InstanceType<typeof ep.ElInputNumber>;
  declare type ElRadioInstance = InstanceType<typeof ep.ElRadio>;
  declare type ElRadioGroupInstance = InstanceType<typeof ep.ElRadioGroup>;
  declare type ElRadioButtonInstance = InstanceType<typeof ep.ElRadioButton>;
  declare type ElCheckboxInstance = InstanceType<typeof ep.ElCheckbox>;
  declare type ElCheckboxGroupInstance = InstanceType<typeof ep.ElCheckboxGroup>;
  declare type ElSwitchInstance = InstanceType<typeof ep.ElSwitch>;
  declare type ElDatePickerInstance = InstanceType<typeof ep.ElDatePicker>;
  declare type ElTimePickerInstance = InstanceType<typeof ep.ElTimePicker>;
  declare type ElTimeSelectInstance = InstanceType<typeof ep.ElTimeSelect>;
  declare type ElCascaderInstance = InstanceType<typeof ep.ElCascader>;
  declare type ElColorPickerInstance = InstanceType<typeof ep.ElColorPicker>;
  declare type ElRateInstance = InstanceType<typeof ep.ElRate>;
  declare type ElSliderInstance = InstanceType<typeof ep.ElSlider>;
  declare type ElScrollbarInstance = InstanceType<typeof ep.ElScrollbar>;
  declare type TransferKey = ep.TransferKey;
  declare type CheckboxValueType = ep.CheckboxValueType;
  declare type ElFormRules = ep.FormRules;
  declare type DateModelType = ep.DateModelType;
  declare type UploadFile = typeof ep.UploadFile;
  declare type UploadFile = ep.UploadFile;
}
src/types/env.d.ts
@@ -1,62 +1,9 @@
declare module '*.vue' {
  import { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}
declare module '*.avif' {
  const src: string;
  export default src;
  const Component: DefineComponent<{}, {}, any>;
  export default Component;
}
declare module '*.bmp' {
  const src: string;
  export default src;
}
declare module '*.gif' {
  const src: string;
  export default src;
}
declare module '*.jpg' {
  const src: string;
  export default src;
}
declare module '*.jpeg' {
  const src: string;
  export default src;
}
declare module '*.png' {
  const src: string;
  export default src;
}
declare module '*.webp' {
  const src: string;
  export default src;
}
declare module '*.svg' {
  const src: string;
  export default src;
}
declare module '*.module.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}
declare module '*.module.scss' {
  const classes: { readonly [key: string]: string };
  export default classes;
}
declare module '*.module.sass' {
  const classes: { readonly [key: string]: string };
  export default classes;
}
// çŽ¯å¢ƒå˜é‡
interface ImportMetaEnv {
  VITE_APP_TITLE: string;
@@ -66,7 +13,9 @@
  VITE_APP_CONTEXT_PATH: string;
  VITE_APP_MONITRO_ADMIN: string;
  VITE_APP_POWERJOB_ADMIN: string;
  VITE_APP_EASYRETRY_ADMIN: string;
  VITE_APP_ENV: string;
  VITE_APP_ENCRYPT: string;
  VITE_APP_RSA_PUBLIC_KEY: string;
  VITE_APP_RSA_PRIVATE_KEY: string;
  VITE_APP_CLIENT_ID: string;
src/types/global.d.ts
@@ -1,4 +1,5 @@
import type { ComponentInternalInstance as ComponentInstance, PropType as VuePropType } from 'vue';
import type { PropType as VuePropType, ComponentInternalInstance as ComponentInstance } from 'vue';
import { LanguageEnum } from '@/enums/LanguageEnum';
declare global {
  /** vue Instance */
@@ -49,6 +50,8 @@
    /** æ˜¯å¦ç¦ç”¨ä¸Šä¼  */
    isUploading: boolean;
    updateSupport: number;
    /** å…¶ä»–参数 */
    [key: string]: any;
  }
@@ -87,5 +90,77 @@
    pageNum: number;
    pageSize: number;
  }
  declare interface LayoutSetting {
    /**
     * æ˜¯å¦æ˜¾ç¤ºé¡¶éƒ¨å¯¼èˆª
     */
    topNav: boolean;
    /**
     * æ˜¯å¦æ˜¾ç¤ºå¤šæ ‡ç­¾å¯¼èˆª
     */
    tagsView: boolean;
    /**
     * æ˜¯å¦å›ºå®šå¤´éƒ¨
     */
    fixedHeader: boolean;
    /**
     * æ˜¯å¦æ˜¾ç¤ºä¾§è¾¹æ Logo
     */
    sidebarLogo: boolean;
    /**
     * æ˜¯å¦æ˜¾ç¤ºåŠ¨æ€æ ‡é¢˜
     */
    dynamicTitle: boolean;
    /**
     * ä¾§è¾¹æ ä¸»é¢˜ theme-dark | theme-light
     */
    sideTheme: string;
    /**
     * ä¸»é¢˜æ¨¡å¼
     */
    theme: string;
  }
  declare interface DefaultSettings extends LayoutSetting {
    /**
     * ç½‘页标题
     */
    title: string;
    /**
     * æ˜¯å¦æ˜¾ç¤ºç³»ç»Ÿå¸ƒå±€è®¾ç½®
     */
    showSettings: boolean;
    /**
     * å¯¼èˆªæ å¸ƒå±€
     */
    layout: string;
    /**
     * å¸ƒå±€å¤§å°
     */
    size: 'large' | 'default' | 'small';
    /**
     * è¯­è¨€
     */
    language: LanguageEnum;
    /**
     * æ˜¯å¦å¯ç”¨åŠ¨ç”»æ•ˆæžœ
     */
    animationEnable: boolean;
    /**
     *  æ˜¯å¦å¯ç”¨æš—黑模式
     *
     * true:暗黑模式
     * false: æ˜Žäº®æ¨¡å¼
     */
    dark: boolean;
    errorLog: string;
  }
}
export {};
src/types/module.d.ts
@@ -1,15 +1,17 @@
import type modal from '@/plugins/modal';
import type tab from '@/plugins/tab';
import type download from '@/plugins/download';
import type auth from '@/plugins/auth';
import type cache from '@/plugins/cache';
import type animate from '@/animate';
import type { useDict } from '@/utils/dict';
import type { addDateRange, handleTree, selectDictLabel, selectDictLabels, parseTime } from '@/utils/ruoyi';
import type { getConfigKey, updateConfigByKey } from '@/api/system/config';
import type { download as rd } from '@/utils/request';
import modal from '@/plugins/modal';
import tab from '@/plugins/tab';
import download from '@/plugins/download';
import auth from '@/plugins/auth';
import cache from '@/plugins/cache';
import animate from '@/animate';
import { useDict } from '@/utils/dict';
import { handleTree, addDateRange, selectDictLabel, selectDictLabels, parseTime } from '@/utils/ruoyi';
import { getConfigKey, updateConfigByKey } from '@/api/system/config';
import { download as rd } from '@/utils/request';
declare module '@vue/runtime-core' {
export {};
declare module 'vue' {
  interface ComponentCustomProperties {
    // å…¨å±€æ–¹æ³•声明
    $modal: typeof modal;
@@ -30,3 +32,8 @@
    parseTime: typeof parseTime;
  }
}
declare module 'vform3-builds' {
  const content: any;
  export = content;
}
src/types/router.d.ts
@@ -1,36 +1,38 @@
import { RouteRecordRaw } from 'vue-router';
import { LocationQuery, type RouteMeta as VRouteMeta } from 'vue-router';
declare module 'vue-router' {
  declare type RouteOption = {
    hidden?: boolean;
  interface RouteMeta extends VRouteMeta {
    link?: string;
    title?: string;
    affix?: boolean;
    noCache?: boolean;
    activeMenu?: string;
    icon?: string;
    breadcrumb?: boolean;
  }
  interface _RouteRecordBase {
    hidden?: boolean | string | number;
    permissions?: string[];
    roles?: string[];
    component?: any;
    children?: RouteOption[];
    alwaysShow?: boolean;
    parentPath?: string;
    meta?: {
      title: string;
      icon: string;
    };
    query?: string;
  } & RouteRecordRaw;
  declare interface _RouteLocationBase {
    children?: RouteOption[];
    parentPath?: string;
  }
  declare interface RouteLocationOptions {
    fullPath?: string;
  }
  declare interface TagView extends Partial<_RouteLocationBase> {
  interface _RouteLocationBase {
    children?: _RouteRecordBase[];
    path?: string;
    title?: string;
    meta?: {
      link?: string;
      title?: string;
      affix?: boolean;
      noCache?: boolean;
    };
  }
  interface TagView {
    fullPath?: string;
    name?: string;
    path?: string;
    title?: string;
    meta?: RouteMeta;
    query?: LocationQuery;
  }
}
export {};
src/types/setting.d.ts
ÎļþÒÑɾ³ý
src/types/vform3-builds.d.ts
ÎļþÒÑɾ³ý
src/utils/index.ts
@@ -89,7 +89,7 @@
 * @returns {Array}
 */
export const cleanArray = (actual: Array<any>) => {
  const newArray = [];
  const newArray: any[] = [];
  for (let i = 0; i < actual.length; i++) {
    if (actual[i]) {
      newArray.push(actual[i]);
src/utils/propTypes.ts
@@ -3,6 +3,7 @@
type PropTypes = VueTypesInterface & {
  readonly style: VueTypeValidableDef<CSSProperties>;
  readonly fieldOption: VueTypeValidableDef<Array<FieldOption>>;
};
const propTypes = createTypes({
src/utils/request.ts
@@ -36,11 +36,12 @@
    // å¯¹åº”国际化资源文件后缀
    config.headers['Content-Language'] = getLanguage();
    const isToken = (config.headers || {}).isToken === false;
    const isToken = config.headers?.isToken === false;
    // æ˜¯å¦éœ€è¦é˜²æ­¢æ•°æ®é‡å¤æäº¤
    const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
    const isRepeatSubmit = config.headers?.repeatSubmit === false;
    // æ˜¯å¦éœ€è¦åР坆
    const isEncrypt = (config.headers || {}).isEncrypt === 'true';
    const isEncrypt = config.headers?.isEncrypt === 'true';
    if (getToken() && !isToken) {
      config.headers['Authorization'] = 'Bearer ' + getToken(); // è®©æ¯ä¸ªè¯·æ±‚携带自定义token è¯·æ ¹æ®å®žé™…情况自行修改
    }
@@ -75,12 +76,14 @@
        }
      }
    }
    // å½“开启参数加密
    if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
      // ç”Ÿæˆä¸€ä¸ª AES å¯†é’¥
      const aesKey = generateAesKey();
      config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
      config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
    if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
      // å½“开启参数加密
      if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
        // ç”Ÿæˆä¸€ä¸ª AES å¯†é’¥
        const aesKey = generateAesKey();
        config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
        config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
      }
    }
    // FormData数据去请求头Content-Type
    if (config.data instanceof FormData) {
@@ -89,7 +92,6 @@
    return config;
  },
  (error: any) => {
    console.log(error);
    return Promise.reject(error);
  }
);
@@ -97,19 +99,21 @@
// å“åº”拦截器
service.interceptors.response.use(
  (res: AxiosResponse) => {
    // åŠ å¯†åŽçš„ AES ç§˜é’¥
    const keyStr = res.headers[encryptHeader];
    // åР坆
    if (keyStr != null && keyStr != '') {
      const data = res.data;
      // è¯·æ±‚体 AES è§£å¯†
      const base64Str = decrypt(keyStr);
      // base64 è§£ç  å¾—到请求头的 AES ç§˜é’¥
      const aesKey = decryptBase64(base64Str.toString());
      // aesKey è§£ç  data
      const decryptData = decryptWithAes(data, aesKey);
      // å°†ç»“æžœ (得到的是 JSON å­—符串) è½¬ä¸º JSON
      res.data = JSON.parse(decryptData);
    if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
      // åŠ å¯†åŽçš„ AES ç§˜é’¥
      const keyStr = res.headers[encryptHeader];
      // åР坆
      if (keyStr != null && keyStr != '') {
        const data = res.data;
        // è¯·æ±‚体 AES è§£å¯†
        const base64Str = decrypt(keyStr);
        // base64 è§£ç  å¾—到请求头的 AES ç§˜é’¥
        const aesKey = decryptBase64(base64Str.toString());
        // aesKey è§£ç  data
        const decryptData = decryptWithAes(data, aesKey);
        // å°†ç»“æžœ (得到的是 JSON å­—符串) è½¬ä¸º JSON
        res.data = JSON.parse(decryptData);
      }
    }
    // æœªè®¾ç½®çŠ¶æ€ç åˆ™é»˜è®¤æˆåŠŸçŠ¶æ€
    const code = res.data.code || HttpStatus.SUCCESS;
@@ -138,7 +142,6 @@
      }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
    } else if (code === HttpStatus.SERVER_ERROR) {
      console.log(msg);
      ElMessage({ message: msg, type: 'error' });
      return Promise.reject(new Error(msg));
    } else if (code === HttpStatus.WARN) {
src/utils/ruoyi.ts
@@ -68,7 +68,7 @@
  if (value === undefined) {
    return '';
  }
  const actions = [];
  const actions: Array<string | number> = [];
  Object.keys(datas).some((key) => {
    if (datas[key].value == '' + value) {
      actions.push(datas[key].label);
@@ -245,3 +245,7 @@
export const blobValidate = (data: any) => {
  return data.type !== 'application/json';
};
export default {
  handleTree
};
src/utils/websocket.ts
@@ -19,10 +19,8 @@
 */
import { getToken } from '@/utils/auth';
import { ElNotification } from 'element-plus';
import useNoticeStore from '@/store/modules/notice';
import { ElNotification } from "element-plus";
const { addNotice } = useNoticeStore();
let socketUrl: any = ''; // socket地址
let websocket: any = null; // websocket å®žä¾‹
@@ -125,7 +123,7 @@
    if (e.data.indexOf('ping') > 0) {
      return;
    }
    addNotice({
    useNoticeStore().addNotice({
      message: e.data,
      read: false,
      time: new Date().toLocaleString()
@@ -135,7 +133,7 @@
      message: e.data,
      type: 'success',
      duration: 3000
    })
    });
    return e.data;
  };
};
src/views/demo/demo/index.vue
@@ -1,23 +1,23 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="部门id" prop="deptId">
              <el-input v-model="queryParams.deptId" placeholder="请输入部门id" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.deptId" placeholder="请输入部门id" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="用户id" prop="userId">
              <el-input v-model="queryParams.userId" placeholder="请输入用户id" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.userId" placeholder="请输入用户id" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="排序号" prop="orderNum">
              <el-input v-model="queryParams.orderNum" placeholder="请输入排序号" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.orderNum" placeholder="请输入排序号" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="key键" prop="testKey">
              <el-input v-model="queryParams.testKey" placeholder="请输入key键" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.testKey" placeholder="请输入key键" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="值" prop="value">
              <el-input v-model="queryParams.value" placeholder="请输入值" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.value" placeholder="请输入值" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -32,26 +32,26 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['demo:demo:add']">新增</el-button>
            <el-button v-hasPermi="['demo:demo:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['demo:demo:edit']">修改</el-button>
            <el-button v-hasPermi="['demo:demo:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['demo:demo:remove']"
            <el-button v-hasPermi="['demo:demo:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
              >删除</el-button
            >
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['demo:demo:export']">导出</el-button>
            <el-button v-hasPermi="['demo:demo:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table v-loading="loading" :data="demoList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="主键" align="center" prop="id" v-if="true" />
        <el-table-column v-if="true" label="主键" align="center" prop="id" />
        <el-table-column label="部门id" align="center" prop="deptId" />
        <el-table-column label="用户id" align="center" prop="userId" />
        <el-table-column label="排序号" align="center" prop="orderNum" />
@@ -60,19 +60,19 @@
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['demo:demo:edit']"></el-button>
              <el-button v-hasPermi="['demo:demo:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['demo:demo:remove']"></el-button>
              <el-button v-hasPermi="['demo:demo:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹æµ‹è¯•å•å¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
      <el-form ref="demoFormRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="部门id" prop="deptId">
          <el-input v-model="form.deptId" placeholder="请输入部门id" />
@@ -129,8 +129,8 @@
  userId: undefined,
  orderNum: undefined,
  testKey: undefined,
  value: undefined,
}
  value: undefined
};
const data = reactive<PageData<DemoForm, DemoQuery>>({
  form: { ...initFormData },
  queryParams: {
@@ -140,27 +140,15 @@
    userId: undefined,
    orderNum: undefined,
    testKey: undefined,
    value: undefined,
    value: undefined
  },
  rules: {
    id: [
      { required: true, message: "主键不能为空", trigger: "blur" }
    ],
    deptId: [
      { required: true, message: "部门id不能为空", trigger: "blur" }
    ],
    userId: [
      { required: true, message: "用户id不能为空", trigger: "blur" }
    ],
    orderNum: [
      { required: true, message: "排序号不能为空", trigger: "blur" }
    ],
    testKey: [
      { required: true, message: "key键不能为空", trigger: "blur" }
    ],
    value: [
      { required: true, message: "值不能为空", trigger: "blur" }
    ],
    id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
    deptId: [{ required: true, message: '部门id不能为空', trigger: 'blur' }],
    userId: [{ required: true, message: '用户id不能为空', trigger: 'blur' }],
    orderNum: [{ required: true, message: '排序号不能为空', trigger: 'blur' }],
    testKey: [{ required: true, message: 'key键不能为空', trigger: 'blur' }],
    value: [{ required: true, message: '值不能为空', trigger: 'blur' }]
  }
});
@@ -173,55 +161,55 @@
  demoList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** å–消按钮 */
const cancel = () => {
  reset();
  dialog.visible = false;
}
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  demoFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: DemoVO[]) => {
  ids.value = selection.map(item => item.id);
  ids.value = selection.map((item) => item.id);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = () => {
  reset();
  dialog.visible = true;
  dialog.title = "添加测试单";
}
  dialog.title = '添加测试单';
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row?: DemoVO) => {
  reset();
  const _id = row?.id || ids.value[0]
  const _id = row?.id || ids.value[0];
  const res = await getDemo(_id);
  Object.assign(form.value, res.data);
  dialog.visible = true;
  dialog.title = "修改测试单";
}
  dialog.title = '修改测试单';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
@@ -229,32 +217,36 @@
    if (valid) {
      buttonLoading.value = true;
      if (form.value.id) {
        await updateDemo(form.value).finally(() => buttonLoading.value = false);
        await updateDemo(form.value).finally(() => (buttonLoading.value = false));
      } else {
        await addDemo(form.value).finally(() => buttonLoading.value = false);
        await addDemo(form.value).finally(() => (buttonLoading.value = false));
      }
      proxy?.$modal.msgSuccess("修改成功");
      proxy?.$modal.msgSuccess('修改成功');
      dialog.visible = false;
      await getList();
    }
  });
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: DemoVO) => {
  const _ids = row?.id || ids.value;
  await proxy?.$modal.confirm('是否确认删除测试单编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
  await proxy?.$modal.confirm('是否确认删除测试单编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
  await delDemo(_ids);
  proxy?.$modal.msgSuccess("删除成功");
  proxy?.$modal.msgSuccess('删除成功');
  await getList();
}
};
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
const handleExport = () => {
  proxy?.download('demo/demo/export', {
    ...queryParams.value
  }, `demo_${new Date().getTime()}.xlsx`)
}
  proxy?.download(
    'demo/demo/export',
    {
      ...queryParams.value
    },
    `demo_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
  getList();
src/views/demo/tree/index.vue
@@ -1,11 +1,11 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="树节点名" prop="treeName">
              <el-input v-model="queryParams.treeName" placeholder="请输入树节点名" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.treeName" placeholder="请输入树节点名" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -20,21 +20,21 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['demo:tree:add']">新增</el-button>
            <el-button v-hasPermi="['demo:tree:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table
        ref="treeTableRef"
        v-loading="loading"
        :data="treeList"
        row-key="id"
        :default-expand-all="isExpandAll"
        :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
        ref="treeTableRef"
        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
      >
        <el-table-column label="父id" align="center" prop="parentId" />
        <el-table-column label="部门id" align="center" prop="deptId" />
@@ -43,20 +43,20 @@
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['demo:tree:edit']" />
              <el-button v-hasPermi="['demo:tree:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
            </el-tooltip>
            <el-tooltip content="新增" placement="top">
              <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['demo:tree:add']" />
              <el-button v-hasPermi="['demo:tree:add']" link type="primary" icon="Plus" @click="handleAdd(scope.row)" />
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['demo:tree:remove']" />
              <el-button v-hasPermi="['demo:tree:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)" />
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹æµ‹è¯•æ ‘å¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
      <el-form ref="treeFormRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="父id" prop="parentId">
          <el-tree-select
@@ -89,18 +89,16 @@
</template>
<script setup name="Tree" lang="ts">
import { listTree, getTree, delTree, addTree, updateTree } from "@/api/demo/tree";
import { listTree, getTree, delTree, addTree, updateTree } from '@/api/demo/tree';
import { TreeVO, TreeQuery, TreeForm } from '@/api/demo/tree/types';
type TreeOption = {
  id: number;
  treeName: string;
  children?: TreeOption[];
}
};
const { proxy } = getCurrentInstance() as ComponentInternalInstance;;
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const treeList = ref<TreeVO[]>([]);
const treeOptions = ref<TreeOption[]>([]);
@@ -111,46 +109,35 @@
const queryFormRef = ref<ElFormInstance>();
const treeFormRef = ref<ElFormInstance>();
const treeTableRef = ref<ElTableInstance>()
const treeTableRef = ref<ElTableInstance>();
const dialog = reactive<DialogOption>({
    visible: false,
    title: ''
  visible: false,
  title: ''
});
const initFormData: TreeForm = {
    id: undefined,
    parentId: undefined,
    deptId: undefined,
    userId: undefined,
    treeName: undefined,
}
  id: undefined,
  parentId: undefined,
  deptId: undefined,
  userId: undefined,
  treeName: undefined
};
const data = reactive<PageData<TreeForm, TreeQuery>>({
  form: {...initFormData},
  form: { ...initFormData },
  queryParams: {
    parentId: undefined,
    deptId: undefined,
    userId: undefined,
    treeName: undefined,
    treeName: undefined
  },
  rules: {
    id: [
      { required: true, message: "主键不能为空", trigger: "blur" }
    ],
    parentId: [
      { required: true, message: "父id不能为空", trigger: "blur" }
    ],
    deptId: [
      { required: true, message: "部门id不能为空", trigger: "blur" }
    ],
    userId: [
      { required: true, message: "用户id不能为空", trigger: "blur" }
    ],
    treeName: [
      { required: true, message: "值不能为空", trigger: "blur" }
    ],
    id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
    parentId: [{ required: true, message: '父id不能为空', trigger: 'blur' }],
    deptId: [{ required: true, message: '部门id不能为空', trigger: 'blur' }],
    userId: [{ required: true, message: '用户id不能为空', trigger: 'blur' }],
    treeName: [{ required: true, message: '值不能为空', trigger: 'blur' }]
  }
});
@@ -160,44 +147,44 @@
const getList = async () => {
  loading.value = true;
  const res = await listTree(queryParams.value);
  const data = proxy?.handleTree<TreeVO>(res.data, "id", "parentId");
  const data = proxy?.handleTree<TreeVO>(res.data, 'id', 'parentId');
  if (data) {
    treeList.value = data;
    loading.value = false;
  }
}
};
/** æŸ¥è¯¢æµ‹è¯•树下拉树结构 */
const getTreeselect = async () => {
  const res = await listTree();
  treeOptions.value = [];
  const data: TreeOption = { id: 0, treeName: '顶级节点', children: [] };
  data.children = proxy?.handleTree<TreeOption>(res.data, "id", "parentId");
  data.children = proxy?.handleTree<TreeOption>(res.data, 'id', 'parentId');
  treeOptions.value.push(data);
}
};
// å–消按钮
const cancel = () => {
  reset();
  dialog.visible = false;
}
};
// è¡¨å•重置
const reset = () => {
  form.value = {...initFormData}
  form.value = { ...initFormData };
  treeFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = (row?: TreeVO) => {
@@ -209,22 +196,22 @@
    form.value.parentId = 0;
  }
  dialog.visible = true;
  dialog.title = "添加测试树";
}
  dialog.title = '添加测试树';
};
/** å±•å¼€/折叠操作 */
const handleToggleExpandAll = () => {
  isExpandAll.value = !isExpandAll.value;
  toggleExpandAll(treeList.value, isExpandAll.value)
}
  toggleExpandAll(treeList.value, isExpandAll.value);
};
/** å±•å¼€/折叠操作 */
const toggleExpandAll = (data: TreeVO[], status: boolean) => {
  data.forEach((item) => {
    treeTableRef.value?.toggleRowExpansion(item, status)
    if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
  })
}
    treeTableRef.value?.toggleRowExpansion(item, status);
    if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
  });
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row: TreeVO) => {
@@ -236,8 +223,8 @@
  const res = await getTree(row.id);
  Object.assign(form.value, res.data);
  dialog.visible = true;
  dialog.title = "修改测试树";
}
  dialog.title = '修改测试树';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
@@ -245,25 +232,25 @@
    if (valid) {
      buttonLoading.value = true;
      if (form.value.id) {
        await updateTree(form.value).finally(() => buttonLoading.value = false);
        await updateTree(form.value).finally(() => (buttonLoading.value = false));
      } else {
        await addTree(form.value).finally(() => buttonLoading.value = false);
        await addTree(form.value).finally(() => (buttonLoading.value = false));
      }
      proxy?.$modal.msgSuccess("操作成功");
      proxy?.$modal.msgSuccess('操作成功');
      dialog.visible = false;
      await getList();
    }
  });
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row: TreeVO) => {
  await proxy?.$modal.confirm('是否确认删除测试树编号为"' + row.id + '"的数据项?');
  loading.value = true;
  await delTree(row.id).finally(() => loading.value = false);
  await delTree(row.id).finally(() => (loading.value = false));
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
onMounted(() => {
  getList();
src/views/error/401.vue
@@ -24,11 +24,11 @@
let { proxy } = getCurrentInstance() as ComponentInternalInstance;
const errGif = ref(errImage + "?" + +new Date());
const errGif = ref(errImage + '?' + +new Date());
function back() {
  if (proxy?.$route.query.noGoBack) {
    proxy.$router.push({ path: "/" });
    proxy.$router.push({ path: '/' });
  } else {
    proxy?.$router.go(-1);
  }
src/views/error/404.vue
@@ -23,13 +23,13 @@
<script setup lang="ts">
let message = computed(() => {
  return '找不到网页!'
})
  return '找不到网页!';
});
</script>
<style lang="scss" scoped>
.wscn-http404-container{
  transform: translate(-50%,-50%);
.wscn-http404-container {
  transform: translate(-50%, -50%);
  position: absolute;
  top: 40%;
  left: 50%;
src/views/index.vue
@@ -21,7 +21,7 @@
          * åˆ†å¸ƒå¼é” Lock4j æ³¨è§£é”ã€å·¥å…·é” å¤šç§å¤šæ ·<br />
          * åˆ†å¸ƒå¼å¹‚ç­‰ Lock4j åŸºäºŽåˆ†å¸ƒå¼é”å®žçް<br />
          * åˆ†å¸ƒå¼é“¾è·¯è¿½è¸ª SkyWalking æ”¯æŒé“¾è·¯è¿½è¸ªã€ç½‘格分析、度量聚合、可视化<br />
          * åˆ†å¸ƒå¼ä»»åŠ¡è°ƒåº¦ PowerJob é«˜æ€§èƒ½ é«˜å¯é  æ˜“扩展<br />
          * åˆ†å¸ƒå¼ä»»åŠ¡è°ƒåº¦ SnailJob é«˜æ€§èƒ½ é«˜å¯é  æ˜“扩展<br />
          * æ–‡ä»¶å­˜å‚¨ Minio æœ¬åœ°å­˜å‚¨<br />
          * æ–‡ä»¶å­˜å‚¨ ä¸ƒç‰›ã€é˜¿é‡Œã€è…¾è®¯ äº‘存储<br />
          * ç›‘控框架 SpringBoot-Admin å…¨æ–¹ä½æœåŠ¡ç›‘æŽ§<br />
@@ -33,7 +33,7 @@
          * éƒ¨ç½²æ–¹å¼ Docker å®¹å™¨ç¼–排 ä¸€é”®éƒ¨ç½²ä¸šåŠ¡é›†ç¾¤<br />
          * å›½é™…化 SpringMessage Spring标准国际化方案<br />
        </p>
        <p><b>当前版本:</b> <span>v5.1.2</span></p>
        <p><b>当前版本:</b> <span>v5.2.0-BETA</span></p>
        <p>
          <el-tag type="danger">&yen;免费开源</el-tag>
        </p>
@@ -78,7 +78,7 @@
          * åˆ†å¸ƒå¼ç›‘控 Prometheus、Grafana å…¨æ–¹ä½æ€§èƒ½ç›‘控<br />
          * å…¶ä½™ä¸Ž Vue ç‰ˆæœ¬ä¸€è‡´<br />
        </p>
        <p><b>当前版本:</b> <span>v2.1.2</span></p>
        <p><b>当前版本:</b> <span>v2.2.0-BETA</span></p>
        <p>
          <el-tag type="danger">&yen;免费开源</el-tag>
        </p>
@@ -96,16 +96,9 @@
</template>
<script setup name="Index" lang="ts">
import { initWebSocket } from '@/utils/websocket';
onMounted(() => {
  let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'
  initWebSocket(protocol + window.location.host + import.meta.env.VITE_APP_BASE_API + "/resource/websocket");
});
const goTarget = (url:string) => {
  window.open(url, '__blank')
}
const goTarget = (url: string) => {
  window.open(url, '__blank');
};
</script>
<style scoped lang="scss">
@@ -131,7 +124,7 @@
    margin: 0;
  }
  font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-size: 13px;
  color: #676a6c;
  overflow-x: hidden;
src/views/login.vue
@@ -2,7 +2,7 @@
  <div class="login">
    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">RuoYi-Vue-Plus多租户管理系统</h3>
      <el-form-item prop="tenantId" v-if="tenantEnabled">
      <el-form-item v-if="tenantEnabled" prop="tenantId">
        <el-select v-model="loginForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
          <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
@@ -18,21 +18,24 @@
          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="code" v-if="captchaEnabled">
      <el-form-item v-if="captchaEnabled" prop="code">
        <el-input v-model="loginForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleLogin">
          <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
        </el-input>
        <div class="login-code">
          <img :src="codeUrl" @click="getCode" class="login-code-img" />
          <img :src="codeUrl" class="login-code-img" @click="getCode" />
        </div>
      </el-form-item>
      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
      <el-form-item style="float: right;">
      <el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">记住密码</el-checkbox>
      <el-form-item style="float: right">
        <el-button circle title="微信登录" @click="doSocialLogin('wechat')">
          <svg-icon icon-class="wechat" />
        </el-button>
        <el-button circle title="MaxKey登录" @click="doSocialLogin('maxkey')">
          <svg-icon icon-class="maxkey" />
        </el-button>
        <el-button circle title="TopIam登录" @click="doSocialLogin('topiam')">
          <svg-icon icon-class="topiam" />
        </el-button>
        <el-button circle title="Gitee登录" @click="doSocialLogin('gitee')">
          <svg-icon icon-class="gitee" />
@@ -41,19 +44,19 @@
          <svg-icon icon-class="github" />
        </el-button>
      </el-form-item>
      <el-form-item style="width:100%;">
        <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin">
      <el-form-item style="width: 100%">
        <el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
          <span v-if="!loading">登 å½•</span>
          <span v-else>登 å½• ä¸­...</span>
        </el-button>
        <div style="float: right;" v-if="register">
        <div v-if="register" style="float: right">
          <router-link class="link-type" :to="'/register'">立即注册</router-link>
        </div>
      </el-form-item>
    </el-form>
    <!--  åº•部  -->
    <div class="el-login-footer">
      <span>Copyright Â© 2018-2023 ç–¯ç‹‚的狮子Li All Rights Reserved.</span>
      <span>Copyright Â© 2018-2024 ç–¯ç‹‚的狮子Li All Rights Reserved.</span>
    </div>
  </div>
</template>
@@ -64,7 +67,7 @@
import { useUserStore } from '@/store/modules/user';
import { LoginData, TenantVO } from '@/api/types';
import { to } from 'await-to-js';
import { HttpStatus } from "@/enums/RespEnum";
import { HttpStatus } from '@/enums/RespEnum';
const userStore = useUserStore();
const router = useRouter();
@@ -79,7 +82,7 @@
} as LoginData);
const loginRules: ElFormRules = {
  tenantId: [{ required: true, trigger: "blur", message: "请输入您的租户编号" }],
  tenantId: [{ required: true, trigger: 'blur', message: '请输入您的租户编号' }],
  username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
  password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
  code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
@@ -92,7 +95,6 @@
// ç§Ÿæˆ·å¼€å…³
const tenantEnabled = ref(true);
// æ³¨å†Œå¼€å…³
const register = ref(false);
const redirect = ref(undefined);
@@ -100,9 +102,13 @@
// ç§Ÿæˆ·åˆ—表
const tenantList = ref<TenantVO[]>([]);
watch(() => router.currentRoute.value, (newRoute: any) => {
  redirect.value = newRoute.query && newRoute.query.redirect;
}, { immediate: true });
watch(
  () => router.currentRoute.value,
  (newRoute: any) => {
    redirect.value = newRoute.query && newRoute.query.redirect;
  },
  { immediate: true }
);
const handleLogin = () => {
  loginRef.value?.validate(async (valid: boolean, fields: any) => {
@@ -110,13 +116,13 @@
      loading.value = true;
      // å‹¾é€‰äº†éœ€è¦è®°ä½å¯†ç è®¾ç½®åœ¨ localStorage ä¸­è®¾ç½®è®°ä½ç”¨æˆ·åå’Œå¯†ç 
      if (loginForm.value.rememberMe) {
        localStorage.setItem("tenantId", String(loginForm.value.tenantId));
        localStorage.setItem('tenantId', String(loginForm.value.tenantId));
        localStorage.setItem('username', String(loginForm.value.username));
        localStorage.setItem('password', String(loginForm.value.password));
        localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
      } else {
        // å¦åˆ™ç§»é™¤
        localStorage.removeItem("tenantId");
        localStorage.removeItem('tenantId');
        localStorage.removeItem('username');
        localStorage.removeItem('password');
        localStorage.removeItem('rememberMe');
@@ -124,7 +130,8 @@
      // è°ƒç”¨action的登录方法
      const [err] = await to(userStore.login(loginForm.value));
      if (!err) {
        await router.push({ path: redirect.value || '/' });
        const redirectUrl = redirect.value || '/';
        await router.push(redirectUrl);
        loading.value = false;
      } else {
        loading.value = false;
@@ -153,7 +160,7 @@
};
const getLoginData = () => {
  const tenantId = localStorage.getItem("tenantId");
  const tenantId = localStorage.getItem('tenantId');
  const username = localStorage.getItem('username');
  const password = localStorage.getItem('password');
  const rememberMe = localStorage.getItem('rememberMe');
@@ -163,8 +170,7 @@
    password: password === null ? String(loginForm.value.password) : String(password),
    rememberMe: rememberMe === null ? false : Boolean(rememberMe)
  } as LoginData;
}
};
/**
 * èŽ·å–ç§Ÿæˆ·åˆ—è¡¨
@@ -178,12 +184,15 @@
      loginForm.value.tenantId = tenantList.value[0].tenantId;
    }
  }
}
};
//检测租户选择框的变化
watch(() => loginForm.value.tenantId, () => {
  localStorage.setItem("tenantId", String(loginForm.value.tenantId))
});
watch(
  () => loginForm.value.tenantId,
  () => {
    localStorage.setItem('tenantId', String(loginForm.value.tenantId));
  }
);
/**
 * ç¬¬ä¸‰æ–¹ç™»å½•
@@ -200,8 +209,6 @@
  });
};
onMounted(() => {
  getCode();
  initTenantList();
@@ -215,7 +222,7 @@
  justify-content: center;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/login-background.jpg");
  background-image: url('../assets/images/login-background.jpg');
  background-size: cover;
}
src/views/monitor/cache/index.vue
@@ -4,8 +4,8 @@
      <el-col :span="24" class="card-box">
        <el-card shadow="hover">
          <template #header>
            <Monitor style="width: 1em; height: 1em; vertical-align: middle;" />
            <span style="vertical-align: middle;">基本信息</span>
            <Monitor style="width: 1em; height: 1em; vertical-align: middle" />
            <span style="vertical-align: middle">基本信息</span>
          </template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
@@ -16,25 +16,25 @@
                    <div class="cell">Redis版本</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.redis_version }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">运行模式</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.redis_mode === "standalone" ? "单机" : "集群" }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.redis_mode === 'standalone' ? '单机' : '集群' }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">端口</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.tcp_port }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">客户端数</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.connected_clients }}</div>
                  </td>
                </tr>
                <tr>
@@ -42,25 +42,25 @@
                    <div class="cell">运行时间(天)</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.uptime_in_days }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">使用内存</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.used_memory_human }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">使用CPU</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div>
                    <div v-if="cache.info" class="cell">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">内存配置</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.maxmemory_human }}</div>
                  </td>
                </tr>
                <tr>
@@ -68,25 +68,25 @@
                    <div class="cell">AOF是否开启</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.aof_enabled === "0" ? "否" : "是" }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.aof_enabled === '0' ? '否' : '是' }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">RDB是否成功</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div>
                    <div v-if="cache.info" class="cell">{{ cache.info.rdb_last_bgsave_status }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">Key数量</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.dbSize">{{ cache.dbSize }}</div>
                    <div v-if="cache.dbSize" class="cell">{{ cache.dbSize }}</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell">网络入口/出口</div>
                  </td>
                  <td class="el-table__cell is-leaf">
                    <div class="cell" v-if="cache.info">
                    <div v-if="cache.info" class="cell">
                      {{ cache.info.instantaneous_input_kbps }}kps/{{ cache.info.instantaneous_output_kbps }}kps
                    </div>
                  </td>
@@ -100,8 +100,8 @@
      <el-col :span="12" class="card-box">
        <el-card shadow="hover">
          <template #header>
            <PieChart style="width: 1em; height: 1em; vertical-align: middle;" />
            <span style="vertical-align: middle;">命令统计</span>
            <PieChart style="width: 1em; height: 1em; vertical-align: middle" />
            <span style="vertical-align: middle">命令统计</span>
          </template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <div ref="commandstats" style="height: 420px" />
@@ -112,7 +112,7 @@
      <el-col :span="12" class="card-box">
        <el-card shadow="hover">
          <template #header>
            <Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">内存信息</span>
            <Odometer style="width: 1em; height: 1em; vertical-align: middle" /> <span style="vertical-align: middle">内存信息</span>
          </template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <div ref="usedmemory" style="height: 420px" />
@@ -126,45 +126,46 @@
<script setup name="Cache" lang="ts">
import { getCache } from '@/api/monitor/cache';
import * as echarts from 'echarts';
import { CacheVO } from '@/api/monitor/cache/types';
const cache = ref<any>({});
const cache = ref<Partial<CacheVO>>({});
const commandstats = ref();
const usedmemory = ref();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const getList = async () => {
  proxy?.$modal.loading("正在加载缓存监控数据,请稍候!");
  proxy?.$modal.loading('正在加载缓存监控数据,请稍候!');
  const res = await getCache();
  proxy?.$modal.closeLoading();
  cache.value = res.data;
  const commandstatsIntance = echarts.init(commandstats.value, "macarons");
  const commandstatsIntance = echarts.init(commandstats.value, 'macarons');
  commandstatsIntance.setOption({
    tooltip: {
      trigger: "item",
      formatter: "{a} <br/>{b} : {c} ({d}%)"
      trigger: 'item',
      formatter: '{a} <br/>{b} : {c} ({d}%)'
    },
    series: [
      {
        name: "命令",
        type: "pie",
        roseType: "radius",
        name: '命令',
        type: 'pie',
        roseType: 'radius',
        radius: [15, 95],
        center: ["50%", "38%"],
        center: ['50%', '38%'],
        data: res.data.commandStats,
        animationEasing: "cubicInOut",
        animationEasing: 'cubicInOut',
        animationDuration: 1000
      }
    ]
  });
  const usedmemoryInstance = echarts.init(usedmemory.value, "macarons");
  const usedmemoryInstance = echarts.init(usedmemory.value, 'macarons');
  usedmemoryInstance.setOption({
    tooltip: {
      formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human
      formatter: '{b} <br/>{a} : ' + cache.value.info.used_memory_human
    },
    series: [
      {
        name: "峰值",
        type: "gauge",
        name: '峰值',
        type: 'gauge',
        min: 0,
        max: 1000,
        detail: {
@@ -173,19 +174,19 @@
        data: [
          {
            value: parseFloat(cache.value.info.used_memory_human),
            name: "内存消耗"
            name: '内存消耗'
          }
        ]
      }
    ]
  })
  window.addEventListener("resize",()=>{
    commandstatsIntance.resize()
    usedmemoryInstance.resize()
  });
}
  window.addEventListener('resize', () => {
    commandstatsIntance.resize();
    usedmemoryInstance.resize();
  });
};
onMounted(() => {
  getList();
})
});
</script>
src/views/monitor/logininfor/index.vue
@@ -1,17 +1,17 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="登录地址" prop="ipaddr">
              <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable style="width: 240px;" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="用户名称" prop="userName">
              <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px;" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="状态" prop="status">
              <el-select v-model="queryParams.status" placeholder="登录状态" clearable style="width: 240px">
              <el-select v-model="queryParams.status" placeholder="登录状态" clearable >
                <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
              </el-select>
            </el-form-item>
@@ -39,22 +39,22 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['monitor:logininfor:remove']">
            <el-button v-hasPermi="['monitor:logininfor:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" @click="handleClean" v-hasPermi="['monitor:logininfor:remove']">清空</el-button>
            <el-button v-hasPermi="['monitor:logininfor:remove']" type="danger" plain icon="Delete" @click="handleClean">清空</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Unlock" :disabled="single" @click="handleUnlock" v-hasPermi="['monitor:logininfor:unlock']">
            <el-button v-hasPermi="['monitor:logininfor:unlock']" type="primary" plain icon="Unlock" :disabled="single" @click="handleUnlock">
              è§£é”
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['monitor:logininfor:export']">导出</el-button>
            <el-button v-hasPermi="['monitor:logininfor:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
@@ -62,8 +62,8 @@
        ref="loginInfoTableRef"
        v-loading="loading"
        :data="loginInfoList"
        @selection-change="handleSelectionChange"
        :default-sort="defaultSort"
        @selection-change="handleSelectionChange"
        @sort-change="handleSortChange"
      >
        <el-table-column type="selection" width="55" align="center" />
@@ -99,18 +99,18 @@
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
  </div>
</template>
<script setup name="Logininfor" lang="ts">
import { list, delLoginInfo, cleanLoginInfo, unlockLoginInfo } from "@/api/monitor/loginInfo";
import { LoginInfoQuery, LoginInfoVO } from "@/api/monitor/loginInfo/types";
import { list, delLoginInfo, cleanLoginInfo, unlockLoginInfo } from '@/api/monitor/loginInfo';
import { LoginInfoQuery, LoginInfoVO } from '@/api/monitor/loginInfo/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type"));
const { sys_common_status } = toRefs<any>(proxy?.useDict("sys_common_status"));
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
const { sys_common_status } = toRefs<any>(proxy?.useDict('sys_common_status'));
const loginInfoList = ref<LoginInfoVO[]>([]);
const loading = ref(true);
@@ -120,85 +120,89 @@
const multiple = ref(true);
const selectName = ref<Array<string>>([]);
const total = ref(0);
const dateRange = ref<[DateModelType,DateModelType]>(['', '']);
const defaultSort = ref<any>({ prop: "loginTime", order: "descending" });
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const defaultSort = ref<any>({ prop: 'loginTime', order: 'descending' });
const queryFormRef = ref<ElFormInstance>();
const loginInfoTableRef = ref<ElTableInstance>();
// æŸ¥è¯¢å‚æ•°
const queryParams = ref<LoginInfoQuery>({
    pageNum: 1,
    pageSize: 10,
    ipaddr: '',
    userName: '',
    status: '',
    orderByColumn: defaultSort.value.prop,
    isAsc: defaultSort.value.order
  pageNum: 1,
  pageSize: 10,
  ipaddr: '',
  userName: '',
  status: '',
  orderByColumn: defaultSort.value.prop,
  isAsc: defaultSort.value.order
});
/** æŸ¥è¯¢ç™»å½•日志列表 */
const getList = async () => {
    loading.value = true;
    const res = await list(proxy?.addDateRange(queryParams.value, dateRange.value));
    loginInfoList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
  loading.value = true;
  const res = await list(proxy?.addDateRange(queryParams.value, dateRange.value));
  loginInfoList.value = res.rows;
  total.value = res.total;
  loading.value = false;
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
    queryParams.value.pageNum = 1;
    getList();
}
  queryParams.value.pageNum = 1;
  getList();
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
    dateRange.value = ['', ''];
    queryFormRef.value?.resetFields();
    queryParams.value.pageNum = 1;
    loginInfoTableRef.value?.sort(defaultSort.value.prop, defaultSort.value.order);
}
  dateRange.value = ['', ''];
  queryFormRef.value?.resetFields();
  queryParams.value.pageNum = 1;
  loginInfoTableRef.value?.sort(defaultSort.value.prop, defaultSort.value.order);
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: LoginInfoVO[]) => {
    ids.value = selection.map(item => item.infoId);
    multiple.value = !selection.length;
    single.value = selection.length != 1;
    selectName.value = selection.map(item => item.userName);
}
  ids.value = selection.map((item) => item.infoId);
  multiple.value = !selection.length;
  single.value = selection.length != 1;
  selectName.value = selection.map((item) => item.userName);
};
/** æŽ’序触发事件 */
const handleSortChange = (column: any) => {
    queryParams.value.orderByColumn = column.prop;
    queryParams.value.isAsc = column.order;
    getList();
}
  queryParams.value.orderByColumn = column.prop;
  queryParams.value.isAsc = column.order;
  getList();
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: LoginInfoVO) => {
    const infoIds = row?.infoId || ids.value;
    await proxy?.$modal.confirm('是否确认删除访问编号为"' + infoIds + '"的数据项?');
    await delLoginInfo(infoIds);
    await getList();
    proxy?.$modal.msgSuccess("删除成功");
}
  const infoIds = row?.infoId || ids.value;
  await proxy?.$modal.confirm('是否确认删除访问编号为"' + infoIds + '"的数据项?');
  await delLoginInfo(infoIds);
  await getList();
  proxy?.$modal.msgSuccess('删除成功');
};
/** æ¸…空按钮操作 */
const handleClean = async () => {
    await proxy?.$modal.confirm("是否确认清空所有登录日志数据项?");
    await cleanLoginInfo();
    await getList();
    proxy?.$modal.msgSuccess("清空成功");
}
  await proxy?.$modal.confirm('是否确认清空所有登录日志数据项?');
  await cleanLoginInfo();
  await getList();
  proxy?.$modal.msgSuccess('清空成功');
};
/** è§£é”æŒ‰é’®æ“ä½œ */
const handleUnlock = async () => {
    const username = selectName.value;
    await proxy?.$modal.confirm('是否确认解锁用户"' + username + '"数据项?');
    await unlockLoginInfo(username);
    proxy?.$modal.msgSuccess("用户" + username + "解锁成功");
}
  const username = selectName.value;
  await proxy?.$modal.confirm('是否确认解锁用户"' + username + '"数据项?');
  await unlockLoginInfo(username);
  proxy?.$modal.msgSuccess('用户' + username + '解锁成功');
};
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
const handleExport = () => {
    proxy?.download("monitor/logininfor/export", {
        ...queryParams.value,
    }, `config_${new Date().getTime()}.xlsx`);
}
  proxy?.download(
    'monitor/logininfor/export',
    {
      ...queryParams.value
    },
    `config_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
    getList();
})
  getList();
});
</script>
src/views/monitor/online/index.vue
@@ -2,12 +2,12 @@
  <div class="p-2">
    <div class="mb-[10px]">
      <el-card shadow="hover">
        <el-form :model="queryParams" ref="queryFormRef" :inline="true">
        <el-form ref="queryFormRef" :model="queryParams" :inline="true">
          <el-form-item label="登录地址" prop="ipaddr">
            <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable style="width: 200px" @keyup.enter="handleQuery" />
            <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable @keyup.enter="handleQuery" />
          </el-form-item>
          <el-form-item label="用户名称" prop="userName">
            <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
            <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -20,7 +20,7 @@
      <el-table
        v-loading="loading"
        :data="onlineList.slice((queryParams.pageNum - 1) * queryParams.pageSize, queryParams.pageNum * queryParams.pageSize)"
        style="width: 100%;"
        style="width: 100%"
      >
        <el-table-column label="序号" width="50" type="index" align="center">
          <template #default="scope">
@@ -48,26 +48,26 @@
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="强退" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleForceLogout(scope.row)" v-hasPermi="['monitor:online:forceLogout']">
              <el-button v-hasPermi="['monitor:online:forceLogout']" link type="primary" icon="Delete" @click="handleForceLogout(scope.row)">
              </el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" />
    </el-card>
  </div>
</template>
<script setup name="Online" lang="ts">
import { forceLogout, list as initData } from "@/api/monitor/online";
import { OnlineQuery, OnlineVO } from "@/api/monitor/online/types";
import api from "@/api/system/user";
import {to} from "await-to-js";
import { forceLogout, list as initData } from '@/api/monitor/online';
import { OnlineQuery, OnlineVO } from '@/api/monitor/online/types';
import api from '@/api/system/user';
import { to } from 'await-to-js';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type"));
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
const onlineList = ref<OnlineVO[]>([]);
const loading = ref(true);
@@ -89,28 +89,28 @@
  onlineList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** å¼ºé€€æŒ‰é’®æ“ä½œ */
const handleForceLogout = async (row: OnlineVO) => {
  const [err] = await to(proxy?.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?') as any);
  if (!err) {
    await forceLogout(row.tokenId);
    await getList();
    proxy?.$modal.msgSuccess("删除成功");
    proxy?.$modal.msgSuccess('删除成功');
  }
}
};
onMounted(() => {
  getList();
})
});
</script>
src/views/monitor/operlog/index.vue
@@ -1,25 +1,25 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="操作地址" prop="operIp">
              <el-input v-model="queryParams.operIp" placeholder="请输入操作地址" clearable style="width: 240px;" @keyup.enter="handleQuery"/>
              <el-input v-model="queryParams.operIp" placeholder="请输入操作地址" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="系统模块" prop="title">
              <el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable style="width: 240px;" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="操作人员" prop="operName">
              <el-input v-model="queryParams.operName" placeholder="请输入操作人员" clearable style="width: 240px;" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.operName" placeholder="请输入操作人员" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="类型" prop="businessType">
              <el-select v-model="queryParams.businessType" placeholder="操作类型" clearable style="width: 240px">
              <el-select v-model="queryParams.businessType" placeholder="操作类型" clearable >
                <el-option v-for="dict in sys_oper_type" :key="dict.value" :label="dict.label" :value="dict.value" />
              </el-select>
            </el-form-item>
            <el-form-item label="状态" prop="status">
              <el-select v-model="queryParams.status" placeholder="操作状态" clearable style="width: 240px">
              <el-select v-model="queryParams.status" placeholder="操作状态" clearable >
                <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
              </el-select>
            </el-form-item>
@@ -47,17 +47,17 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['monitor:operlog:remove']">
            <el-button v-hasPermi="['monitor:operlog:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="WarnTriangleFilled" @click="handleClean" v-hasPermi="['monitor:operlog:remove']">清空</el-button>
            <el-button v-hasPermi="['monitor:operlog:remove']" type="danger" plain icon="WarnTriangleFilled" @click="handleClean">清空</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['monitor:operlog:export']">导出</el-button>
            <el-button v-hasPermi="['monitor:operlog:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
@@ -65,8 +65,8 @@
        ref="operLogTableRef"
        v-loading="loading"
        :data="operlogList"
        @selection-change="handleSelectionChange"
        :default-sort="defaultSort"
        @selection-change="handleSelectionChange"
        @sort-change="handleSortChange"
      >
        <el-table-column type="selection" width="50" align="center" />
@@ -114,20 +114,20 @@
        <el-table-column label="操作" fixed="right" align="center" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="详细" placement="top">
              <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['monitor:operlog:query']"> </el-button>
              <el-button v-hasPermi="['monitor:operlog:query']" link type="primary" icon="View" @click="handleView(scope.row)"> </el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ“ä½œæ—¥å¿—详细 -->
    <el-dialog title="操作日志详细" v-model="dialog.visible" width="700px" append-to-body>
    <el-dialog v-model="dialog.visible" title="操作日志详细" width="700px" append-to-body>
      <el-form :model="form" label-width="100px">
        <el-row>
          <el-col :span="24">
            <el-form-item label="登录信息:">{{ form.operName }} / {{form.deptName}} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
            <el-form-item label="登录信息:">{{ form.operName }} / {{ form.deptName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="请求信息:">{{ form.requestMethod }} {{ form.operUrl }}</el-form-item>
@@ -157,7 +157,7 @@
            <el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
          </el-col>
          <el-col :span="24">
            <el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
            <el-form-item v-if="form.status === 1" label="异常信息:">{{ form.errorMsg }}</el-form-item>
          </el-col>
        </el-row>
      </el-form>
@@ -175,7 +175,7 @@
import { OperLogForm, OperLogQuery, OperLogVO } from '@/api/monitor/operlog/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_oper_type, sys_common_status } = toRefs<any>(proxy?.useDict("sys_oper_type", "sys_common_status"));
const { sys_oper_type, sys_common_status } = toRefs<any>(proxy?.useDict('sys_oper_type', 'sys_common_status'));
const operlogList = ref<OperLogVO[]>([]);
const loading = ref(true);
@@ -184,7 +184,7 @@
const multiple = ref(true);
const total = ref(0);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const defaultSort = ref<any>({ prop: "operTime", order: "descending" });
const defaultSort = ref<any>({ prop: 'operTime', order: 'descending' });
const operLogTableRef = ref<ElTableInstance>();
const queryFormRef = ref<ElFormInstance>();
@@ -193,7 +193,6 @@
  visible: false,
  title: ''
});
const data = reactive<PageData<OperLogForm, OperLogQuery>>({
  form: {
@@ -240,63 +239,67 @@
  operlogList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** æ“ä½œæ—¥å¿—类型字典翻译 */
const typeFormat = (row: OperLogForm) => {
  return proxy?.selectDictLabel(sys_oper_type.value, row.businessType);
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value?.resetFields();
  queryParams.value.pageNum = 1;
  operLogTableRef.value?.sort(defaultSort.value.prop, defaultSort.value.order);
}
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: OperLogVO[]) => {
  ids.value = selection.map(item => item.operId);
  ids.value = selection.map((item) => item.operId);
  multiple.value = !selection.length;
}
};
/** æŽ’序触发事件 */
const handleSortChange = (column: any) => {
  queryParams.value.orderByColumn = column.prop;
  queryParams.value.isAsc = column.order;
  getList();
}
};
/** è¯¦ç»†æŒ‰é’®æ“ä½œ */
const handleView = (row: OperLogVO) => {
  dialog.visible = true;
  form.value = row;
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: OperLogVO) => {
  const operIds = row?.operId || ids.value;
  await proxy?.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?');
  await delOperlog(operIds);
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
/** æ¸…空按钮操作 */
const handleClean = async () => {
  await proxy?.$modal.confirm("是否确认清空所有操作日志数据项?");
  await proxy?.$modal.confirm('是否确认清空所有操作日志数据项?');
  await cleanOperlog();
  await getList();
  proxy?.$modal.msgSuccess("清空成功");
}
  proxy?.$modal.msgSuccess('清空成功');
};
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
const handleExport = () => {
  proxy?.download("monitor/operlog/export", {
    ...queryParams.value,
  }, `config_${new Date().getTime()}.xlsx`);
}
  proxy?.download(
    'monitor/operlog/export',
    {
      ...queryParams.value
    },
    `config_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
  getList();
})
});
</script>
src/views/monitor/snailjob/index.vue
ÎļþÃû´Ó src/views/monitor/powerjob/index.vue ÐÞ¸Ä
@@ -5,5 +5,5 @@
</template>
<script setup lang="ts">
const url = ref(import.meta.env.VITE_APP_POWERJOB_ADMIN);
const url = ref(import.meta.env.VITE_APP_SNAILJOB_ADMIN);
</script>
src/views/redirect/index.vue
@@ -3,12 +3,12 @@
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const { params, query } = route
const { path } = params
const { params, query } = route;
const { path } = params;
router.replace({ path: '/' + path, query })
router.replace({ path: '/' + path, query });
</script>
src/views/register.vue
@@ -2,7 +2,7 @@
  <div class="register">
    <el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
      <h3 class="title">RuoYi-Vue-Plus多租户管理系统</h3>
      <el-form-item prop="tenantId" v-if="tenantEnabled">
      <el-form-item v-if="tenantEnabled" prop="tenantId">
        <el-select v-model="registerForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
          <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
@@ -30,27 +30,27 @@
          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="code" v-if="captchaEnabled">
        <el-input size="large" v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleRegister">
      <el-form-item v-if="captchaEnabled" prop="code">
        <el-input v-model="registerForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleRegister">
          <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
        </el-input>
        <div class="register-code">
          <img :src="codeUrl" @click="getCode" class="register-code-img" />
          <img :src="codeUrl" class="register-code-img" @click="getCode" />
        </div>
      </el-form-item>
      <el-form-item style="width:100%;">
        <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleRegister">
      <el-form-item style="width: 100%">
        <el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleRegister">
          <span v-if="!loading">注 å†Œ</span>
          <span v-else>注 å†Œ ä¸­...</span>
        </el-button>
        <div style="float: right;">
        <div style="float: right">
          <router-link class="link-type" :to="'/login'">使用已有账户登录</router-link>
        </div>
      </el-form-item>
    </el-form>
    <!--  åº•部  -->
    <div class="el-register-footer">
      <span>Copyright Â© 2018-2023 ç–¯ç‹‚的狮子Li All Rights Reserved.</span>
      <span>Copyright Â© 2018-2024 ç–¯ç‹‚的狮子Li All Rights Reserved.</span>
    </div>
  </div>
</template>
@@ -63,46 +63,44 @@
const router = useRouter();
const registerForm = ref<RegisterForm>({
  tenantId: "",
  username: "",
  password: "",
  confirmPassword: "",
  code: "",
  uuid: "",
  userType: "sys_user"
  tenantId: '',
  username: '',
  password: '',
  confirmPassword: '',
  code: '',
  uuid: '',
  userType: 'sys_user'
});
// ç§Ÿæˆ·å¼€å…³
const tenantEnabled = ref(true);
const equalToPassword = (rule: any, value: string, callback: any) => {
  if (registerForm.value.password !== value) {
    callback(new Error("两次输入的密码不一致"));
    callback(new Error('两次输入的密码不一致'));
  } else {
    callback();
  }
};
const registerRules: ElFormRules = {
  tenantId: [
    { required: true, trigger: "blur", message: "请输入您的租户编号" }
  ],
  tenantId: [{ required: true, trigger: 'blur', message: '请输入您的租户编号' }],
  username: [
    { required: true, trigger: "blur", message: "请输入您的账号" },
    { min: 2, max: 20, message: "用户账号长度必须介于 2 å’Œ 20 ä¹‹é—´", trigger: "blur" }
    { required: true, trigger: 'blur', message: '请输入您的账号' },
    { min: 2, max: 20, message: '用户账号长度必须介于 2 å’Œ 20 ä¹‹é—´', trigger: 'blur' }
  ],
  password: [
    { required: true, trigger: "blur", message: "请输入您的密码" },
    { min: 5, max: 20, message: "用户密码长度必须介于 5 å’Œ 20 ä¹‹é—´", trigger: "blur" }
    { required: true, trigger: 'blur', message: '请输入您的密码' },
    { min: 5, max: 20, message: '用户密码长度必须介于 5 å’Œ 20 ä¹‹é—´', trigger: 'blur' },
    { pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\\ |', trigger: 'blur' }
  ],
  confirmPassword: [
    { required: true, trigger: "blur", message: "请再次输入您的密码" },
    { required: true, validator: equalToPassword, trigger: "blur" }
    { required: true, trigger: 'blur', message: '请再次输入您的密码' },
    { required: true, validator: equalToPassword, trigger: 'blur' }
  ],
  code: [{ required: true, trigger: "change", message: "请输入验证码" }]
  code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
};
const codeUrl = ref("");
const codeUrl = ref('');
const loading = ref(false);
const captchaEnabled = ref(true);
const registerRef = ref<ElFormInstance>();
@@ -116,20 +114,20 @@
      const [err] = await to(register(registerForm.value));
      if (!err) {
        const username = registerForm.value.username;
        await ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + username + " æ³¨å†ŒæˆåŠŸï¼</font>", "系统提示", {
        await ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + username + ' æ³¨å†ŒæˆåŠŸï¼</font>', '系统提示', {
          dangerouslyUseHTMLString: true,
          type: "success",
          type: 'success'
        });
        await router.push("/login");
        await router.push('/login');
      } else {
        loading.value = false;
        if (captchaEnabled) {
        if (captchaEnabled.value) {
          getCode();
        }
      }
    }
  });
}
};
const getCode = async () => {
  const res = await getCodeImg();
@@ -150,12 +148,12 @@
      registerForm.value.tenantId = tenantList.value[0].tenantId;
    }
  }
}
};
onMounted(() => {
  getCode();
  initTenantList();
})
});
</script>
<style lang="scss" scoped>
@@ -164,7 +162,7 @@
  justify-content: center;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/login-background.jpg");
  background-image: url('../assets/images/login-background.jpg');
  background-size: cover;
}
src/views/system/client/index.vue
@@ -1,16 +1,16 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="search" v-show="showSearch">
        <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px">
      <div v-show="showSearch" class="search">
        <el-form ref="queryFormRef" :model="queryParams" :inline="true">
          <el-form-item label="客户端key" prop="clientKey">
            <el-input v-model="queryParams.clientKey" placeholder="请输入客户端key" clearable style="width: 240px" @keyup.enter="handleQuery" />
            <el-input v-model="queryParams.clientKey" placeholder="请输入客户端key" clearable @keyup.enter="handleQuery" />
          </el-form-item>
          <el-form-item label="客户端秘钥" prop="clientSecret">
            <el-input v-model="queryParams.clientSecret" placeholder="请输入客户端秘钥" clearable style="width: 240px" @keyup.enter="handleQuery" />
            <el-input v-model="queryParams.clientSecret" placeholder="请输入客户端秘钥" clearable @keyup.enter="handleQuery" />
          </el-form-item>
          <el-form-item label="状态" prop="status">
            <el-select v-model="queryParams.status" placeholder="状态" clearable>
            <el-select v-model="queryParams.status" placeholder="状态" clearable >
              <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
            </el-select>
          </el-form-item>
@@ -26,28 +26,28 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:client:add']">新增</el-button>
            <el-button v-hasPermi="['system:client:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:client:edit']">
            <el-button v-hasPermi="['system:client:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">
              ä¿®æ”¹
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:client:remove']">
            <el-button v-hasPermi="['system:client:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:client:export']">导出</el-button>
            <el-button v-hasPermi="['system:client:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table v-loading="loading" :data="clientList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="id" align="center" prop="id" v-if="true" />
        <el-table-column v-if="true" label="id" align="center" prop="id" />
        <el-table-column label="客户端id" align="center" prop="clientId" />
        <el-table-column label="客户端key" align="center" prop="clientKey" />
        <el-table-column label="客户端秘钥" align="center" prop="clientSecret" />
@@ -63,7 +63,7 @@
        </el-table-column>
        <el-table-column label="Token活跃超时时间" align="center" prop="activeTimeout" />
        <el-table-column label="Token固定超时时间" align="center" prop="timeout" />
        <el-table-column label="状态" align="center" key="status">
        <el-table-column key="status" label="状态" align="center">
          <template #default="scope">
            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
          </template>
@@ -71,19 +71,19 @@
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:client:edit']"></el-button>
              <el-button v-hasPermi="['system:client:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:client:remove']"></el-button>
              <el-button v-hasPermi="['system:client:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total>0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å®¢æˆ·ç«¯ç®¡ç†å¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
      <el-form ref="clientFormRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="客户端key" prop="clientKey">
          <el-input v-model="form.clientKey" :disabled="form.id != null" placeholder="请输入客户端key" />
@@ -125,7 +125,7 @@
        </el-form-item>
        <el-form-item label="状态">
          <el-radio-group v-model="form.status">
            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">
            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">
              {{ dict.label }}
            </el-radio>
          </el-radio-group>
@@ -146,9 +146,9 @@
import { ClientVO, ClientQuery, ClientForm } from '@/api/system/client/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
const { sys_grant_type } = toRefs<any>(proxy?.useDict("sys_grant_type"));
const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type"));
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const { sys_grant_type } = toRefs<any>(proxy?.useDict('sys_grant_type'));
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
const clientList = ref<ClientVO[]>([]);
const buttonLoading = ref(false);
@@ -176,10 +176,10 @@
  deviceType: undefined,
  activeTimeout: undefined,
  timeout: undefined,
  status: undefined,
}
  status: undefined
};
const data = reactive<PageData<ClientForm, ClientQuery>>({
  form: {...initFormData},
  form: { ...initFormData },
  queryParams: {
    pageNum: 1,
    pageSize: 10,
@@ -190,27 +190,15 @@
    deviceType: undefined,
    activeTimeout: undefined,
    timeout: undefined,
    status: undefined,
    status: undefined
  },
  rules: {
    id: [
      { required: true, message: "id不能为空", trigger: "blur" }
    ],
    clientId: [
      { required: true, message: "客户端id不能为空", trigger: "blur" }
    ],
    clientKey: [
      { required: true, message: "客户端key不能为空", trigger: "blur" }
    ],
    clientSecret: [
      { required: true, message: "客户端秘钥不能为空", trigger: "blur" }
    ],
    grantTypeList: [
      { required: true, message: "授权类型不能为空", trigger: "change" }
    ],
    deviceType: [
      { required: true, message: "设备类型不能为空", trigger: "change" }
    ],
    id: [{ required: true, message: 'id不能为空', trigger: 'blur' }],
    clientId: [{ required: true, message: '客户端id不能为空', trigger: 'blur' }],
    clientKey: [{ required: true, message: '客户端key不能为空', trigger: 'blur' }],
    clientSecret: [{ required: true, message: '客户端秘钥不能为空', trigger: 'blur' }],
    grantTypeList: [{ required: true, message: '授权类型不能为空', trigger: 'change' }],
    deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }]
  }
});
@@ -223,55 +211,55 @@
  clientList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** å–消按钮 */
const cancel = () => {
  reset();
  dialog.visible = false;
}
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = {...initFormData};
  form.value = { ...initFormData };
  clientFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: ClientVO[]) => {
  ids.value = selection.map(item => item.id);
  ids.value = selection.map((item) => item.id);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = () => {
  reset();
  dialog.visible = true;
  dialog.title = "添加客户端管理";
}
  dialog.title = '添加客户端管理';
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row?: ClientVO) => {
  reset();
  const _id = row?.id || ids.value[0]
  const _id = row?.id || ids.value[0];
  const res = await getClient(_id);
  Object.assign(form.value, res.data);
  dialog.visible = true;
  dialog.title = "修改客户端管理";
}
  dialog.title = '修改客户端管理';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
@@ -279,44 +267,48 @@
    if (valid) {
      buttonLoading.value = true;
      if (form.value.id) {
        await updateClient(form.value).finally(() =>  buttonLoading.value = false);
        await updateClient(form.value).finally(() => (buttonLoading.value = false));
      } else {
        await addClient(form.value).finally(() =>  buttonLoading.value = false);
        await addClient(form.value).finally(() => (buttonLoading.value = false));
      }
      proxy?.$modal.msgSuccess("修改成功");
      proxy?.$modal.msgSuccess('修改成功');
      dialog.visible = false;
      await getList();
    }
  });
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: ClientVO) => {
  const _ids = row?.id || ids.value;
  await proxy?.$modal.confirm('是否确认删除客户端管理编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
  await proxy?.$modal.confirm('是否确认删除客户端管理编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
  await delClient(_ids);
  proxy?.$modal.msgSuccess("删除成功");
  proxy?.$modal.msgSuccess('删除成功');
  await getList();
}
};
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
const handleExport = () => {
  proxy?.download('system/client/export', {
    ...queryParams.value
  }, `client_${new Date().getTime()}.xlsx`)
}
  proxy?.download(
    'system/client/export',
    {
      ...queryParams.value
    },
    `client_${new Date().getTime()}.xlsx`
  );
};
/** çŠ¶æ€ä¿®æ”¹  */
const handleStatusChange = async (row: ClientVO) => {
  let text = row.status === "0" ? "启用" : "停用"
  let text = row.status === '0' ? '启用' : '停用';
  try {
    await proxy?.$modal.confirm('确认要"' + text + '"吗?');
    await changeStatus(row.id, row.status);
    proxy?.$modal.msgSuccess(text + "成功");
    await changeStatus(row.clientId, row.status);
    proxy?.$modal.msgSuccess(text + '成功');
  } catch (err) {
    row.status = row.status === "0" ? "1" : "0";
    row.status = row.status === '0' ? '1' : '0';
  }
}
};
onMounted(() => {
  getList();
src/views/system/config/index.vue
@@ -1,21 +1,21 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="参数名称" prop="configName">
              <el-input v-model="queryParams.configName" placeholder="请输入参数名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.configName" placeholder="请输入参数名称" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="参数键名" prop="configKey">
              <el-input v-model="queryParams.configKey" placeholder="请输入参数键名" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.configKey" placeholder="请输入参数键名" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="系统内置" prop="configType">
              <el-select v-model="queryParams.configType" placeholder="系统内置" clearable>
              <el-select v-model="queryParams.configType" placeholder="系统内置" clearable >
                <el-option v-for="dict in sys_yes_no" :key="dict.value" :label="dict.label" :value="dict.value" />
              </el-select>
            </el-form-item>
            <el-form-item label="创建时间" style="width: 308px;">
            <el-form-item label="创建时间" style="width: 308px">
              <el-date-picker
                v-model="dateRange"
                value-format="YYYY-MM-DD HH:mm:ss"
@@ -38,31 +38,31 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:config:add']">新增</el-button>
            <el-button v-hasPermi="['system:config:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:config:edit']">
            <el-button v-hasPermi="['system:config:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">
              ä¿®æ”¹
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:config:remove']">
            <el-button v-hasPermi="['system:config:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:config:export']">导出</el-button>
            <el-button v-hasPermi="['system:config:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Refresh" @click="handleRefreshCache" v-hasPermi="['system:config:remove']">刷新缓存</el-button>
            <el-button v-hasPermi="['system:config:remove']" type="danger" plain icon="Refresh" @click="handleRefreshCache">刷新缓存</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="参数主键" align="center" prop="configId" v-if="false" />
        <el-table-column v-if="false" label="参数主键" align="center" prop="configId" />
        <el-table-column label="参数名称" align="center" prop="configName" :show-overflow-tooltip="true" />
        <el-table-column label="参数键名" align="center" prop="configKey" :show-overflow-tooltip="true" />
        <el-table-column label="参数键值" align="center" prop="configValue" :show-overflow-tooltip="true" />
@@ -80,19 +80,19 @@
        <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:config:edit']"></el-button>
              <el-button v-hasPermi="['system:config:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:config:remove']"></el-button>
              <el-button v-hasPermi="['system:config:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
      <el-form ref="configFormRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="参数名称" prop="configName">
          <el-input v-model="form.configName" placeholder="请输入参数名称" />
@@ -105,7 +105,7 @@
        </el-form-item>
        <el-form-item label="系统内置" prop="configType">
          <el-radio-group v-model="form.configType">
            <el-radio v-for="dict in sys_yes_no" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
            <el-radio v-for="dict in sys_yes_no" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
@@ -123,11 +123,11 @@
</template>
<script setup name="Config" lang="ts">
import { listConfig, getConfig, delConfig, addConfig, updateConfig, refreshCache } from "@/api/system/config";
import { ConfigForm, ConfigQuery, ConfigVO } from "@/api/system/config/types";
import { listConfig, getConfig, delConfig, addConfig, updateConfig, refreshCache } from '@/api/system/config';
import { ConfigForm, ConfigQuery, ConfigVO } from '@/api/system/config/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_yes_no } = toRefs<any>(proxy?.useDict("sys_yes_no"));
const { sys_yes_no } = toRefs<any>(proxy?.useDict('sys_yes_no'));
const configList = ref<ConfigVO[]>([]);
const loading = ref(true);
@@ -149,9 +149,9 @@
  configName: '',
  configKey: '',
  configValue: '',
  configType: "Y",
  configType: 'Y',
  remark: ''
}
};
const data = reactive<PageData<ConfigForm, ConfigQuery>>({
  form: { ...initFormData },
  queryParams: {
@@ -159,12 +159,12 @@
    pageSize: 10,
    configName: '',
    configKey: '',
    configType: '',
    configType: ''
  },
  rules: {
    configName: [{ required: true, message: "参数名称不能为空", trigger: "blur" }],
    configKey: [{ required: true, message: "参数键名不能为空", trigger: "blur" }],
    configValue: [{ required: true, message: "参数键值不能为空", trigger: "blur" }]
    configName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
    configKey: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
    configValue: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }]
  }
});
@@ -177,40 +177,40 @@
  configList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** å–消按钮 */
const cancel = () => {
  reset();
  dialog.visible = false;
}
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  configFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: ConfigVO[]) => {
  ids.value = selection.map(item => item.configId);
  ids.value = selection.map((item) => item.configId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = () => {
  reset();
  dialog.visible = true;
  dialog.title = "添加参数";
}
  dialog.title = '添加参数';
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row?: ConfigVO) => {
  reset();
@@ -218,40 +218,44 @@
  const res = await getConfig(configId);
  Object.assign(form.value, res.data);
  dialog.visible = true;
  dialog.title = "修改参数";
}
  dialog.title = '修改参数';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  configFormRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      form.value.configId ? await updateConfig(form.value) : await addConfig(form.value);
      proxy?.$modal.msgSuccess("操作成功");
      proxy?.$modal.msgSuccess('操作成功');
      dialog.visible = false;
      await getList();
    }
  });
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: ConfigVO) => {
  const configIds = row?.configId || ids.value;
  await proxy?.$modal.confirm('是否确认删除参数编号为"' + configIds + '"的数据项?');
  await delConfig(configIds);
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
const handleExport = () => {
  proxy?.download("system/config/export", {
    ...queryParams.value
  }, `config_${new Date().getTime()}.xlsx`);
}
  proxy?.download(
    'system/config/export',
    {
      ...queryParams.value
    },
    `config_${new Date().getTime()}.xlsx`
  );
};
/** åˆ·æ–°ç¼“存按钮操作 */
const handleRefreshCache = async () => {
  await refreshCache();
  proxy?.$modal.msgSuccess("刷新缓存成功");
}
  proxy?.$modal.msgSuccess('刷新缓存成功');
};
onMounted(() => {
  getList();
})
});
</script>
src/views/system/dept/index.vue
@@ -1,14 +1,17 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="部门名称" prop="deptName">
              <el-input v-model="queryParams.deptName" placeholder="请输入部门名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.deptName" placeholder="请输入部门名称" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="类别编码" prop="deptCategory">
              <el-input v-model="queryParams.deptCategory" placeholder="请输入类别编码" clearable style="width: 240px" @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="状态" prop="status">
              <el-select v-model="queryParams.status" placeholder="部门状态" clearable>
              <el-select v-model="queryParams.status" placeholder="部门状态" clearable >
                <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
              </el-select>
            </el-form-item>
@@ -25,24 +28,25 @@
      <template #header>
        <el-row :gutter="10">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['system:dept:add']">新增 </el-button>
            <el-button v-hasPermi="['system:dept:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增 </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table
        ref="deptTableRef"
        v-loading="loading"
        :data="deptList"
        row-key="deptId"
        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
        ref="deptTableRef"
        :default-expand-all="isExpandAll"
      >
        <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
        <el-table-column prop="deptCategory" align="center" label="类别编码" width="200"></el-table-column>
        <el-table-column prop="orderNum" align="center" label="排序" width="200"></el-table-column>
        <el-table-column prop="status" align="center" label="状态" width="100">
          <template #default="scope">
@@ -57,23 +61,23 @@
        <el-table-column fixed="right" align="center" label="操作">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']" />
              <el-button v-hasPermi="['system:dept:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
            </el-tooltip>
            <el-tooltip content="新增" placement="top">
              <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']" />
              <el-button v-hasPermi="['system:dept:add']" link type="primary" icon="Plus" @click="handleAdd(scope.row)" />
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']" />
              <el-button v-hasPermi="['system:dept:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)" />
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-body width="600px">
    <el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-body width="600px">
      <el-form ref="deptFormRef" :model="form" :rules="rules" label-width="80px">
        <el-row>
          <el-col :span="24" v-if="form.parentId !== 0">
          <el-col v-if="form.parentId !== 0" :span="24">
            <el-form-item label="上级部门" prop="parentId">
              <el-tree-select
                v-model="form.parentId"
@@ -88,6 +92,11 @@
          <el-col :span="12">
            <el-form-item label="部门名称" prop="deptName">
              <el-input v-model="form.deptName" placeholder="请输入部门名称" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="类别编码" prop="deptCategory">
              <el-input v-model="form.deptCategory" placeholder="请输入类别编码" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -115,8 +124,7 @@
          <el-col :span="12">
            <el-form-item label="部门状态">
              <el-radio-group v-model="form.status">
                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label
                }}</el-radio>
                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
@@ -133,26 +141,25 @@
</template>
<script setup name="Dept" lang="ts">
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
import { DeptForm, DeptQuery, DeptVO } from "@/api/system/dept/types";
import {UserVO} from "@/api/system/user/types";
import {listUserByDeptId} from "@/api/system/user";
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from '@/api/system/dept';
import { DeptForm, DeptQuery, DeptVO } from '@/api/system/dept/types';
import { UserVO } from '@/api/system/user/types';
import { listUserByDeptId } from '@/api/system/user';
interface DeptOptionsType {
  deptId: number | string;
  deptName: string;
  children: DeptOptionsType[];
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const deptList = ref<DeptVO[]>([])
const loading = ref(true)
const showSearch = ref(true)
const deptOptions = ref<DeptOptionsType[]>([])
const isExpandAll = ref(true)
const deptList = ref<DeptVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const deptOptions = ref<DeptOptionsType[]>([]);
const isExpandAll = ref(true);
const deptUserList = ref<UserVO[]>([]);
const dialog = reactive<DialogOption>({
@@ -168,45 +175,48 @@
  deptId: undefined,
  parentId: undefined,
  deptName: undefined,
  deptCategory: undefined,
  orderNum: 0,
  leader: undefined,
  phone: undefined,
  email: undefined,
  status: "0"
}
const data = reactive<PageData<DeptForm, DeptQuery>>({
  status: '0'
};
const initData: PageData<DeptForm, DeptQuery> = {
  form: { ...initFormData },
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    deptName: undefined,
    deptCategory: undefined,
    status: undefined
  },
  rules: {
    parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
    deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
    orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
    email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
    phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
  },
})
    parentId: [{ required: true, message: '上级部门不能为空', trigger: 'blur' }],
    deptName: [{ required: true, message: '部门名称不能为空', trigger: 'blur' }],
    orderNum: [{ required: true, message: '显示排序不能为空', trigger: 'blur' }],
    email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
    phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }]
  }
};
const data = reactive<PageData<DeptForm, DeptQuery>>(initData);
const { queryParams, form, rules } = toRefs<PageData<DeptForm, DeptQuery>>(data)
const { queryParams, form, rules } = toRefs<PageData<DeptForm, DeptQuery>>(data);
/** æŸ¥è¯¢èœå•列表 */
const getList = async () => {
  loading.value = true;
  const res = await listDept(queryParams.value);
  const data = proxy?.handleTree<DeptVO>(res.data, "deptId")
  const data = proxy?.handleTree<DeptVO>(res.data, 'deptId');
  if (data) {
    deptList.value = data
    deptList.value = data;
  }
  loading.value = false
}
  loading.value = false;
};
/** æŸ¥è¯¢å½“前部门的所有用户 */
async function getDeptAllUser(deptId: any) {
  if (deptId !== null && deptId !== "" && deptId !== undefined) {
  if (deptId !== null && deptId !== '' && deptId !== undefined) {
    const res = await listUserByDeptId(deptId);
    deptUserList.value = res.data;
  }
@@ -214,52 +224,52 @@
/** å–消按钮 */
const cancel = () => {
  reset()
  dialog.visible = false
}
  reset();
  dialog.visible = false;
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  deptFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  handleQuery()
}
  handleQuery();
};
/** å±•å¼€/折叠操作 */
const handleToggleExpandAll = () => {
  isExpandAll.value = !isExpandAll.value;
  toggleExpandAll(deptList.value, isExpandAll.value)
}
  toggleExpandAll(deptList.value, isExpandAll.value);
};
/** å±•å¼€/折叠所有 */
const toggleExpandAll = (data: DeptVO[], status: boolean) => {
  data.forEach((item) => {
    deptTableRef.value?.toggleRowExpansion(item, status)
    if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
  })
}
    deptTableRef.value?.toggleRowExpansion(item, status);
    if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
  });
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = async (row?: DeptVO) => {
  reset();
  const res = await listDept();
  const data = proxy?.handleTree<DeptOptionsType>(res.data, "deptId");
  const data = proxy?.handleTree<DeptOptionsType>(res.data, 'deptId');
  if (data) {
    deptOptions.value = data
    deptOptions.value = data;
    if (row && row.deptId) {
      form.value.parentId = row?.deptId;
    }
    dialog.visible = true;
    dialog.title = "添加部门";
    dialog.title = '添加部门';
  }
}
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row: DeptVO) => {
@@ -267,9 +277,9 @@
  //查询当前部门所有用户
  getDeptAllUser(row.deptId);
  const res = await getDept(row.deptId);
  form.value = res.data
  form.value = res.data;
  const response = await listDeptExcludeChild(row.deptId);
  const data = proxy?.handleTree<DeptOptionsType>(response.data, "deptId")
  const data = proxy?.handleTree<DeptOptionsType>(response.data, 'deptId');
  if (data) {
    deptOptions.value = data;
    if (data.length === 0) {
@@ -282,26 +292,26 @@
    }
  }
  dialog.visible = true;
  dialog.title = "修改部门";
}
  dialog.title = '修改部门';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  deptFormRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      form.value.deptId ? await updateDept(form.value) : await addDept(form.value);
      proxy?.$modal.msgSuccess("操作成功");
      proxy?.$modal.msgSuccess('操作成功');
      dialog.visible = false;
      await getList();
    }
  })
}
  });
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row: DeptVO) => {
  await proxy?.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?');
  await delDept(row.deptId);
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
onMounted(() => {
  getList();
src/views/system/dict/data.vue
@@ -1,16 +1,16 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="字典名称" prop="dictType">
              <el-select v-model="queryParams.dictType" style="width: 200px">
              <el-select v-model="queryParams.dictType">
                <el-option v-for="item in typeOptions" :key="item.dictId" :label="item.dictName" :value="item.dictType" />
              </el-select>
            </el-form-item>
            <el-form-item label="字典标签" prop="dictLabel">
              <el-input v-model="queryParams.dictLabel" placeholder="请输入字典标签" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.dictLabel" placeholder="请输入字典标签" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -24,33 +24,41 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:dict:add']">新增</el-button>
            <el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:dict:edit']">修改</el-button>
            <el-button v-hasPermi="['system:dict:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:dict:remove']">
            <el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:dict:export']">导出</el-button>
            <el-button v-hasPermi="['system:dict:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="字典编码" align="center" prop="dictCode" v-if="false" />
        <el-table-column v-if="false" label="字典编码" align="center" prop="dictCode" />
        <el-table-column label="字典标签" align="center" prop="dictLabel">
          <template #default="scope">
            <span v-if="(scope.row.listClass === '' || scope.row.listClass === 'default') && (scope.row.cssClass === '' || scope.row.cssClass == null)">{{ scope.row.dictLabel }}</span>
            <el-tag v-else :type="(scope.row.listClass === 'primary' || scope.row.listClass === 'default') ? '' : scope.row.listClass" :class="scope.row.cssClass">{{ scope.row.dictLabel }}</el-tag>
            <span
              v-if="(scope.row.listClass === '' || scope.row.listClass === 'default') && (scope.row.cssClass === '' || scope.row.cssClass == null)"
              >{{ scope.row.dictLabel }}</span
            >
            <el-tag
              v-else
              :type="scope.row.listClass === 'primary' || scope.row.listClass === 'default' ? 'primary' : scope.row.listClass"
              :class="scope.row.cssClass"
              >{{ scope.row.dictLabel }}</el-tag
            >
          </template>
        </el-table-column>
        <el-table-column label="字典键值" align="center" prop="dictValue" />
@@ -64,19 +72,19 @@
        <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']"></el-button>
              <el-button v-hasPermi="['system:dict:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']"></el-button>
              <el-button v-hasPermi="['system:dict:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
      <el-form ref="dataFormRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="字典类型">
          <el-input v-model="form.dictType" :disabled="true" />
@@ -118,13 +126,14 @@
</template>
<script setup name="Data" lang="ts">
import useDictStore from '@/store/modules/dict'
import { optionselect as getDictOptionselect, getType } from "@/api/system/dict/type";
import { listData, getData, delData, addData, updateData } from "@/api/system/dict/data";
import useDictStore from '@/store/modules/dict';
import { optionselect as getDictOptionselect, getType } from '@/api/system/dict/type';
import { listData, getData, delData, addData, updateData } from '@/api/system/dict/data';
import { DictTypeVO } from '@/api/system/dict/type/types';
import { DictDataForm, DictDataQuery, DictDataVO } from "@/api/system/dict/data/types";
import { DictDataForm, DictDataQuery, DictDataVO } from '@/api/system/dict/data/types';
import { RouteLocationNormalized } from 'vue-router';
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const dataList = ref<DictDataVO[]>([]);
@@ -134,12 +143,11 @@
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const defaultDictType = ref("");
const defaultDictType = ref('');
const typeOptions = ref<DictTypeVO[]>([]);
const dataFormRef = ref<ElFormInstance>();
const queryFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
  visible: false,
@@ -147,13 +155,13 @@
});
// æ•°æ®æ ‡ç­¾å›žæ˜¾æ ·å¼
const listClassOptions = ref<Array<{ value: string, label: string }>>([
  { value: "default", label: "默认" },
  { value: "primary", label: "主要" },
  { value: "success", label: "成功" },
  { value: "info", label: "信息" },
  { value: "warning", label: "警告" },
  { value: "danger", label: "危险" }
const listClassOptions = ref<Array<{ value: string; label: string }>>([
  { value: 'default', label: '默认' },
  { value: 'primary', label: '主要' },
  { value: 'success', label: '成功' },
  { value: 'info', label: '信息' },
  { value: 'warning', label: '警告' },
  { value: 'danger', label: '危险' }
]);
const initFormData: DictDataForm = {
@@ -161,10 +169,10 @@
  dictLabel: '',
  dictValue: '',
  cssClass: '',
  listClass: "default",
  listClass: 'primary',
  dictSort: 0,
  remark: ''
}
};
const data = reactive<PageData<DictDataForm, DictDataQuery>>({
  form: { ...initFormData },
  queryParams: {
@@ -175,9 +183,9 @@
    dictLabel: ''
  },
  rules: {
    dictLabel: [{ required: true, message: "数据标签不能为空", trigger: "blur" }],
    dictValue: [{ required: true, message: "数据键值不能为空", trigger: "blur" }],
    dictSort: [{ required: true, message: "数据顺序不能为空", trigger: "blur" }]
    dictLabel: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
    dictValue: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
    dictSort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }]
  }
});
@@ -189,13 +197,13 @@
  queryParams.value.dictType = data.dictType;
  defaultDictType.value = data.dictType;
  getList();
}
};
/** æŸ¥è¯¢å­—典类型列表 */
const getTypeList = async () => {
  const res = await getDictOptionselect()
  const res = await getDictOptionselect();
  typeOptions.value = res.data;
}
};
/** æŸ¥è¯¢å­—典数据列表 */
const getList = async () => {
  loading.value = true;
@@ -203,46 +211,56 @@
  dataList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** å–消按钮 */
const cancel = () => {
  dialog.visible = false;
  reset();
}
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  dataFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** è¿”回按钮操作 */
const handleClose = () => {
  const obj = { path: "/system/dict" };
  const obj: RouteLocationNormalized = {
    fullPath: '',
    hash: '',
    matched: [],
    meta: undefined,
    name: undefined,
    params: undefined,
    query: undefined,
    redirectedFrom: undefined,
    path: '/system/dict'
  };
  proxy?.$tab.closeOpenPage(obj);
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  queryParams.value.dictType = defaultDictType.value;
  handleQuery();
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = () => {
  reset();
  form.value.dictType = queryParams.value.dictType;
  dialog.visible = true;
  dialog.title = "添加字典数据";
}
  dialog.title = '添加字典数据';
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: DictDataVO[]) => {
  ids.value = selection.map(item => item.dictCode);
  ids.value = selection.map((item) => item.dictCode);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row?: DictDataVO) => {
  reset();
@@ -250,40 +268,42 @@
  const res = await getData(dictCode);
  Object.assign(form.value, res.data);
  dialog.visible = true;
  dialog.title = "修改字典数据";
}
  dialog.title = '修改字典数据';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  dataFormRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      form.value.dictCode ? await updateData(form.value) : await addData(form.value);
      useDictStore().removeDict(queryParams.value.dictType);
      proxy?.$modal.msgSuccess("操作成功");
      proxy?.$modal.msgSuccess('操作成功');
      dialog.visible = false;
      await getList();
    }
  });
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: DictDataVO) => {
  const dictCodes = row?.dictCode || ids.value;
  await proxy?.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?');
  await delData(dictCodes);
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
  proxy?.$modal.msgSuccess('删除成功');
  useDictStore().removeDict(queryParams.value.dictType);
}
};
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
const handleExport = () => {
  proxy?.download("system/dict/data/export", {
    ...queryParams.value
  }, `dict_data_${new Date().getTime()}.xlsx`);
}
  proxy?.download(
    'system/dict/data/export',
    {
      ...queryParams.value
    },
    `dict_data_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
  getTypes(route.params && route.params.dictId as string);
  getTypes(route.params && (route.params.dictId as string));
  getTypeList();
})
});
</script>
src/views/system/dict/index.vue
@@ -1,14 +1,14 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="字典名称" prop="dictName">
              <el-input v-model="queryParams.dictName" placeholder="请输入字典名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.dictName" placeholder="请输入字典名称" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="字典类型" prop="dictType">
              <el-input v-model="queryParams.dictType" placeholder="请输入字典类型" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.dictType" placeholder="请输入字典类型" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="创建时间" style="width: 308px">
              <el-date-picker
@@ -33,29 +33,29 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:dict:add']">新增</el-button>
            <el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:dict:edit']">修改</el-button>
            <el-button v-hasPermi="['system:dict:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:dict:remove']">
            <el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:dict:export']">导出</el-button>
            <el-button v-hasPermi="['system:dict:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Refresh" @click="handleRefreshCache" v-hasPermi="['system:dict:remove']">刷新缓存</el-button>
            <el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Refresh" @click="handleRefreshCache">刷新缓存</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table v-loading="loading" :data="typeList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="字典编号" align="center" prop="dictId" v-if="false" />
        <el-table-column v-if="false" label="字典编号" align="center" prop="dictId" />
        <el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
        <el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
          <template #default="scope">
@@ -73,19 +73,19 @@
        <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']"></el-button>
              <el-button v-hasPermi="['system:dict:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']"></el-button>
              <el-button v-hasPermi="['system:dict:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
      <el-form ref="dictFormRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="字典名称" prop="dictName">
          <el-input v-model="form.dictName" placeholder="请输入字典名称" />
@@ -108,9 +108,9 @@
</template>
<script setup name="Dict" lang="ts">
import useDictStore from '@/store/modules/dict'
import { listType, getType, delType, addType, updateType, refreshCache } from "@/api/system/dict/type";
import { DictTypeForm, DictTypeQuery, DictTypeVO } from "@/api/system/dict/type/types";
import useDictStore from '@/store/modules/dict';
import { listType, getType, delType, addType, updateType, refreshCache } from '@/api/system/dict/type';
import { DictTypeForm, DictTypeQuery, DictTypeVO } from '@/api/system/dict/type/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -126,7 +126,6 @@
const dictFormRef = ref<ElFormInstance>();
const queryFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
  visible: false,
  title: ''
@@ -137,7 +136,7 @@
  dictName: '',
  dictType: '',
  remark: ''
}
};
const data = reactive<PageData<DictTypeForm, DictTypeQuery>>({
  form: { ...initFormData },
  queryParams: {
@@ -147,9 +146,9 @@
    dictType: ''
  },
  rules: {
    dictName: [{ required: true, message: "字典名称不能为空", trigger: "blur" }],
    dictType: [{ required: true, message: "字典类型不能为空", trigger: "blur" }]
  },
    dictName: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
    dictType: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }]
  }
});
const { queryParams, form, rules } = toRefs(data);
@@ -157,45 +156,45 @@
/** æŸ¥è¯¢å­—典类型列表 */
const getList = () => {
  loading.value = true;
  listType(proxy?.addDateRange(queryParams.value, dateRange.value)).then(res => {
  listType(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
    typeList.value = res.rows;
    total.value = res.total;
    loading.value = false;
  });
}
};
/** å–消按钮 */
const cancel = () => {
  reset();
  dialog.visible = false;
}
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  dictFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = () => {
  reset();
  dialog.visible = true;
  dialog.title = "添加字典类型";
}
  dialog.title = '添加字典类型';
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: DictTypeVO[]) => {
  ids.value = selection.map(item => item.dictId);
  ids.value = selection.map((item) => item.dictId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row?: DictTypeVO) => {
  reset();
@@ -203,41 +202,45 @@
  const res = await getType(dictId);
  Object.assign(form.value, res.data);
  dialog.visible = true;
  dialog.title = "修改字典类型";
}
  dialog.title = '修改字典类型';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  dictFormRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      form.value.dictId ? await updateType(form.value) : await addType(form.value);
      proxy?.$modal.msgSuccess("操作成功");
      proxy?.$modal.msgSuccess('操作成功');
      dialog.visible = false;
      getList();
    }
  });
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: DictTypeVO) => {
  const dictIds = row?.dictId || ids.value;
  await proxy?.$modal.confirm('是否确认删除字典编号为"' + dictIds + '"的数据项?');
  await delType(dictIds);
  getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
const handleExport = () => {
  proxy?.download("system/dict/type/export", {
    ...queryParams.value
  }, `dict_${new Date().getTime()}.xlsx`);
}
  proxy?.download(
    'system/dict/type/export',
    {
      ...queryParams.value
    },
    `dict_${new Date().getTime()}.xlsx`
  );
};
/** åˆ·æ–°ç¼“存按钮操作 */
const handleRefreshCache = async () => {
  await refreshCache();
  proxy?.$modal.msgSuccess("刷新成功");
  proxy?.$modal.msgSuccess('刷新成功');
  useDictStore().cleanDict();
}
};
onMounted(() => {
  getList();
})
});
</script>
src/views/system/menu/index.vue
@@ -1,14 +1,14 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="菜单名称" prop="menuName">
              <el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="状态" prop="status">
              <el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
              <el-select v-model="queryParams.status" placeholder="菜单状态" clearable >
                <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
              </el-select>
            </el-form-item>
@@ -25,21 +25,21 @@
      <template #header>
        <el-row :gutter="10">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['system:menu:add']">新增 </el-button>
            <el-button v-hasPermi="['system:menu:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增 </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table
        ref="menuTableRef"
        v-loading="loading"
        :data="menuList"
        row-key="menuId"
        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
        ref="menuTableRef"
        :default-expand-all="isExpandAll"
      >
        <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
@@ -64,20 +64,20 @@
        <el-table-column fixed="right" label="操作" width="180">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']" />
              <el-button v-hasPermi="['system:menu:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
            </el-tooltip>
            <el-tooltip content="新增" placement="top">
              <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']" />
              <el-button v-hasPermi="['system:menu:add']" link type="primary" icon="Plus" @click="handleAdd(scope.row)" />
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']" />
              <el-button v-hasPermi="['system:menu:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)" />
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-bod width="750px">
    <el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-bod width="750px">
      <el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
        <el-row>
          <el-col :span="24">
@@ -95,13 +95,13 @@
          <el-col :span="24">
            <el-form-item label="菜单类型" prop="menuType">
              <el-radio-group v-model="form.menuType">
                <el-radio label="M">目录</el-radio>
                <el-radio label="C">菜单</el-radio>
                <el-radio label="F">按钮</el-radio>
                <el-radio value="M">目录</el-radio>
                <el-radio value="C">菜单</el-radio>
                <el-radio value="F">按钮</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="24" v-if="form.menuType !== 'F'">
          <el-col v-if="form.menuType !== 'F'" :span="24">
            <el-form-item label="菜单图标" prop="icon">
              <!-- å›¾æ ‡é€‰æ‹©å™¨ -->
              <icon-select v-model="form.icon" />
@@ -117,7 +117,7 @@
              <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.menuType !== 'F'">
          <el-col v-if="form.menuType !== 'F'" :span="12">
            <el-form-item>
              <template #label>
                <span>
@@ -134,7 +134,7 @@
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.menuType !== 'F'">
          <el-col v-if="form.menuType !== 'F'" :span="12">
            <el-form-item prop="path">
              <template #label>
                <span>
@@ -149,7 +149,7 @@
              <el-input v-model="form.path" placeholder="请输入路由地址" />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.menuType === 'C'">
          <el-col v-if="form.menuType === 'C'" :span="12">
            <el-form-item prop="component">
              <template #label>
                <span>
@@ -164,7 +164,7 @@
              <el-input v-model="form.component" placeholder="请输入组件路径" />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.menuType !== 'M'">
          <el-col v-if="form.menuType !== 'M'" :span="12">
            <el-form-item>
              <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
              <template #label>
@@ -179,7 +179,7 @@
              </template>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.menuType === 'C'">
          <el-col v-if="form.menuType === 'C'" :span="12">
            <el-form-item>
              <el-input v-model="form.queryParam" placeholder="请输入路由参数" maxlength="255" />
              <template #label>
@@ -194,7 +194,7 @@
              </template>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.menuType === 'C'">
          <el-col v-if="form.menuType === 'C'" :span="12">
            <el-form-item>
              <template #label>
                <span>
@@ -212,7 +212,7 @@
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.menuType !== 'F'">
          <el-col v-if="form.menuType !== 'F'" :span="12">
            <el-form-item>
              <template #label>
                <span>
@@ -271,14 +271,14 @@
  children: MenuOptionsType[] | undefined;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_show_hide", "sys_normal_disable"));
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_show_hide', 'sys_normal_disable'));
const menuList = ref<MenuVO[]>([])
const loading = ref(true)
const showSearch = ref(true)
const menuOptions = ref<MenuOptionsType[]>([])
const isExpandAll = ref(false)
const menuList = ref<MenuVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const menuOptions = ref<MenuOptionsType[]>([]);
const isExpandAll = ref(false);
const dialog = reactive<DialogOption>({
  visible: false,
@@ -295,11 +295,11 @@
  icon: '',
  menuType: MenuTypeEnum.M,
  orderNum: 1,
  isFrame: "1",
  isCache: "0",
  visible: "0",
  status: "0"
}
  isFrame: '1',
  isCache: '0',
  visible: '0',
  status: '0'
};
const data = reactive<PageData<MenuForm, MenuQuery>>({
  form: { ...initFormData },
  queryParams: {
@@ -307,73 +307,73 @@
    status: undefined
  },
  rules: {
    menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
    orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
    path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
  },
})
    menuName: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
    orderNum: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
    path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }]
  }
});
const menuTableRef = ref<ElTableInstance>();
const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data)
const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data);
/** æŸ¥è¯¢èœå•列表 */
const getList = async () => {
  loading.value = true
  loading.value = true;
  const res = await listMenu(queryParams.value);
  const data = proxy?.handleTree<MenuVO>(res.data, "menuId")
  const data = proxy?.handleTree<MenuVO>(res.data, 'menuId');
  if (data) {
    menuList.value = data
    menuList.value = data;
  }
  loading.value = false
}
  loading.value = false;
};
/** æŸ¥è¯¢èœå•下拉树结构 */
const getTreeselect = async () => {
  menuOptions.value = []
  menuOptions.value = [];
  const response = await listMenu();
  const menu: MenuOptionsType = { menuId: 0, menuName: "主类目", children: [] }
  menu.children = proxy?.handleTree<MenuOptionsType>(response.data, "menuId")
  menuOptions.value.push(menu)
}
  const menu: MenuOptionsType = { menuId: 0, menuName: '主类目', children: [] };
  menu.children = proxy?.handleTree<MenuOptionsType>(response.data, 'menuId');
  menuOptions.value.push(menu);
};
/** å–消按钮 */
const cancel = () => {
  reset()
  dialog.visible = false
}
  reset();
  dialog.visible = false;
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  menuFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = (row?: MenuVO) => {
  reset();
  getTreeselect();
  row && row.menuId ? form.value.parentId = row.menuId : form.value.parentId = 0;
  row && row.menuId ? (form.value.parentId = row.menuId) : (form.value.parentId = 0);
  dialog.visible = true;
  dialog.title = "添加菜单";
}
  dialog.title = '添加菜单';
};
/** å±•å¼€/折叠操作 */
const handleToggleExpandAll = () => {
  isExpandAll.value = !isExpandAll.value;
  toggleExpandAll(menuList.value, isExpandAll.value)
}
  toggleExpandAll(menuList.value, isExpandAll.value);
};
/** å±•å¼€/折叠所有 */
const toggleExpandAll = (data: MenuVO[], status: boolean) => {
  data.forEach((item: MenuVO) => {
    menuTableRef.value?.toggleRowExpansion(item, status)
    if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
  })
}
    menuTableRef.value?.toggleRowExpansion(item, status);
    if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
  });
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row: MenuVO) => {
  reset();
@@ -383,26 +383,26 @@
    form.value = data;
  }
  dialog.visible = true;
  dialog.title = "修改菜单";
}
  dialog.title = '修改菜单';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  menuFormRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      form.value.menuId ? await updateMenu(form.value) : await addMenu(form.value);
      proxy?.$modal.msgSuccess("操作成功");
      proxy?.$modal.msgSuccess('操作成功');
      dialog.visible = false;
      await getList();
    }
  })
}
  });
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row: MenuVO) => {
  await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?');
  await delMenu(row.menuId);
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
onMounted(() => {
  getList();
src/views/system/notice/index.vue
@@ -1,17 +1,17 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="公告标题" prop="noticeTitle">
              <el-input v-model="queryParams.noticeTitle" placeholder="请输入公告标题" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.noticeTitle" placeholder="请输入公告标题" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="操作人员" prop="createByName">
              <el-input v-model="queryParams.createByName" placeholder="请输入操作人员" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.createByName" placeholder="请输入操作人员" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="类型" prop="noticeType">
              <el-select v-model="queryParams.noticeType" placeholder="公告类型" clearable style="width: 200px">
              <el-select v-model="queryParams.noticeType" placeholder="公告类型" clearable>
                <el-option v-for="dict in sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value" />
              </el-select>
            </el-form-item>
@@ -28,25 +28,25 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:notice:add']">新增</el-button>
            <el-button v-hasPermi="['system:notice:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:notice:edit']"
            <el-button v-hasPermi="['system:notice:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
              >修改</el-button
            >
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:notice:remove']">
            <el-button v-hasPermi="['system:notice:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="序号" align="center" prop="noticeId" width="100" v-if="false" />
        <el-table-column v-if="false" label="序号" align="center" prop="noticeId" width="100" />
        <el-table-column label="公告标题" align="center" prop="noticeTitle" :show-overflow-tooltip="true" />
        <el-table-column label="公告类型" align="center" prop="noticeType" width="100">
          <template #default="scope">
@@ -67,19 +67,19 @@
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:notice:edit']"></el-button>
              <el-button v-hasPermi="['system:notice:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:notice:remove']"></el-button>
              <el-button v-hasPermi="['system:notice:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å…¬å‘Šå¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="780px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="780px" append-to-body>
      <el-form ref="noticeFormRef" :model="form" :rules="rules" label-width="80px">
        <el-row>
          <el-col :span="12">
@@ -97,8 +97,7 @@
          <el-col :span="24">
            <el-form-item label="状态">
              <el-radio-group v-model="form.status">
                <el-radio v-for="dict in sys_notice_status" :key="dict.value" :label="dict.value">{{ dict.label
                }}</el-radio>
                <el-radio v-for="dict in sys_notice_status" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
@@ -120,11 +119,11 @@
</template>
<script setup name="Notice" lang="ts">
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice";
import { NoticeForm, NoticeQuery, NoticeVO } from "@/api/system/notice/types";
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from '@/api/system/notice';
import { NoticeForm, NoticeQuery, NoticeVO } from '@/api/system/notice/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_notice_status, sys_notice_type } = toRefs<any>(proxy?.useDict("sys_notice_status", "sys_notice_type"));
const { sys_notice_status, sys_notice_type } = toRefs<any>(proxy?.useDict('sys_notice_status', 'sys_notice_type'));
const noticeList = ref<NoticeVO[]>([]);
const loading = ref(true);
@@ -137,7 +136,6 @@
const queryFormRef = ref<ElFormInstance>();
const noticeFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
  visible: false,
  title: ''
@@ -148,10 +146,10 @@
  noticeTitle: '',
  noticeType: '',
  noticeContent: '',
  status: "0",
  status: '0',
  remark: '',
  createByName: ''
}
};
const data = reactive<PageData<NoticeForm, NoticeQuery>>({
  form: { ...initFormData },
  queryParams: {
@@ -163,9 +161,9 @@
    noticeType: ''
  },
  rules: {
    noticeTitle: [{ required: true, message: "公告标题不能为空", trigger: "blur" }],
    noticeType: [{ required: true, message: "公告类型不能为空", trigger: "change" }]
  },
    noticeTitle: [{ required: true, message: '公告标题不能为空', trigger: 'blur' }],
    noticeType: [{ required: true, message: '公告类型不能为空', trigger: 'change' }]
  }
});
const { queryParams, form, rules } = toRefs(data);
@@ -177,39 +175,39 @@
  noticeList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** å–消按钮 */
const cancel = () => {
  reset();
  dialog.visible = false;
}
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  noticeFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: NoticeVO[]) => {
  ids.value = selection.map(item => item.noticeId);
  ids.value = selection.map((item) => item.noticeId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = () => {
  reset();
  dialog.visible = true;
  dialog.title = "添加公告";
}
  dialog.title = '添加公告';
};
/**修改按钮操作 */
const handleUpdate = async (row?: NoticeVO) => {
  reset();
@@ -217,29 +215,29 @@
  const { data } = await getNotice(noticeId);
  Object.assign(form.value, data);
  dialog.visible = true;
  dialog.title = "修改公告";
}
  dialog.title = '修改公告';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  noticeFormRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      form.value.noticeId ? await updateNotice(form.value) : await addNotice(form.value);
      proxy?.$modal.msgSuccess("修改成功");
      proxy?.$modal.msgSuccess('操作成功');
      dialog.visible = false;
      await getList();
    }
  });
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: NoticeVO) => {
  const noticeIds = row?.noticeId || ids.value
  const noticeIds = row?.noticeId || ids.value;
  await proxy?.$modal.confirm('是否确认删除公告编号为"' + noticeIds + '"的数据项?');
  await delNotice(noticeIds);
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
onMounted(() => {
  getList();
})
});
</script>
src/views/system/oss/config.vue
@@ -1,17 +1,17 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="配置key" prop="configKey">
              <el-input v-model="queryParams.configKey" placeholder="配置key" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.configKey" placeholder="配置key" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="桶名称" prop="bucketName">
              <el-input v-model="queryParams.bucketName" placeholder="请输入桶名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.bucketName" placeholder="请输入桶名称" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="是否默认" prop="status">
              <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 200px">
              <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
                <el-option key="0" label="是" value="0" />
                <el-option key="1" label="否" value="1" />
              </el-select>
@@ -29,37 +29,39 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:ossConfig:add']">新增</el-button>
            <el-button v-hasPermi="['system:ossConfig:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:ossConfig:edit']">修改</el-button>
            <el-button v-hasPermi="['system:ossConfig:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
              >修改</el-button
            >
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:ossConfig:remove']">
            <el-button v-hasPermi="['system:ossConfig:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table v-loading="loading" :data="ossConfigList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="主建" align="center" prop="ossConfigId" v-if="columns[0].visible" />
        <el-table-column label="配置key" align="center" prop="configKey" v-if="columns[1].visible" />
        <el-table-column label="访问站点" align="center" prop="endpoint" v-if="columns[2].visible" width="200" />
        <el-table-column label="自定义域名" align="center" prop="domain" v-if="columns[3].visible" width="200" />
        <el-table-column label="桶名称" align="center" prop="bucketName" v-if="columns[4].visible" />
        <el-table-column label="前缀" align="center" prop="prefix" v-if="columns[5].visible" />
        <el-table-column label="域" align="center" prop="region" v-if="columns[6].visible" />
        <el-table-column label="桶权限类型" align="center" prop="accessPolicy" v-if="columns[7].visible">
        <el-table-column v-if="columns[0].visible" label="主建" align="center" prop="ossConfigId" />
        <el-table-column v-if="columns[1].visible" label="配置key" align="center" prop="configKey" />
        <el-table-column v-if="columns[2].visible" label="访问站点" align="center" prop="endpoint" width="200" />
        <el-table-column v-if="columns[3].visible" label="自定义域名" align="center" prop="domain" width="200" />
        <el-table-column v-if="columns[4].visible" label="桶名称" align="center" prop="bucketName" />
        <el-table-column v-if="columns[5].visible" label="前缀" align="center" prop="prefix" />
        <el-table-column v-if="columns[6].visible" label="域" align="center" prop="region" />
        <el-table-column v-if="columns[7].visible" label="桶权限类型" align="center" prop="accessPolicy">
          <template #default="scope">
            <el-tag type="warning" v-if="scope.row.accessPolicy === '0'">private</el-tag>
            <el-tag type="success" v-if="scope.row.accessPolicy === '1'">public</el-tag>
            <el-tag type="info" v-if="scope.row.accessPolicy === '2'">custom</el-tag>
            <el-tag v-if="scope.row.accessPolicy === '0'" type="warning">private</el-tag>
            <el-tag v-if="scope.row.accessPolicy === '1'" type="success">public</el-tag>
            <el-tag v-if="scope.row.accessPolicy === '2'" type="info">custom</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="是否默认" align="center" prop="status" v-if="columns[8].visible">
        <el-table-column v-if="columns[8].visible" label="是否默认" align="center" prop="status">
          <template #default="scope">
            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
          </template>
@@ -67,19 +69,19 @@
        <el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:ossConfig:edit']"></el-button>
              <el-button v-hasPermi="['system:ossConfig:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:ossConfig:remove']"></el-button>
              <el-button v-hasPermi="['system:ossConfig:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å¯¹è±¡å­˜å‚¨é…ç½®å¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="800px" append-to-body>
      <el-form ref="ossConfigFormRef" :model="form" :rules="rules" label-width="120px">
        <el-form-item label="配置key" prop="configKey">
          <el-input v-model="form.configKey" placeholder="请输入配置key" />
@@ -104,14 +106,14 @@
        </el-form-item>
        <el-form-item label="是否HTTPS">
          <el-radio-group v-model="form.isHttps">
            <el-radio v-for="dict in sys_yes_no" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
            <el-radio v-for="dict in sys_yes_no" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="桶权限类型">
          <el-radio-group v-model="form.accessPolicy">
            <el-radio label="0">private</el-radio>
            <el-radio label="1">public</el-radio>
            <el-radio label="2">custom</el-radio>
            <el-radio value="0">private</el-radio>
            <el-radio value="1">public</el-radio>
            <el-radio value="2">custom</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="域" prop="region">
@@ -132,19 +134,11 @@
</template>
<script setup name="OssConfig" lang="ts">
import {
  listOssConfig,
  getOssConfig,
  delOssConfig,
  addOssConfig,
  updateOssConfig,
  changeOssConfigStatus
} from "@/api/system/ossConfig";
import { OssConfigForm, OssConfigQuery, OssConfigVO } from "@/api/system/ossConfig/types";
import { listOssConfig, getOssConfig, delOssConfig, addOssConfig, updateOssConfig, changeOssConfigStatus } from '@/api/system/ossConfig';
import { OssConfigForm, OssConfigQuery, OssConfigVO } from '@/api/system/ossConfig/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { sys_yes_no } = toRefs<any>(proxy?.useDict("sys_yes_no"));
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_yes_no } = toRefs<any>(proxy?.useDict('sys_yes_no'));
const ossConfigList = ref<OssConfigVO[]>([]);
const buttonLoading = ref(false);
@@ -176,7 +170,6 @@
  { key: 8, label: `状态`, visible: true }
]);
const initFormData: OssConfigForm = {
  ossConfigId: undefined,
  configKey: '',
@@ -186,12 +179,12 @@
  prefix: '',
  endpoint: '',
  domain: '',
  isHttps: "N",
  accessPolicy: "1",
  isHttps: 'N',
  accessPolicy: '1',
  region: '',
  status: "1",
  remark: '',
}
  status: '1',
  remark: ''
};
const data = reactive<PageData<OssConfigForm, OssConfigQuery>>({
  form: { ...initFormData },
  // æŸ¥è¯¢å‚æ•°
@@ -200,47 +193,47 @@
    pageSize: 10,
    configKey: '',
    bucketName: '',
    status: '',
    status: ''
  },
  rules: {
    configKey: [{ required: true, message: "configKey不能为空", trigger: "blur" },],
    configKey: [{ required: true, message: 'configKey不能为空', trigger: 'blur' }],
    accessKey: [
      { required: true, message: "accessKey不能为空", trigger: "blur" },
      { required: true, message: 'accessKey不能为空', trigger: 'blur' },
      {
        min: 2,
        max: 200,
        message: "accessKey长度必须介于 2 å’Œ 100 ä¹‹é—´",
        trigger: "blur",
      },
        message: 'accessKey长度必须介于 2 å’Œ 100 ä¹‹é—´',
        trigger: 'blur'
      }
    ],
    secretKey: [
      { required: true, message: "secretKey不能为空", trigger: "blur" },
      { required: true, message: 'secretKey不能为空', trigger: 'blur' },
      {
        min: 2,
        max: 100,
        message: "secretKey长度必须介于 2 å’Œ 100 ä¹‹é—´",
        trigger: "blur",
      },
        message: 'secretKey长度必须介于 2 å’Œ 100 ä¹‹é—´',
        trigger: 'blur'
      }
    ],
    bucketName: [
      { required: true, message: "bucketName不能为空", trigger: "blur" },
      { required: true, message: 'bucketName不能为空', trigger: 'blur' },
      {
        min: 2,
        max: 100,
        message: "bucketName长度必须介于 2 å’Œ 100 ä¹‹é—´",
        trigger: "blur",
      },
        message: 'bucketName长度必须介于 2 å’Œ 100 ä¹‹é—´',
        trigger: 'blur'
      }
    ],
    endpoint: [
      { required: true, message: "endpoint不能为空", trigger: "blur" },
      { required: true, message: 'endpoint不能为空', trigger: 'blur' },
      {
        min: 2,
        max: 100,
        message: "endpoint名称长度必须介于 2 å’Œ 100 ä¹‹é—´",
        trigger: "blur",
      },
        message: 'endpoint名称长度必须介于 2 å’Œ 100 ä¹‹é—´',
        trigger: 'blur'
      }
    ],
    accessPolicy: [{ required: true, message: "accessPolicy不能为空", trigger: "blur" }]
    accessPolicy: [{ required: true, message: 'accessPolicy不能为空', trigger: 'blur' }]
  }
});
@@ -253,39 +246,39 @@
  ossConfigList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** å–消按钮 */
const cancel = () => {
  dialog.visible = false;
  reset();
}
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  ossConfigFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  handleQuery();
}
};
/** é€‰æ‹©æ¡æ•°  */
const handleSelectionChange = (selection: OssConfigVO[]) => {
  ids.value = selection.map(item => item.ossConfigId);
  ids.value = selection.map((item) => item.ossConfigId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = () => {
  reset();
  dialog.visible = true;
  dialog.title = "添加对象存储配置";
}
  dialog.title = '添加对象存储配置';
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row?: OssConfigVO) => {
  reset();
@@ -293,49 +286,49 @@
  const res = await getOssConfig(ossConfigId);
  Object.assign(form.value, res.data);
  dialog.visible = true;
  dialog.title = "修改对象存储配置";
}
  dialog.title = '修改对象存储配置';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  ossConfigFormRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      buttonLoading.value = true;
      if (form.value.ossConfigId) {
        await updateOssConfig(form.value).finally(() => buttonLoading.value = false);
        await updateOssConfig(form.value).finally(() => (buttonLoading.value = false));
      } else {
        await addOssConfig(form.value).finally(() => buttonLoading.value = false);
        await addOssConfig(form.value).finally(() => (buttonLoading.value = false));
      }
      proxy?.$modal.msgSuccess("新增成功");
      proxy?.$modal.msgSuccess('新增成功');
      dialog.visible = false;
      await getList();
    }
  });
}
};
/** çŠ¶æ€ä¿®æ”¹  */
const handleStatusChange = async (row: OssConfigVO) => {
  let text = row.status === "0" ? "启用" : "停用";
  let text = row.status === '0' ? '启用' : '停用';
  try {
    await proxy?.$modal.confirm('确认要"' + text + '""' + row.configKey + '"配置吗?');
    await changeOssConfigStatus(row.ossConfigId, row.status, row.configKey);
    await getList()
    proxy?.$modal.msgSuccess(text + "成功");
  } catch { return } finally {
    row.status = row.status === "0" ? "1" : "0";
    await getList();
    proxy?.$modal.msgSuccess(text + '成功');
  } catch {
    return;
  } finally {
    row.status = row.status === '0' ? '1' : '0';
  }
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: OssConfigVO) => {
  const ossConfigIds = row?.ossConfigId || ids.value;
  await proxy?.$modal.confirm('是否确认删除OSS配置编号为"' + ossConfigIds + '"的数据项?');
  loading.value = true;
  await delOssConfig(ossConfigIds).finally(() => loading.value = false);
  await delOssConfig(ossConfigIds).finally(() => (loading.value = false));
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
onMounted(() => {
  getList();
})
});
</script>
src/views/system/oss/index.vue
@@ -1,19 +1,19 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
      <div v-show="showSearch" class="mb-[10px]">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
            <el-form-item label="文件名" prop="fileName">
              <el-input v-model="queryParams.fileName" placeholder="请输入文件名" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.fileName" placeholder="请输入文件名" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="原名" prop="originalName">
              <el-input v-model="queryParams.originalName" placeholder="请输入原名" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.originalName" placeholder="请输入原名" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="文件后缀" prop="fileSuffix">
              <el-input v-model="queryParams.fileSuffix" placeholder="请输入文件后缀" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.fileSuffix" placeholder="请输入文件后缀" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item label="创建时间">
            <el-form-item label="创建时间" style="width: 308px;">
              <el-date-picker
                v-model="dateRangeCreateTime"
                value-format="YYYY-MM-DD HH:mm:ss"
@@ -25,7 +25,7 @@
              ></el-date-picker>
            </el-form-item>
            <el-form-item label="服务商" prop="service">
              <el-input v-model="queryParams.service" placeholder="请输入服务商" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="queryParams.service" placeholder="请输入服务商" clearable @keyup.enter="handleQuery" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
@@ -40,44 +40,42 @@
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Upload" @click="handleFile" v-hasPermi="['system:oss:upload']">上传文件</el-button>
            <el-button v-hasPermi="['system:oss:upload']" type="primary" plain icon="Upload" @click="handleFile">上传文件</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Upload" @click="handleImage" v-hasPermi="['system:oss:upload']">上传图片</el-button>
            <el-button v-hasPermi="['system:oss:upload']" type="primary" plain icon="Upload" @click="handleImage">上传图片</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:oss:remove']">
            <el-button v-hasPermi="['system:oss:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
              åˆ é™¤
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button
              v-hasPermi="['system:oss:edit']"
              :type="previewListResource ? 'danger' : 'warning'"
              plain
              @click="handlePreviewListResource(!previewListResource)"
              v-hasPermi="['system:oss:edit']"
              >预览开关 :
              {{
                previewListResource ? "禁用" : "启用" }}</el-button
              >预览开关 : {{ previewListResource ? '禁用' : '启用' }}</el-button
            >
          </el-col>
          <el-col :span="1.5">
            <el-button type="info" plain icon="Operation" @click="handleOssConfig" v-hasPermi="['system:oss:list']">配置管理</el-button>
            <el-button v-hasPermi="['system:oss:list']" type="info" plain icon="Operation" @click="handleOssConfig">配置管理</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
          <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table
        v-if="showTable"
        v-loading="loading"
        :data="ossList"
        @selection-change="handleSelectionChange"
        :header-cell-class-name="handleHeaderClass"
        @selection-change="handleSelectionChange"
        @header-click="handleHeaderCLick"
        v-if="showTable"
      >
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="对象存储主键" align="center" prop="ossId" v-if="false" />
        <el-table-column v-if="false" label="对象存储主键" align="center" prop="ossId" />
        <el-table-column label="文件名" align="center" prop="fileName" />
        <el-table-column label="原名" align="center" prop="originalName" />
        <el-table-column label="文件后缀" align="center" prop="fileSuffix" />
@@ -90,7 +88,7 @@
              :src="scope.row.url"
              :preview-src-list="[scope.row.url]"
            />
            <span v-text="scope.row.url" v-if="!checkFileSuffix(scope.row.fileSuffix) || !previewListResource" />
            <span v-if="!checkFileSuffix(scope.row.fileSuffix) || !previewListResource" v-text="scope.row.url" />
          </template>
        </el-table-column>
        <el-table-column label="创建时间" align="center" prop="createTime" width="180" sortable="custom">
@@ -103,23 +101,23 @@
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="下载" placement="top">
              <el-button link type="primary" icon="Download" @click="handleDownload(scope.row)" v-hasPermi="['system:oss:download']"></el-button>
              <el-button v-hasPermi="['system:oss:download']" link type="primary" icon="Download" @click="handleDownload(scope.row)"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:oss:remove']"></el-button>
              <el-button v-hasPermi="['system:oss:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹OSS对象存储对话框 -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
      <el-form ref="ossFormRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="文件名">
          <fileUpload v-model="form.file" v-if="type === 0" />
          <imageUpload v-model="form.file" v-if="type === 1" />
          <fileUpload v-if="type === 0" v-model="form.file" />
          <imageUpload v-if="type === 1" v-model="form.file" />
        </el-form-item>
      </el-form>
      <template #footer>
@@ -133,9 +131,9 @@
</template>
<script setup name="Oss" lang="ts">
import { listOss, delOss } from "@/api/system/oss";
import ImagePreview from "@/components/ImagePreview/index.vue";
import { OssForm, OssQuery, OssVO } from "@/api/system/oss/types";
import { listOss, delOss } from '@/api/system/oss';
import ImagePreview from '@/components/ImagePreview/index.vue';
import { OssForm, OssQuery, OssVO } from '@/api/system/oss/types';
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -165,8 +163,8 @@
const queryFormRef = ref<ElFormInstance>();
const initFormData = {
  file: undefined,
}
  file: undefined
};
const data = reactive<PageData<OssForm, OssQuery>>({
  form: { ...initFormData },
  // æŸ¥è¯¢å‚æ•°
@@ -182,9 +180,7 @@
    isAsc: defaultSort.value.order
  },
  rules: {
    file: [
      { required: true, message: "文件不能为空", trigger: "blur" }
    ]
    file: [{ required: true, message: '文件不能为空', trigger: 'blur' }]
  }
});
@@ -193,19 +189,18 @@
/** æŸ¥è¯¢OSS对象存储列表 */
const getList = async () => {
  loading.value = true;
  const res = await proxy?.getConfigKey("sys.oss.previewListResource");
  const res = await proxy?.getConfigKey('sys.oss.previewListResource');
  previewListResource.value = res?.data === undefined ? true : res.data === 'true';
  const response = await listOss(proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, "CreateTime"));
  const response = await listOss(proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, 'CreateTime'));
  ossList.value = response.rows;
  total.value = response.total;
  loading.value = false;
  showTable.value = true;
}
function checkFileSuffix(fileSuffix: string[]) {
  let arr = ["png", "jpg", "jpeg"];
  return arr.some(type => {
    return fileSuffix.indexOf(type) > -1;
  });
};
function checkFileSuffix(fileSuffix: string | string[]) {
  const arr = [".png", ".jpg", ".jpeg"];
  const suffixArray = Array.isArray(fileSuffix) ? fileSuffix : [fileSuffix];
  return suffixArray.some(suffix => arr.includes(suffix.toLowerCase()));
}
/** å–消按钮 */
function cancel() {
@@ -233,18 +228,18 @@
}
/** é€‰æ‹©æ¡æ•°  */
function handleSelectionChange(selection: OssVO[]) {
  ids.value = selection.map(item => item.ossId);
  ids.value = selection.map((item) => item.ossId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
/** è®¾ç½®åˆ—的排序为我们自定义的排序 */
const handleHeaderClass = ({ column }: any): any => {
  column.order = column.multiOrder
}
  column.order = column.multiOrder;
};
/** ç‚¹å‡»è¡¨å¤´è¿›è¡ŒæŽ’序 */
const handleHeaderCLick = (column: any) => {
  if (column.sortable !== 'custom') {
    return
    return;
  }
  switch (column.multiOrder) {
    case 'descending':
@@ -257,20 +252,20 @@
      column.multiOrder = 'descending';
      break;
  }
  handleOrderChange(column.property, column.multiOrder)
}
  handleOrderChange(column.property, column.multiOrder);
};
const handleOrderChange = (prop: string, order: string) => {
  let orderByArr = queryParams.value.orderByColumn ? queryParams.value.orderByColumn.split(",") : [];
  let isAscArr = queryParams.value.isAsc ? queryParams.value.isAsc.split(",") : [];
  let propIndex = orderByArr.indexOf(prop)
  let orderByArr = queryParams.value.orderByColumn ? queryParams.value.orderByColumn.split(',') : [];
  let isAscArr = queryParams.value.isAsc ? queryParams.value.isAsc.split(',') : [];
  let propIndex = orderByArr.indexOf(prop);
  if (propIndex !== -1) {
    if (order) {
      //排序里已存在 åªä¿®æ”¹æŽ’序
      isAscArr[propIndex] = order;
    } else {
      //如果order为null åˆ™åˆ é™¤æŽ’序字段和属性
      isAscArr.splice(propIndex, 1);//删除排序
      orderByArr.splice(propIndex, 1);//删除属性
      isAscArr.splice(propIndex, 1); //删除排序
      orderByArr.splice(propIndex, 1); //删除属性
    }
  } else {
    //排序里不存在则新增排序
@@ -278,58 +273,60 @@
    isAscArr.push(order);
  }
  //合并排序
  queryParams.value.orderByColumn = orderByArr.join(",");
  queryParams.value.isAsc = isAscArr.join(",");
  queryParams.value.orderByColumn = orderByArr.join(',');
  queryParams.value.isAsc = isAscArr.join(',');
  getList();
}
};
/** ä»»åŠ¡æ—¥å¿—åˆ—è¡¨æŸ¥è¯¢ */
const handleOssConfig = () => {
  router.push('/system/oss-config/index')
}
  router.push('/system/oss-config/index');
};
/** æ–‡ä»¶æŒ‰é’®æ“ä½œ */
const handleFile = () => {
  reset();
  type.value = 0;
  dialog.visible = true;
  dialog.title = "上传文件";
}
  dialog.title = '上传文件';
};
/** å›¾ç‰‡æŒ‰é’®æ“ä½œ */
const handleImage = () => {
  reset();
  type.value = 1;
  dialog.visible = true;
  dialog.title = "上传图片";
}
  dialog.title = '上传图片';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  dialog.visible = false;
  getList();
}
};
/** ä¸‹è½½æŒ‰é’®æ“ä½œ */
const handleDownload = (row: OssVO) => {
  proxy?.$download.oss(row.ossId)
}
  proxy?.$download.oss(row.ossId);
};
/** ç”¨æˆ·çŠ¶æ€ä¿®æ”¹  */
const handlePreviewListResource = async (preview: boolean) => {
  let text = preview ? "启用" : "停用";
  let text = preview ? '启用' : '停用';
  try {
    await proxy?.$modal.confirm('确认要"' + text + '""预览列表图片"配置吗?');
    await proxy?.updateConfigByKey("sys.oss.previewListResource", preview);
    await getList()
    proxy?.$modal.msgSuccess(text + "成功");
  } catch { return }
}
    await proxy?.updateConfigByKey('sys.oss.previewListResource', preview);
    await getList();
    proxy?.$modal.msgSuccess(text + '成功');
  } catch {
    return;
  }
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: OssVO) => {
  const ossIds = row?.ossId || ids.value;
  await proxy?.$modal.confirm('是否确认删除OSS对象存储编号为"' + ossIds + '"的数据项?');
  loading.value = true;
  await delOss(ossIds).finally(() => loading.value = false);
  await delOss(ossIds).finally(() => (loading.value = false));
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
onMounted(() => {
  getList();
})
});
</script>
src/views/system/post/index.vue
@@ -1,117 +1,182 @@
<template>
  <div class="p-2">
    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
      <div class="mb-[10px]" v-show="showSearch">
    <el-row :gutter="20">
      <!-- éƒ¨é—¨æ ‘ -->
      <el-col :lg="4" :xs="24" style="">
        <el-card shadow="hover">
          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="70">
            <el-form-item label="岗位编码" prop="postCode">
              <el-input v-model="queryParams.postCode" placeholder="请输入岗位编码" clearable style="width: 200px" @keyup.enter="handleQuery" />
            </el-form-item>
          <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
          <el-tree
            ref="deptTreeRef"
            class="mt-2"
            node-key="id"
            :data="deptOptions"
            :props="{ label: 'label', children: 'children' }"
            :expand-on-click-node="false"
            :filter-node-method="filterNode"
            highlight-current
            default-expand-all
            @node-click="handleNodeClick"
          />
        </el-card>
      </el-col>
      <el-col :lg="20" :xs="24">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
          <div v-show="showSearch" class="mb-[10px]">
            <el-card shadow="hover">
              <el-form ref="queryFormRef" :model="queryParams" :inline="true">
                <el-form-item label="岗位编码" prop="postCode">
                  <el-input v-model="queryParams.postCode" placeholder="请输入岗位编码" clearable @keyup.enter="handleQuery" />
                </el-form-item>
                <el-form-item label="类别编码" prop="postCategory">
                  <el-input
                    v-model="queryParams.postCategory"
                    placeholder="请输入类别编码"
                    clearable
                    style="width: 200px"
                    @keyup.enter="handleQuery"
                  />
                </el-form-item>
                <el-form-item label="岗位名称" prop="postName">
                  <el-input v-model="queryParams.postName" placeholder="请输入岗位名称" clearable @keyup.enter="handleQuery" />
                </el-form-item>
                <el-form-item label="部门" prop="deptId">
                  <el-tree-select
                    v-model="queryParams.deptId"
                    :data="deptOptions"
                    :props="{ value: 'id', label: 'label', children: 'children' }"
                    value-key="id"
                    placeholder="请选择部门"
                    check-strictly
                  />
                </el-form-item>
                <el-form-item label="状态" prop="status">
                  <el-select v-model="queryParams.status" placeholder="岗位状态" clearable>
                    <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
                  </el-select>
                </el-form-item>
                <el-form-item>
                  <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                  <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                </el-form-item>
              </el-form>
            </el-card>
          </div>
        </transition>
        <el-card shadow="hover">
          <template #header>
            <el-row :gutter="10" class="mb8">
              <el-col :span="1.5">
                <el-button v-hasPermi="['system:post:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
              </el-col>
              <el-col :span="1.5">
                <el-button v-hasPermi="['system:post:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
                  >修改</el-button
                >
              </el-col>
              <el-col :span="1.5">
                <el-button v-hasPermi="['system:post:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
                  åˆ é™¤
                </el-button>
              </el-col>
              <el-col :span="1.5">
                <el-button v-hasPermi="['system:post:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
              </el-col>
              <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
            </el-row>
          </template>
          <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange">
            <el-table-column type="selection" width="55" align="center" />
            <el-table-column v-if="false" label="岗位编号" align="center" prop="postId" />
            <el-table-column label="岗位编码" align="center" prop="postCode" />
            <el-table-column label="类别编码" align="center" prop="postCategory" />
            <el-table-column label="岗位名称" align="center" prop="postName" />
            <el-table-column label="部门" align="center" prop="deptName" />
            <el-table-column label="排序" align="center" prop="postSort" />
            <el-table-column label="状态" align="center" prop="status">
              <template #default="scope">
                <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
              </template>
            </el-table-column>
            <el-table-column label="创建时间" align="center" prop="createTime" width="180">
              <template #default="scope">
                <span>{{ parseTime(scope.row.createTime) }}</span>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="180" align="center" class-name="small-padding fixed-width">
              <template #default="scope">
                <el-tooltip content="修改" placement="top">
                  <el-button v-hasPermi="['system:post:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
                </el-tooltip>
                <el-tooltip content="删除" placement="top">
                  <el-button v-hasPermi="['system:post:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
                </el-tooltip>
              </template>
            </el-table-column>
          </el-table>
          <pagination
            v-show="total > 0"
            v-model:page="queryParams.pageNum"
            v-model:limit="queryParams.pageSize"
            :total="total"
            @pagination="getList"
          />
        </el-card>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹å²—ä½å¯¹è¯æ¡† -->
        <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
          <el-form ref="postFormRef" :model="form" :rules="rules" label-width="80px">
            <el-form-item label="岗位名称" prop="postName">
              <el-input v-model="queryParams.postName" placeholder="请输入岗位名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
              <el-input v-model="form.postName" placeholder="请输入岗位名称" />
            </el-form-item>
            <el-form-item label="状态" prop="status">
              <el-select v-model="queryParams.status" placeholder="岗位状态" clearable style="width: 200px">
                <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
              </el-select>
            <el-form-item label="部门" prop="deptId">
              <el-tree-select
                v-model="form.deptId"
                :data="deptOptions"
                :props="{ value: 'id', label: 'label', children: 'children' }"
                value-key="id"
                placeholder="请选择部门"
                check-strictly
              />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
            <el-form-item label="岗位编码" prop="postCode">
              <el-input v-model="form.postCode" placeholder="请输入编码名称" />
            </el-form-item>
            <el-form-item label="类别编码" prop="postCategory">
              <el-input v-model="form.postCategory" placeholder="请输入类别编码" />
            </el-form-item>
            <el-form-item label="岗位顺序" prop="postSort">
              <el-input-number v-model="form.postSort" controls-position="right" :min="0" />
            </el-form-item>
            <el-form-item label="岗位状态" prop="status">
              <el-radio-group v-model="form.status">
                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
              </el-radio-group>
            </el-form-item>
            <el-form-item label="备注" prop="remark">
              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
            </el-form-item>
          </el-form>
        </el-card>
      </div>
    </transition>
    <el-card shadow="hover">
      <template #header>
        <el-row :gutter="10" class="mb8">
          <el-col :span="1.5">
            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:post:add']">新增</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:post:edit']">修改</el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:post:remove']">
              åˆ é™¤
            </el-button>
          </el-col>
          <el-col :span="1.5">
            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:post:export']">导出</el-button>
          </el-col>
          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
        </el-row>
      </template>
      <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="岗位编号" align="center" prop="postId" v-if="false" />
        <el-table-column label="岗位编码" align="center" prop="postCode" />
        <el-table-column label="岗位名称" align="center" prop="postName" />
        <el-table-column label="岗位排序" align="center" prop="postSort" />
        <el-table-column label="状态" align="center" prop="status">
          <template #default="scope">
            <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
          <template #footer>
            <div class="dialog-footer">
              <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
              <el-button @click="cancel">取 æ¶ˆ</el-button>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
          <template #default="scope">
            <span>{{ parseTime(scope.row.createTime) }}</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180" align="center" class-name="small-padding fixed-width">
          <template #default="scope">
            <el-tooltip content="修改" placement="top">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:post:edit']"></el-button>
            </el-tooltip>
            <el-tooltip content="删除" placement="top">
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:post:remove']"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
    </el-card>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å²—ä½å¯¹è¯æ¡† -->
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
      <el-form ref="postFormRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="岗位名称" prop="postName">
          <el-input v-model="form.postName" placeholder="请输入岗位名称" />
        </el-form-item>
        <el-form-item label="岗位编码" prop="postCode">
          <el-input v-model="form.postCode" placeholder="请输入编码名称" />
        </el-form-item>
        <el-form-item label="岗位顺序" prop="postSort">
          <el-input-number v-model="form.postSort" controls-position="right" :min="0" />
        </el-form-item>
        <el-form-item label="岗位状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
        </el-dialog>
      </el-col>
    </el-row>
  </div>
</template>
<script setup name="Post" lang="ts">
import { listPost, addPost, delPost, getPost, updatePost } from "@/api/system/post";
import { PostForm, PostQuery, PostVO } from "@/api/system/post/types";
import { listPost, addPost, delPost, getPost, updatePost } from '@/api/system/post';
import { PostForm, PostQuery, PostVO } from '@/api/system/post/types';
import { DeptVO } from '@/api/system/dept/types';
import api from '@/api/system/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const postList = ref<PostVO[]>([]);
const loading = ref(true);
@@ -120,7 +185,9 @@
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const deptName = ref('');
const deptOptions = ref<DeptVO[]>([]);
const deptTreeRef = ref<ElTreeInstance>();
const postFormRef = ref<ElFormInstance>();
const queryFormRef = ref<ElFormInstance>();
@@ -131,12 +198,14 @@
const initFormData: PostForm = {
  postId: undefined,
  deptId: undefined,
  postCode: '',
  postName: '',
  postCategory: '',
  postSort: 0,
  status: "0",
  status: '0',
  remark: ''
}
};
const data = reactive<PageData<PostForm, PostQuery>>({
  form: { ...initFormData },
@@ -145,16 +214,47 @@
    pageSize: 10,
    postCode: '',
    postName: '',
    postCategory: '',
    status: ''
  },
  rules: {
    postName: [{ required: true, message: "岗位名称不能为空", trigger: "blur" }],
    postCode: [{ required: true, message: "岗位编码不能为空", trigger: "blur" }],
    postSort: [{ required: true, message: "岗位顺序不能为空", trigger: "blur" }],
    postName: [{ required: true, message: '岗位名称不能为空', trigger: 'blur' }],
    postCode: [{ required: true, message: '岗位编码不能为空', trigger: 'blur' }],
    deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
    postSort: [{ required: true, message: '岗位顺序不能为空', trigger: 'blur' }]
  }
});
const { queryParams, form, rules } = toRefs<PageData<PostForm, PostQuery>>(data);
/** é€šè¿‡æ¡ä»¶è¿‡æ»¤èŠ‚ç‚¹  */
const filterNode = (value: string, data: any) => {
  if (!value) return true;
  return data.label.indexOf(value) !== -1;
};
/** æ ¹æ®åç§°ç­›é€‰éƒ¨é—¨æ ‘ */
watchEffect(
  () => {
    deptTreeRef.value?.filter(deptName.value);
  },
  {
    flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
  }
);
/** æŸ¥è¯¢éƒ¨é—¨ä¸‹æ‹‰æ ‘结构 */
const getTreeSelect = async () => {
  const res = await api.deptTreeSelect();
  deptOptions.value = res.data;
};
/** èŠ‚ç‚¹å•å‡»äº‹ä»¶ */
const handleNodeClick = (data: DeptVO) => {
  queryParams.value.belongDeptId = data.id;
  queryParams.value.deptId = undefined;
  handleQuery();
};
/** æŸ¥è¯¢å²—位列表 */
const getList = async () => {
@@ -163,39 +263,54 @@
  postList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
};
/** å–消按钮 */
const cancel = () => {
  reset();
  dialog.visible = false;
}
};
/** è¡¨å•重置 */
const reset = () => {
  form.value = { ...initFormData };
  postFormRef.value?.resetFields();
}
};
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  if (queryParams.value.deptId) {
    queryParams.value.belongDeptId = undefined;
  }
  getList();
}
};
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
  queryFormRef.value?.resetFields();
  queryParams.value.pageNum = 1;
  queryParams.value.deptId = undefined;
  deptTreeRef.value?.setCurrentKey(undefined);
  /** æ¸…空左边部门树选中值 */
  queryParams.value.belongDeptId = undefined;
  handleQuery();
}
};
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
const handleSelectionChange = (selection: PostVO[]) => {
  ids.value = selection.map(item => item.postId);
  ids.value = selection.map((item) => item.postId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
};
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = () => {
  reset();
  dialog.visible = true;
  dialog.title = "添加岗位";
}
  dialog.title = '添加岗位';
};
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row?: PostVO) => {
  reset();
@@ -203,35 +318,43 @@
  const res = await getPost(postId);
  Object.assign(form.value, res.data);
  dialog.visible = true;
  dialog.title = "修改岗位";
}
  dialog.title = '修改岗位';
};
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  postFormRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      form.value.postId ? await updatePost(form.value) : await addPost(form.value);
      proxy?.$modal.msgSuccess("操作成功");
      proxy?.$modal.msgSuccess('操作成功');
      dialog.visible = false;
      await getList();
    }
  });
}
};
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row?: PostVO) => {
  const postIds = row?.postId || ids.value;
  await proxy?.$modal.confirm('是否确认删除岗位编号为"' + postIds + '"的数据项?');
  await delPost(postIds);
  await getList();
  proxy?.$modal.msgSuccess("删除成功");
}
  proxy?.$modal.msgSuccess('删除成功');
};
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
const handleExport = () => {
  proxy?.download("system/post/export", {
    ...queryParams.value
  }, `post_${new Date().getTime()}.xlsx`);
}
  proxy?.download(
    'system/post/export',
    {
      ...queryParams.value
    },
    `post_${new Date().getTime()}.xlsx`
  );
};
onMounted(() => {
  getTreeSelect(); // åˆå§‹åŒ–部门数据
  getList();
});
</script>
在上述文件截断后对比
src/views/system/role/authUser.vue src/views/system/role/index.vue src/views/system/role/selectUser.vue src/views/system/tenant/index.vue src/views/system/tenantPackage/index.vue src/views/system/user/authRole.vue src/views/system/user/index.vue src/views/system/user/profile/index.vue src/views/system/user/profile/onlineDevice.vue src/views/system/user/profile/resetPwd.vue src/views/system/user/profile/thirdParty.vue src/views/system/user/profile/userAvatar.vue src/views/system/user/profile/userInfo.vue src/views/tool/gen/basicInfoForm.vue src/views/tool/gen/editTable.vue src/views/tool/gen/genInfoForm.vue src/views/tool/gen/importTable.vue src/views/tool/gen/index.vue src/views/workflow/category/index.vue src/views/workflow/formManage/index.vue src/views/workflow/leave/index.vue src/views/workflow/leave/leaveEdit.vue src/views/workflow/model/index.vue src/views/workflow/processDefinition/components/processPreview.vue src/views/workflow/processDefinition/index.vue src/views/workflow/processInstance/index.vue src/views/workflow/task/allTaskWaiting.vue src/views/workflow/task/myDocument.vue src/views/workflow/task/taskCopyList.vue src/views/workflow/task/taskFinish.vue src/views/workflow/task/taskWaiting.vue tsconfig.json uno.config.ts vite.config.ts vite/plugins/compression.ts vite/plugins/i18n.ts vite/plugins/index.ts vite/plugins/setup-extend.ts vite/plugins/unocss.ts