From 759813095e6f8b999b35fae17a7ec235fd89f561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E6=A0=A1=E4=BA=91?= <14135925+chenxilxy@user.noreply.gitee.com> Date: Thu, 26 Feb 2026 15:11:34 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 15 + .env.development | 19 + .env.production | 19 + .eslintrc-auto-import.json | 316 +++++ .gitignore | 20 + .prettierignore | 12 + .prettierrc.yaml | 41 + .stylelintignore | 11 + .stylelintrc.cjs | 38 + .vscode/settings.json | 71 ++ .vscode/vue3.0.code-snippets | 23 + .vscode/vue3.2.code-snippets | 17 + .vscode/vue3.3.code-snippets | 21 + CHANGELOG.md | 386 ++++++ LICENSE | 21 + README.md | 11 + commitlint.config.cjs | 93 ++ eslint.config.ts | 238 ++++ index.html | 93 ++ logo.png | Bin 0 -> 52081 bytes package.json | 131 ++ public/favicon.ico | Bin 0 -> 4286 bytes public/img/logo.png | Bin 0 -> 52081 bytes src/App.vue | 17 + src/api/BoosAccountManagement/index.ts | 65 + src/api/TaskManagement/index.ts | 67 + src/api/ai/index.ts | 191 +++ src/api/auth-api.ts | 86 ++ .../accountAdjustmentApplication/index.ts | 74 ++ .../announcementManagement/index.ts | 75 ++ .../applicationForSealApproval/index.ts | 82 ++ src/api/calibration/approval/index.ts | 56 + src/api/calibration/bidRegistration/index.ts | 78 ++ src/api/calibration/caseLabel/index.ts | 128 ++ src/api/calibration/caseManagement/index.ts | 272 +++++ .../conflictOfInterestSearch/index.ts | 110 ++ src/api/calibration/department/index.ts | 63 + .../departureFinancialRegistration/index.ts | 72 ++ .../inventoryRegistration/index.ts | 77 ++ .../calibration/invoiceApplication/index.ts | 81 ++ .../lawFirmStandardDocuments/index.ts | 45 + .../lmportantScheduleManagement/index.ts | 85 ++ src/api/calibration/login/index.ts | 40 + .../onboardingRegistration/index.ts | 54 + .../paymentApplicationForm/index.ts | 84 ++ .../calibration/permissionManagement/index.ts | 72 ++ .../calibration/personnelManagement/index.ts | 125 ++ src/api/calibration/preRegistration/index.ts | 96 ++ .../calibration/registrationPlatform/index.ts | 74 ++ src/api/calibration/reimbursement/index.ts | 78 ++ .../calibration/revenueRecognition/index.ts | 76 ++ src/api/calibration/roleManagement/index.ts | 94 ++ .../salaryBonusAdjustment/index.ts | 74 ++ src/api/calibration/systemManagement/index.ts | 75 ++ src/api/calibration/teamManagement/index.ts | 82 ++ src/api/system/config-api.ts | 70 ++ src/api/system/dept-api.ts | 75 ++ src/api/system/dict-api.ts | 145 +++ src/api/system/log-api.ts | 89 ++ src/api/system/menu-api.ts | 135 ++ src/api/system/notice-api.ts | 121 ++ src/api/system/role-api.ts | 79 ++ src/api/system/user-api.ts | 384 ++++++ src/assets/font-icon/Excel.svg | 1 + src/assets/font-icon/PDF.svg | 1 + src/assets/font-icon/Ppt.svg | 1 + src/assets/font-icon/Txt.svg | 1 + src/assets/font-icon/Word.svg | 1 + src/assets/font-icon/shiping.svg | 1 + src/assets/font-icon/tupian.svg | 1 + src/assets/font-icon/weizhi.svg | 1 + src/assets/font-icon/yasuo.svg | 1 + src/assets/icons/ai.svg | 1 + src/assets/icons/api.svg | 1 + src/assets/icons/backtop.svg | 1 + src/assets/icons/bell.svg | 1 + src/assets/icons/bilibili.svg | 1 + src/assets/icons/browser.svg | 1 + src/assets/icons/captcha.svg | 1 + src/assets/icons/cascader.svg | 1 + src/assets/icons/client.svg | 1 + src/assets/icons/close.svg | 1 + src/assets/icons/close_all.svg | 1 + src/assets/icons/close_left.svg | 1 + src/assets/icons/close_other.svg | 1 + src/assets/icons/close_right.svg | 1 + src/assets/icons/cnblogs.svg | 1 + src/assets/icons/code.svg | 1 + src/assets/icons/collapse.svg | 1 + src/assets/icons/csdn.svg | 6 + src/assets/icons/dict.svg | 1 + src/assets/icons/document.svg | 1 + src/assets/icons/down.svg | 1 + src/assets/icons/download.svg | 1 + src/assets/icons/enter.svg | 1 + src/assets/icons/esc.svg | 1 + src/assets/icons/file.svg | 1 + src/assets/icons/fullscreen-exit.svg | 1 + src/assets/icons/fullscreen.svg | 1 + src/assets/icons/gitcode.svg | 1 + src/assets/icons/gitee.svg | 1 + src/assets/icons/github.svg | 1 + src/assets/icons/homepage.svg | 1 + src/assets/icons/java.svg | 1 + src/assets/icons/juejin.svg | 1 + src/assets/icons/language.svg | 1 + src/assets/icons/menu.svg | 1 + src/assets/icons/message.svg | 1 + src/assets/icons/monitor.svg | 1 + src/assets/icons/project.svg | 1 + src/assets/icons/qq.svg | 1 + src/assets/icons/refresh.svg | 1 + src/assets/icons/role.svg | 1 + src/assets/icons/search.svg | 1 + src/assets/icons/setting.svg | 1 + src/assets/icons/size.svg | 1 + src/assets/icons/system.svg | 1 + src/assets/icons/table.svg | 1 + src/assets/icons/todo.svg | 1 + src/assets/icons/tree.svg | 1 + src/assets/icons/typescript.svg | 1 + src/assets/icons/up.svg | 1 + src/assets/icons/user.svg | 1 + src/assets/icons/visitor.svg | 1 + src/assets/icons/vue.svg | 1 + src/assets/icons/wechat.svg | 1 + src/assets/icons/xml.svg | 1 + src/assets/images/401.svg | 398 ++++++ src/assets/images/404.svg | 340 ++++++ src/assets/images/login-bg.svg | 73 ++ src/assets/images/login-bg1.svg | 71 ++ src/assets/logo.png | Bin 0 -> 52081 bytes src/assets/user.png | Bin 0 -> 5205 bytes src/components/AppLink/index.vue | 38 + src/components/Breadcrumb/index.vue | 85 ++ src/components/CURD/PageContent.vue | 933 ++++++++++++++ src/components/CURD/PageModal.vue | 273 +++++ src/components/CURD/PageSearch.vue | 169 +++ src/components/CURD/types.ts | 223 ++++ src/components/CURD/usePage.ts | 105 ++ src/components/CommonWrapper/index.vue | 21 + src/components/DarkModeSwitch/index.vue | 39 + src/components/Fullscreen/index.vue | 11 + src/components/Hamburger/index.vue | 66 + src/components/LangSelect/index.vue | 49 + src/components/Notification/ApprovalForm.vue | 126 ++ .../ConfirmationOfProxyServiceForm.vue | 48 + .../Notification/ViewTheConflictList.vue | 189 +++ src/components/Notification/index.vue | 207 ++++ src/components/Pagination/index.vue | 91 ++ src/components/SizeSelect/index.vue | 40 + src/composables/auth/useTokenRefresh.ts | 91 ++ src/composables/index.ts | 14 + src/composables/layout/useDeviceDetection.ts | 40 + src/composables/layout/useLayout.ts | 62 + src/composables/layout/useLayoutMenu.ts | 39 + src/composables/useAiAction.ts | 270 ++++ src/composables/useTableSelection.ts | 63 + src/composables/websocket/useDictSync.ts | 205 ++++ src/composables/websocket/useOnlineCount.ts | 217 ++++ src/composables/websocket/useStomp.ts | 530 ++++++++ src/constants/index.ts | 74 ++ src/directives/index.ts | 8 + src/directives/permission/index.ts | 58 + src/enums/api/code-enum.ts | 23 + src/enums/codegen/form-enum.ts | 15 + src/enums/codegen/query-enum.ts | 37 + src/enums/index.ts | 11 + src/enums/settings/device-enum.ts | 14 + src/enums/settings/layout-enum.ts | 53 + src/enums/settings/locale-enum.ts | 14 + src/enums/settings/theme-enum.ts | 32 + src/enums/system/menu-enum.ts | 7 + src/env.d.ts | 8 + src/lang/index.ts | 27 + src/lang/package/en.json | 91 ++ src/lang/package/zh-cn.json | 94 ++ src/layouts/components/AppLogo/index.vue | 72 ++ src/layouts/components/AppMain/index.vue | 91 ++ src/layouts/components/Menu/BasicMenu.vue | 245 ++++ src/layouts/components/Menu/MixTopMenu.vue | 187 +++ .../components/Menu/components/MenuItem.vue | 229 ++++ .../Menu/components/MenuItemContent.vue | 30 + .../NavBar/components/NavbarActions.vue | 272 +++++ src/layouts/components/NavBar/index.vue | 49 + src/layouts/components/Settings/index.vue | 562 +++++++++ src/layouts/components/TagsView/index.vue | 410 +++++++ src/layouts/index.vue | 46 + src/layouts/modes/base/index.vue | 36 + src/layouts/modes/left/index.vue | 132 ++ src/layouts/modes/mix/index.vue | 281 +++++ src/layouts/modes/top/index.vue | 136 +++ src/main.ts | 24 + src/plugins/icons.ts | 9 + src/plugins/index.ts | 34 + src/plugins/permission.ts | 195 +++ src/plugins/vxeTable.ts | 70 ++ src/plugins/websocket.ts | 142 +++ src/router/index.ts | 437 +++++++ src/settings.ts | 67 + src/store/index.ts | 17 + src/store/modules/app-store.ts | 108 ++ src/store/modules/dict-store.ts | 79 ++ src/store/modules/permission-store.ts | 103 ++ src/store/modules/settings-store.ts | 176 +++ src/store/modules/tags-view-store.ts | 275 +++++ src/store/modules/user-store.ts | 165 +++ src/styles/dark/css-vars.css | 7 + src/styles/element-plus.scss | 45 + src/styles/index.scss | 121 ++ src/styles/reset.scss | 77 ++ src/styles/variables.module.scss | 11 + src/styles/variables.scss | 93 ++ src/styles/vxe-table.css | 92 ++ src/styles/vxe-table.scss | 39 + src/types/auto-imports.d.ts | 991 +++++++++++++++ src/types/components.d.ts | 90 ++ src/types/env.d.ts | 35 + src/types/global.d.ts | 111 ++ src/types/router.d.ts | 54 + src/types/shims-vue.d.ts | 5 + src/types/socket.d.ts | 6 + src/utils/TimerManager/index.ts | 30 + src/utils/auth.ts | 76 ++ src/utils/auxiliaryFunction/index.ts | 257 ++++ src/utils/functionDialogBox/index.ts | 102 ++ src/utils/i18n.ts | 12 + src/utils/index.ts | 58 + src/utils/nprogress.ts | 18 + src/utils/request.ts | 99 ++ src/utils/storage.ts | 101 ++ src/utils/theme.ts | 112 ++ .../components/BoosAccountDetails.vue | 7 + .../components/BoosAccountForm.vue | 108 ++ src/views/BoosAccountManagement/index.vue | 257 ++++ .../TaskManagement/components/TaskDetails.vue | 7 + .../TaskManagement/components/TaskForm.vue | 114 ++ src/views/TaskManagement/index.vue | 254 ++++ .../components/DeptTree.vue | 83 ++ .../components/UserImport.vue | 198 +++ .../accountAdjustmentApplication/index.vue | 323 +++++ .../components/AnnouncementForm.vue | 163 +++ .../announcementManagement/index.vue | 234 ++++ .../components/StampingForm.vue | 213 ++++ .../applicationForSealApproval/index.vue | 294 +++++ .../components/BidRegistrationForm.vue | 269 ++++ .../components/BusinessProjectForm.vue | 257 ++++ .../businessSystem/bidRegistration/index.vue | 313 +++++ .../components/BidRegistrationForm.vue | 305 +++++ .../components/BusinessProjectForm.vue | 257 ++++ .../conflictOfInterestSearch/index.vue | 486 ++++++++ .../components/PreRegistrationForm.vue | 364 ++++++ .../businessSystem/preRegistration/index.vue | 307 +++++ .../components/AdjustTheYearForm.vue | 91 ++ .../components/BidRegistrationForm.vue | 305 +++++ .../components/BusinessProjectForm.vue | 555 +++++++++ .../projectRegistration/index.vue | 342 ++++++ .../caseLabel/components/CaseLabelForm.vue | 108 ++ src/views/calibration/caseLabel/index.vue | 261 ++++ .../components/CaseLogForm.vue | 103 ++ .../components/CaseLogComponent/index.vue | 181 +++ .../components/CaseManagementForm.vue | 227 ++++ .../components/CaseInvoiceForm.vue | 133 ++ .../ChangeApplicationComponent/index.vue | 190 +++ .../CumulativeReceivedPaymentForm.vue | 88 ++ .../components/UploadOfDataForm.vue | 140 +++ .../calibration/caseManagement/index.vue | 604 +++++++++ src/views/calibration/department/index.vue | 258 ++++ .../components/SeparationRegistrationForm.vue | 132 ++ .../components/StampingForm.vue | 217 ++++ .../departureFinancialRegistration/index.vue | 284 +++++ .../components/StampingForm.vue | 176 +++ .../inventoryRegistration/index.vue | 260 ++++ .../components/DeptTree.vue | 83 ++ .../components/UserImport.vue | 198 +++ .../calibration/invoiceApplication/index.vue | 346 ++++++ .../components/LawFirmStandardsForm.vue | 153 +++ .../lawFirmStandardDocuments/index.vue | 255 ++++ .../components/LmportantScheduleForm.vue | 137 +++ .../lmportantScheduleManagement/index.vue | 265 ++++ .../components/DepartmentSelectionForm.vue | 111 ++ .../onboardingRegistration/index.vue | 294 +++++ .../components/DeptTree.vue | 83 ++ .../components/UserImport.vue | 198 +++ .../paymentApplicationForm/index.vue | 368 ++++++ .../components/permissionForm.vue | 134 ++ .../permissionManagement/index.vue | 235 ++++ .../components/DeptTree.vue | 83 ++ .../components/EditorPasswordForm.vue | 112 ++ .../components/UserImport.vue | 198 +++ .../calibration/personnelManagement/index.vue | 1088 +++++++++++++++++ .../components/RegistrationPlatformForm.vue | 130 ++ .../registrationPlatform/index.vue | 221 ++++ .../reimbursement/components/DeptTree.vue | 83 ++ .../reimbursement/components/UserImport.vue | 198 +++ src/views/calibration/reimbursement/index.vue | 349 ++++++ .../components/DeptTree.vue | 83 ++ .../components/UserImport.vue | 198 +++ .../calibration/revenueRecognition/index.vue | 343 ++++++ .../components/GrantPermissionsForm.vue | 106 ++ .../roleManagement/components/RoleForm.vue | 104 ++ .../calibration/roleManagement/index.vue | 261 ++++ .../components/DeptTree.vue | 83 ++ .../components/UserImport.vue | 198 +++ .../salaryBonusAdjustment/index.vue | 340 ++++++ .../components/SystemForm.vue | 161 +++ .../calibration/systemManagement/index.vue | 234 ++++ .../components/GrantPermissionsForm.vue | 106 ++ .../teamManagement/components/RoleForm.vue | 104 ++ .../teamManagement/components/TeamForm.vue | 111 ++ .../calibration/teamManagement/index.vue | 230 ++++ src/views/dashboard/AnnouncementDetails.vue | 79 ++ src/views/dashboard/config.ts | 106 ++ src/views/dashboard/index.vue | 588 +++++++++ src/views/error/401.vue | 25 + src/views/error/404.vue | 64 + src/views/login/components/Login.vue | 156 +++ src/views/login/index.vue | 118 ++ tsconfig.json | 40 + uno.config.ts | 82 ++ vite.config.ts | 252 ++++ 321 files changed, 37486 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.development create mode 100644 .env.production create mode 100644 .eslintrc-auto-import.json create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.yaml create mode 100644 .stylelintignore create mode 100644 .stylelintrc.cjs create mode 100644 .vscode/settings.json create mode 100644 .vscode/vue3.0.code-snippets create mode 100644 .vscode/vue3.2.code-snippets create mode 100644 .vscode/vue3.3.code-snippets create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 commitlint.config.cjs create mode 100644 eslint.config.ts create mode 100644 index.html create mode 100644 logo.png create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 public/img/logo.png create mode 100644 src/App.vue create mode 100644 src/api/BoosAccountManagement/index.ts create mode 100644 src/api/TaskManagement/index.ts create mode 100644 src/api/ai/index.ts create mode 100644 src/api/auth-api.ts create mode 100644 src/api/calibration/accountAdjustmentApplication/index.ts create mode 100644 src/api/calibration/announcementManagement/index.ts create mode 100644 src/api/calibration/applicationForSealApproval/index.ts create mode 100644 src/api/calibration/approval/index.ts create mode 100644 src/api/calibration/bidRegistration/index.ts create mode 100644 src/api/calibration/caseLabel/index.ts create mode 100644 src/api/calibration/caseManagement/index.ts create mode 100644 src/api/calibration/conflictOfInterestSearch/index.ts create mode 100644 src/api/calibration/department/index.ts create mode 100644 src/api/calibration/departureFinancialRegistration/index.ts create mode 100644 src/api/calibration/inventoryRegistration/index.ts create mode 100644 src/api/calibration/invoiceApplication/index.ts create mode 100644 src/api/calibration/lawFirmStandardDocuments/index.ts create mode 100644 src/api/calibration/lmportantScheduleManagement/index.ts create mode 100644 src/api/calibration/login/index.ts create mode 100644 src/api/calibration/onboardingRegistration/index.ts create mode 100644 src/api/calibration/paymentApplicationForm/index.ts create mode 100644 src/api/calibration/permissionManagement/index.ts create mode 100644 src/api/calibration/personnelManagement/index.ts create mode 100644 src/api/calibration/preRegistration/index.ts create mode 100644 src/api/calibration/registrationPlatform/index.ts create mode 100644 src/api/calibration/reimbursement/index.ts create mode 100644 src/api/calibration/revenueRecognition/index.ts create mode 100644 src/api/calibration/roleManagement/index.ts create mode 100644 src/api/calibration/salaryBonusAdjustment/index.ts create mode 100644 src/api/calibration/systemManagement/index.ts create mode 100644 src/api/calibration/teamManagement/index.ts create mode 100644 src/api/system/config-api.ts create mode 100644 src/api/system/dept-api.ts create mode 100644 src/api/system/dict-api.ts create mode 100644 src/api/system/log-api.ts create mode 100644 src/api/system/menu-api.ts create mode 100644 src/api/system/notice-api.ts create mode 100644 src/api/system/role-api.ts create mode 100644 src/api/system/user-api.ts create mode 100644 src/assets/font-icon/Excel.svg create mode 100644 src/assets/font-icon/PDF.svg create mode 100644 src/assets/font-icon/Ppt.svg create mode 100644 src/assets/font-icon/Txt.svg create mode 100644 src/assets/font-icon/Word.svg create mode 100644 src/assets/font-icon/shiping.svg create mode 100644 src/assets/font-icon/tupian.svg create mode 100644 src/assets/font-icon/weizhi.svg create mode 100644 src/assets/font-icon/yasuo.svg create mode 100644 src/assets/icons/ai.svg create mode 100644 src/assets/icons/api.svg create mode 100644 src/assets/icons/backtop.svg create mode 100644 src/assets/icons/bell.svg create mode 100644 src/assets/icons/bilibili.svg create mode 100644 src/assets/icons/browser.svg create mode 100644 src/assets/icons/captcha.svg create mode 100644 src/assets/icons/cascader.svg create mode 100644 src/assets/icons/client.svg create mode 100644 src/assets/icons/close.svg create mode 100644 src/assets/icons/close_all.svg create mode 100644 src/assets/icons/close_left.svg create mode 100644 src/assets/icons/close_other.svg create mode 100644 src/assets/icons/close_right.svg create mode 100644 src/assets/icons/cnblogs.svg create mode 100644 src/assets/icons/code.svg create mode 100644 src/assets/icons/collapse.svg create mode 100644 src/assets/icons/csdn.svg create mode 100644 src/assets/icons/dict.svg create mode 100644 src/assets/icons/document.svg create mode 100644 src/assets/icons/down.svg create mode 100644 src/assets/icons/download.svg create mode 100644 src/assets/icons/enter.svg create mode 100644 src/assets/icons/esc.svg create mode 100644 src/assets/icons/file.svg create mode 100644 src/assets/icons/fullscreen-exit.svg create mode 100644 src/assets/icons/fullscreen.svg create mode 100644 src/assets/icons/gitcode.svg create mode 100644 src/assets/icons/gitee.svg create mode 100644 src/assets/icons/github.svg create mode 100644 src/assets/icons/homepage.svg create mode 100644 src/assets/icons/java.svg create mode 100644 src/assets/icons/juejin.svg create mode 100644 src/assets/icons/language.svg create mode 100644 src/assets/icons/menu.svg create mode 100644 src/assets/icons/message.svg create mode 100644 src/assets/icons/monitor.svg create mode 100644 src/assets/icons/project.svg create mode 100644 src/assets/icons/qq.svg create mode 100644 src/assets/icons/refresh.svg create mode 100644 src/assets/icons/role.svg create mode 100644 src/assets/icons/search.svg create mode 100644 src/assets/icons/setting.svg create mode 100644 src/assets/icons/size.svg create mode 100644 src/assets/icons/system.svg create mode 100644 src/assets/icons/table.svg create mode 100644 src/assets/icons/todo.svg create mode 100644 src/assets/icons/tree.svg create mode 100644 src/assets/icons/typescript.svg create mode 100644 src/assets/icons/up.svg create mode 100644 src/assets/icons/user.svg create mode 100644 src/assets/icons/visitor.svg create mode 100644 src/assets/icons/vue.svg create mode 100644 src/assets/icons/wechat.svg create mode 100644 src/assets/icons/xml.svg create mode 100644 src/assets/images/401.svg create mode 100644 src/assets/images/404.svg create mode 100644 src/assets/images/login-bg.svg create mode 100644 src/assets/images/login-bg1.svg create mode 100644 src/assets/logo.png create mode 100644 src/assets/user.png create mode 100644 src/components/AppLink/index.vue create mode 100644 src/components/Breadcrumb/index.vue create mode 100644 src/components/CURD/PageContent.vue create mode 100644 src/components/CURD/PageModal.vue create mode 100644 src/components/CURD/PageSearch.vue create mode 100644 src/components/CURD/types.ts create mode 100644 src/components/CURD/usePage.ts create mode 100644 src/components/CommonWrapper/index.vue create mode 100644 src/components/DarkModeSwitch/index.vue create mode 100644 src/components/Fullscreen/index.vue create mode 100644 src/components/Hamburger/index.vue create mode 100644 src/components/LangSelect/index.vue create mode 100644 src/components/Notification/ApprovalForm.vue create mode 100644 src/components/Notification/ConfirmationOfProxyServiceForm.vue create mode 100644 src/components/Notification/ViewTheConflictList.vue create mode 100644 src/components/Notification/index.vue create mode 100644 src/components/Pagination/index.vue create mode 100644 src/components/SizeSelect/index.vue create mode 100644 src/composables/auth/useTokenRefresh.ts create mode 100644 src/composables/index.ts create mode 100644 src/composables/layout/useDeviceDetection.ts create mode 100644 src/composables/layout/useLayout.ts create mode 100644 src/composables/layout/useLayoutMenu.ts create mode 100644 src/composables/useAiAction.ts create mode 100644 src/composables/useTableSelection.ts create mode 100644 src/composables/websocket/useDictSync.ts create mode 100644 src/composables/websocket/useOnlineCount.ts create mode 100644 src/composables/websocket/useStomp.ts create mode 100644 src/constants/index.ts create mode 100644 src/directives/index.ts create mode 100644 src/directives/permission/index.ts create mode 100644 src/enums/api/code-enum.ts create mode 100644 src/enums/codegen/form-enum.ts create mode 100644 src/enums/codegen/query-enum.ts create mode 100644 src/enums/index.ts create mode 100644 src/enums/settings/device-enum.ts create mode 100644 src/enums/settings/layout-enum.ts create mode 100644 src/enums/settings/locale-enum.ts create mode 100644 src/enums/settings/theme-enum.ts create mode 100644 src/enums/system/menu-enum.ts create mode 100644 src/env.d.ts create mode 100644 src/lang/index.ts create mode 100644 src/lang/package/en.json create mode 100644 src/lang/package/zh-cn.json create mode 100644 src/layouts/components/AppLogo/index.vue create mode 100644 src/layouts/components/AppMain/index.vue create mode 100644 src/layouts/components/Menu/BasicMenu.vue create mode 100644 src/layouts/components/Menu/MixTopMenu.vue create mode 100644 src/layouts/components/Menu/components/MenuItem.vue create mode 100644 src/layouts/components/Menu/components/MenuItemContent.vue create mode 100644 src/layouts/components/NavBar/components/NavbarActions.vue create mode 100644 src/layouts/components/NavBar/index.vue create mode 100644 src/layouts/components/Settings/index.vue create mode 100644 src/layouts/components/TagsView/index.vue create mode 100644 src/layouts/index.vue create mode 100644 src/layouts/modes/base/index.vue create mode 100644 src/layouts/modes/left/index.vue create mode 100644 src/layouts/modes/mix/index.vue create mode 100644 src/layouts/modes/top/index.vue create mode 100644 src/main.ts create mode 100644 src/plugins/icons.ts create mode 100644 src/plugins/index.ts create mode 100644 src/plugins/permission.ts create mode 100644 src/plugins/vxeTable.ts create mode 100644 src/plugins/websocket.ts create mode 100644 src/router/index.ts create mode 100644 src/settings.ts create mode 100644 src/store/index.ts create mode 100644 src/store/modules/app-store.ts create mode 100644 src/store/modules/dict-store.ts create mode 100644 src/store/modules/permission-store.ts create mode 100644 src/store/modules/settings-store.ts create mode 100644 src/store/modules/tags-view-store.ts create mode 100644 src/store/modules/user-store.ts create mode 100644 src/styles/dark/css-vars.css create mode 100644 src/styles/element-plus.scss create mode 100644 src/styles/index.scss create mode 100644 src/styles/reset.scss create mode 100644 src/styles/variables.module.scss create mode 100644 src/styles/variables.scss create mode 100644 src/styles/vxe-table.css create mode 100644 src/styles/vxe-table.scss create mode 100644 src/types/auto-imports.d.ts create mode 100644 src/types/components.d.ts create mode 100644 src/types/env.d.ts create mode 100644 src/types/global.d.ts create mode 100644 src/types/router.d.ts create mode 100644 src/types/shims-vue.d.ts create mode 100644 src/types/socket.d.ts create mode 100644 src/utils/TimerManager/index.ts create mode 100644 src/utils/auth.ts create mode 100644 src/utils/auxiliaryFunction/index.ts create mode 100644 src/utils/functionDialogBox/index.ts create mode 100644 src/utils/i18n.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/nprogress.ts create mode 100644 src/utils/request.ts create mode 100644 src/utils/storage.ts create mode 100644 src/utils/theme.ts create mode 100644 src/views/BoosAccountManagement/components/BoosAccountDetails.vue create mode 100644 src/views/BoosAccountManagement/components/BoosAccountForm.vue create mode 100644 src/views/BoosAccountManagement/index.vue create mode 100644 src/views/TaskManagement/components/TaskDetails.vue create mode 100644 src/views/TaskManagement/components/TaskForm.vue create mode 100644 src/views/TaskManagement/index.vue create mode 100644 src/views/calibration/accountAdjustmentApplication/components/DeptTree.vue create mode 100644 src/views/calibration/accountAdjustmentApplication/components/UserImport.vue create mode 100644 src/views/calibration/accountAdjustmentApplication/index.vue create mode 100644 src/views/calibration/announcementManagement/components/AnnouncementForm.vue create mode 100644 src/views/calibration/announcementManagement/index.vue create mode 100644 src/views/calibration/applicationForSealApproval/components/StampingForm.vue create mode 100644 src/views/calibration/applicationForSealApproval/index.vue create mode 100644 src/views/calibration/businessSystem/bidRegistration/components/BidRegistrationForm.vue create mode 100644 src/views/calibration/businessSystem/bidRegistration/components/BusinessProjectForm.vue create mode 100644 src/views/calibration/businessSystem/bidRegistration/index.vue create mode 100644 src/views/calibration/businessSystem/conflictOfInterestSearch/components/BidRegistrationForm.vue create mode 100644 src/views/calibration/businessSystem/conflictOfInterestSearch/components/BusinessProjectForm.vue create mode 100644 src/views/calibration/businessSystem/conflictOfInterestSearch/index.vue create mode 100644 src/views/calibration/businessSystem/preRegistration/components/PreRegistrationForm.vue create mode 100644 src/views/calibration/businessSystem/preRegistration/index.vue create mode 100644 src/views/calibration/businessSystem/projectRegistration/components/AdjustTheYearForm.vue create mode 100644 src/views/calibration/businessSystem/projectRegistration/components/BidRegistrationForm.vue create mode 100644 src/views/calibration/businessSystem/projectRegistration/components/BusinessProjectForm.vue create mode 100644 src/views/calibration/businessSystem/projectRegistration/index.vue create mode 100644 src/views/calibration/caseLabel/components/CaseLabelForm.vue create mode 100644 src/views/calibration/caseLabel/index.vue create mode 100644 src/views/calibration/caseManagement/components/CaseLogComponent/components/CaseLogForm.vue create mode 100644 src/views/calibration/caseManagement/components/CaseLogComponent/index.vue create mode 100644 src/views/calibration/caseManagement/components/CaseManagementForm.vue create mode 100644 src/views/calibration/caseManagement/components/ChangeApplicationComponent/components/CaseInvoiceForm.vue create mode 100644 src/views/calibration/caseManagement/components/ChangeApplicationComponent/index.vue create mode 100644 src/views/calibration/caseManagement/components/CumulativeReceivedPaymentForm.vue create mode 100644 src/views/calibration/caseManagement/components/UploadOfDataForm.vue create mode 100644 src/views/calibration/caseManagement/index.vue create mode 100644 src/views/calibration/department/index.vue create mode 100644 src/views/calibration/departureFinancialRegistration/components/SeparationRegistrationForm.vue create mode 100644 src/views/calibration/departureFinancialRegistration/components/StampingForm.vue create mode 100644 src/views/calibration/departureFinancialRegistration/index.vue create mode 100644 src/views/calibration/inventoryRegistration/components/StampingForm.vue create mode 100644 src/views/calibration/inventoryRegistration/index.vue create mode 100644 src/views/calibration/invoiceApplication/components/DeptTree.vue create mode 100644 src/views/calibration/invoiceApplication/components/UserImport.vue create mode 100644 src/views/calibration/invoiceApplication/index.vue create mode 100644 src/views/calibration/lawFirmStandardDocuments/components/LawFirmStandardsForm.vue create mode 100644 src/views/calibration/lawFirmStandardDocuments/index.vue create mode 100644 src/views/calibration/lmportantScheduleManagement/components/LmportantScheduleForm.vue create mode 100644 src/views/calibration/lmportantScheduleManagement/index.vue create mode 100644 src/views/calibration/onboardingRegistration/components/DepartmentSelectionForm.vue create mode 100644 src/views/calibration/onboardingRegistration/index.vue create mode 100644 src/views/calibration/paymentApplicationForm/components/DeptTree.vue create mode 100644 src/views/calibration/paymentApplicationForm/components/UserImport.vue create mode 100644 src/views/calibration/paymentApplicationForm/index.vue create mode 100644 src/views/calibration/permissionManagement/components/permissionForm.vue create mode 100644 src/views/calibration/permissionManagement/index.vue create mode 100644 src/views/calibration/personnelManagement/components/DeptTree.vue create mode 100644 src/views/calibration/personnelManagement/components/EditorPasswordForm.vue create mode 100644 src/views/calibration/personnelManagement/components/UserImport.vue create mode 100644 src/views/calibration/personnelManagement/index.vue create mode 100644 src/views/calibration/registrationPlatform/components/RegistrationPlatformForm.vue create mode 100644 src/views/calibration/registrationPlatform/index.vue create mode 100644 src/views/calibration/reimbursement/components/DeptTree.vue create mode 100644 src/views/calibration/reimbursement/components/UserImport.vue create mode 100644 src/views/calibration/reimbursement/index.vue create mode 100644 src/views/calibration/revenueRecognition/components/DeptTree.vue create mode 100644 src/views/calibration/revenueRecognition/components/UserImport.vue create mode 100644 src/views/calibration/revenueRecognition/index.vue create mode 100644 src/views/calibration/roleManagement/components/GrantPermissionsForm.vue create mode 100644 src/views/calibration/roleManagement/components/RoleForm.vue create mode 100644 src/views/calibration/roleManagement/index.vue create mode 100644 src/views/calibration/salaryBonusAdjustment/components/DeptTree.vue create mode 100644 src/views/calibration/salaryBonusAdjustment/components/UserImport.vue create mode 100644 src/views/calibration/salaryBonusAdjustment/index.vue create mode 100644 src/views/calibration/systemManagement/components/SystemForm.vue create mode 100644 src/views/calibration/systemManagement/index.vue create mode 100644 src/views/calibration/teamManagement/components/GrantPermissionsForm.vue create mode 100644 src/views/calibration/teamManagement/components/RoleForm.vue create mode 100644 src/views/calibration/teamManagement/components/TeamForm.vue create mode 100644 src/views/calibration/teamManagement/index.vue create mode 100644 src/views/dashboard/AnnouncementDetails.vue create mode 100644 src/views/dashboard/config.ts create mode 100644 src/views/dashboard/index.vue create mode 100644 src/views/error/401.vue create mode 100644 src/views/error/404.vue create mode 100644 src/views/login/components/Login.vue create mode 100644 src/views/login/index.vue create mode 100644 tsconfig.json create mode 100644 uno.config.ts create mode 100644 vite.config.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..00ee2de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org +root = true + +# 表示所有文件适用 +[*] +charset = utf-8 # 设置文件字符集为 utf-8 +end_of_line = lf # 控制换行类型(lf | cr | crlf) +indent_style = space # 缩进风格(tab | space) +indent_size = 2 # 缩进大小 +insert_final_newline = true # 始终在文件末尾插入一个新行 + +# 表示仅 md 文件适用以下规则 +[*.md] +max_line_length = off # 关闭最大行长度限制 +trim_trailing_whitespace = false # 关闭末尾空格修剪 diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..b03c6a3 --- /dev/null +++ b/.env.development @@ -0,0 +1,19 @@ +# 应用端口 +VITE_APP_PORT=3000 +# 项目名称 +VITE_APP_TITLE=vue3-element-admin +# 代理前缀 +VITE_APP_BASE_API=/dev-api + +# 生产环境配置 +VITE_APP_ENV = 'development' + +# 接口地址 +# VITE_APP_API_URL=http://192.168.0.61:8007 # 本地 +VITE_APP_API_URL=http://26.151.107.60:8007 # 本地 + +# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws +VITE_APP_WS_ENDPOINT= + +# 启用 Mock 服务 +VITE_MOCK_DEV_SERVER=false diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..98c1b32 --- /dev/null +++ b/.env.production @@ -0,0 +1,19 @@ +# 应用端口 +VITE_APP_PORT=3000 +# 项目名称 +VITE_APP_TITLE=vue3-element-admin +# 代理前缀 +VITE_APP_BASE_API=/dev-api + +# 生产环境配置 +VITE_APP_ENV = 'production' + +# 接口地址 +VITE_APP_API_URL=http://8.137.99.82:9000 # 线上 +# VITE_APP_API_URL=http://localhost:8989 # 本地 + +# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws +VITE_APP_WS_ENDPOINT= + +# 启用 Mock 服务 +VITE_MOCK_DEV_SERVER=false diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json new file mode 100644 index 0000000..0e8546f --- /dev/null +++ b/.eslintrc-auto-import.json @@ -0,0 +1,316 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "EffectScope": true, + "ElMessage": true, + "ElMessageBox": true, + "ElNotification": true, + "InjectionKey": true, + "PropType": true, + "Ref": true, + "VNode": true, + "asyncComputed": true, + "autoResetRef": true, + "computed": true, + "computedAsync": true, + "computedEager": true, + "computedInject": true, + "computedWithControl": true, + "controlledComputed": true, + "controlledRef": true, + "createApp": true, + "createEventHook": true, + "createGlobalState": true, + "createInjectionState": true, + "createReactiveFn": true, + "createReusableTemplate": true, + "createSharedComposable": true, + "createTemplatePromise": true, + "createUnrefFn": true, + "customRef": true, + "debouncedRef": true, + "debouncedWatch": true, + "defineAsyncComponent": true, + "defineComponent": true, + "eagerComputed": true, + "effectScope": true, + "extendRef": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "ignorableWatch": true, + "inject": true, + "isDefined": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "makeDestructurable": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onClickOutside": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onKeyStroke": true, + "onLongPress": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onStartTyping": true, + "onUnmounted": true, + "onUpdated": true, + "pausableWatch": true, + "provide": true, + "reactify": true, + "reactifyObject": true, + "reactive": true, + "reactiveComputed": true, + "reactiveOmit": true, + "reactivePick": true, + "readonly": true, + "ref": true, + "refAutoReset": true, + "refDebounced": true, + "refDefault": true, + "refThrottled": true, + "refWithControl": true, + "resolveComponent": true, + "resolveRef": true, + "resolveUnref": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "syncRef": true, + "syncRefs": true, + "templateRef": true, + "throttledRef": true, + "throttledWatch": true, + "toRaw": true, + "toReactive": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "tryOnBeforeMount": true, + "tryOnBeforeUnmount": true, + "tryOnMounted": true, + "tryOnScopeDispose": true, + "tryOnUnmounted": true, + "unref": true, + "unrefElement": true, + "until": true, + "useActiveElement": true, + "useAnimate": true, + "useArrayDifference": true, + "useArrayEvery": true, + "useArrayFilter": true, + "useArrayFind": true, + "useArrayFindIndex": true, + "useArrayFindLast": true, + "useArrayIncludes": true, + "useArrayJoin": true, + "useArrayMap": true, + "useArrayReduce": true, + "useArraySome": true, + "useArrayUnique": true, + "useAsyncQueue": true, + "useAsyncState": true, + "useAttrs": true, + "useBase64": true, + "useBattery": true, + "useBluetooth": true, + "useBreakpoints": true, + "useBroadcastChannel": true, + "useBrowserLocation": true, + "useCached": true, + "useClipboard": true, + "useCloned": true, + "useColorMode": true, + "useConfirmDialog": true, + "useCounter": true, + "useCssModule": true, + "useCssVar": true, + "useCssVars": true, + "useCurrentElement": true, + "useCycleList": true, + "useDark": true, + "useDateFormat": true, + "useDebounce": true, + "useDebounceFn": true, + "useDebouncedRefHistory": true, + "useDeviceMotion": true, + "useDeviceOrientation": true, + "useDevicePixelRatio": true, + "useDevicesList": true, + "useDisplayMedia": true, + "useDocumentVisibility": true, + "useDraggable": true, + "useDropZone": true, + "useElementBounding": true, + "useElementByPoint": true, + "useElementHover": true, + "useElementSize": true, + "useElementVisibility": true, + "useEventBus": true, + "useEventListener": true, + "useEventSource": true, + "useEyeDropper": true, + "useFavicon": true, + "useFetch": true, + "useFileDialog": true, + "useFileSystemAccess": true, + "useFocus": true, + "useFocusWithin": true, + "useFps": true, + "useFullscreen": true, + "useGamepad": true, + "useGeolocation": true, + "useIdle": true, + "useImage": true, + "useInfiniteScroll": true, + "useIntersectionObserver": true, + "useInterval": true, + "useIntervalFn": true, + "useKeyModifier": true, + "useLastChanged": true, + "useLocalStorage": true, + "useMagicKeys": true, + "useManualRefHistory": true, + "useMediaControls": true, + "useMediaQuery": true, + "useMemoize": true, + "useMemory": true, + "useMounted": true, + "useMouse": true, + "useMouseInElement": true, + "useMousePressed": true, + "useMutationObserver": true, + "useNavigatorLanguage": true, + "useNetwork": true, + "useNow": true, + "useObjectUrl": true, + "useOffsetPagination": true, + "useOnline": true, + "usePageLeave": true, + "useParallax": true, + "useParentElement": true, + "usePerformanceObserver": true, + "usePermission": true, + "usePointer": true, + "usePointerLock": true, + "usePointerSwipe": true, + "usePreferredColorScheme": true, + "usePreferredContrast": true, + "usePreferredDark": true, + "usePreferredLanguages": true, + "usePreferredReducedMotion": true, + "usePrevious": true, + "useRafFn": true, + "useRefHistory": true, + "useResizeObserver": true, + "useScreenOrientation": true, + "useScreenSafeArea": true, + "useScriptTag": true, + "useScroll": true, + "useScrollLock": true, + "useSessionStorage": true, + "useShare": true, + "useSlots": true, + "useSorted": true, + "useSpeechRecognition": true, + "useSpeechSynthesis": true, + "useStepper": true, + "useStorage": true, + "useStorageAsync": true, + "useStyleTag": true, + "useSupported": true, + "useSwipe": true, + "useTemplateRefsList": true, + "useTextDirection": true, + "useTextSelection": true, + "useTextareaAutosize": true, + "useThrottle": true, + "useThrottleFn": true, + "useThrottledRefHistory": true, + "useTimeAgo": true, + "useTimeout": true, + "useTimeoutFn": true, + "useTimeoutPoll": true, + "useTimestamp": true, + "useTitle": true, + "useToNumber": true, + "useToString": true, + "useToggle": true, + "useTransition": true, + "useUrlSearchParams": true, + "useUserMedia": true, + "useVModel": true, + "useVModels": true, + "useVibrate": true, + "useVirtualList": true, + "useWakeLock": true, + "useWebNotification": true, + "useWebSocket": true, + "useWebWorker": true, + "useWebWorkerFn": true, + "useWindowFocus": true, + "useWindowScroll": true, + "useWindowSize": true, + "watch": true, + "watchArray": true, + "watchAtMost": true, + "watchDebounced": true, + "watchDeep": true, + "watchEffect": true, + "watchIgnorable": true, + "watchImmediate": true, + "watchOnce": true, + "watchPausable": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "watchThrottled": true, + "watchTriggerable": true, + "watchWithFilter": true, + "useRoute": true, + "useRouter": true, + "storeToRefs": true, + "whenever": true, + "DirectiveBinding": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "MaybeRef": true, + "MaybeRefOrGetter": true, + "WritableComputedRef": true, + "acceptHMRUpdate": true, + "createPinia": true, + "defineStore": true, + "getActivePinia": true, + "injectLocal": true, + "mapActions": true, + "mapGetters": true, + "mapState": true, + "mapStores": true, + "mapWritableState": true, + "onBeforeRouteLeave": true, + "onBeforeRouteUpdate": true, + "onWatcherCleanup": true, + "provideLocal": true, + "setActivePinia": true, + "setMapStoreSuffix": true, + "useClipboardItems": true, + "useI18n": true, + "useId": true, + "useLink": true, + "useModel": true, + "useTemplateRef": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6d77ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.history + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +stats.html +pnpm-lock.yaml +package-lock.json +.stylelintcache +.eslintcache diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..44421a7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +dist +node_modules +public +.husky +.vscode +.idea +*.sh +*.md + +src/assets +stats.html +pnpm-lock.yaml diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..c6484bf --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,41 @@ +# 在单参数箭头函数中始终添加括号 +arrowParens: "always" +# JSX 多行元素的闭合标签另起一行 +bracketSameLine: false +# 对象字面量中的括号之间添加空格 +bracketSpacing: true +# 自动格式化嵌入的代码(如 Markdown 和 HTML 内的代码) +embeddedLanguageFormatting: "auto" +# 忽略 HTML 空白敏感度,将空白视为非重要内容 +htmlWhitespaceSensitivity: "ignore" +# 不插入 @prettier 的 pragma 注释 +insertPragma: false +# 在 JSX 中使用双引号 +jsxSingleQuote: false +# 每行代码的最大长度限制为 100 字符 +printWidth: 100 +# 在 Markdown 中保留原有的换行格式 +proseWrap: "preserve" +# 仅在必要时添加对象属性的引号 +quoteProps: "as-needed" +# 不要求文件开头插入 @prettier 的 pragma 注释 +requirePragma: false +# 在语句末尾添加分号 +semi: false +# 使用双引号而不是单引号 +singleQuote: true +# 缩进使用 2 个空格 +tabWidth: 2 +# 在多行元素的末尾添加逗号(ES5 支持的对象、数组等) +trailingComma: "none" +# 使用空格而不是制表符缩进 +useTabs: false +# Vue 文件中的 ", + "", + "", + "" + ], + "description": "Vue3.0" + } +} diff --git a/.vscode/vue3.2.code-snippets b/.vscode/vue3.2.code-snippets new file mode 100644 index 0000000..a083940 --- /dev/null +++ b/.vscode/vue3.2.code-snippets @@ -0,0 +1,17 @@ +{ + "Vue3.2+快速生成模板": { + "scope": "vue", + "prefix": "Vue3.2+", + "body": [ + "", + "", + "", + "", + "", + "" + ], + "description": "Vue3.2+" + } +} diff --git a/.vscode/vue3.3.code-snippets b/.vscode/vue3.3.code-snippets new file mode 100644 index 0000000..705e04f --- /dev/null +++ b/.vscode/vue3.3.code-snippets @@ -0,0 +1,21 @@ +{ + "Vue3.3+defineOptions快速生成模板": { + "scope": "vue", + "prefix": "Vue3.3+", + "body": [ + "", + "", + "", + "", + "", + "" + ], + "description": "Vue3.3+defineOptions快速生成模板" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aef3f6d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,386 @@ + +# 2.11.5 (2024/6/18) + +## ✨ feat + +- 支持后端文件导入([#142](https://github.com/youlaitech/vue3-element-admin/pull/142)) [@cshaptx4869](https://github.com/cshaptx4869) + + +## 🐛 fix +- vue-dev-tools 插件导致菜单路由切换卡死,暂时关闭 ([28349e](https://github.com/youlaitech/vue3-element-admin/commit/28349efe147afab36531ba148eaac3a448fe6c71)) [@haoxianrui](https://github.com/haoxianrui) + + + +# 2.11.4 (2024/6/16) + +## ✨ feat + +- 操作栏增加render配置参数([#138](https://github.com/youlaitech/vue3-element-admin/pull/140)) [@cshaptx4869](https://github.com/cshaptx4869) +- 左侧工具栏增加type配置参数([#141](https://github.com/youlaitech/vue3-element-admin/pull/141)) [@diamont1001](https://github.com/diamont1001) + +## ♻️ refactor +- 更换权限分配弹窗类型为 drawer 并添加父子联动开关([2d9193](https://github.com/youlaitech/vue3-element-admin/commit/2d9193c47fd224f01f82b9c0b2bbeb5e7cb33584)) [@haoxianrui](https://github.com/haoxianrui) + + + +# 2.11.3 (2024/6/11) + +## ✨ feat + +- 支持默认工具栏的导入([#138](https://github.com/youlaitech/vue3-element-admin/pull/138)) [@cshaptx4869](https://github.com/cshaptx4869) +- 添加CURD导入示例([19e7bb](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869) + +## ♻️ refactor +- 修改导出全量数据选项文本([904fec](https://github.com/youlaitech/vue3-element-admin/commit/904fecad65217650482fcdbb10ffb7f3d27eb9ea)) [@cshaptx4869](https://github.com/cshaptx4869) + +## 🐛 fix +- 菜单列表未适配el-icon导致图标不显示问题修复([e72b68](https://github.com/youlaitech/vue3-element-admin/commit/e72b68337562b5a7ea24ad55bbe00023e1266b40)) [@haoxianrui](https://github.com/haoxianrui) + +# 2.11.2 (2024/6/8) + +## ✨ feat + +- 支持表格远程筛选([#131](https://github.com/youlaitech/vue3-element-admin/pull/131)) [@cshaptx4869](https://github.com/cshaptx4869) +- 支持标签输入框([#132](https://github.com/youlaitech/vue3-element-admin/pull/132)) [@cshaptx4869](https://github.com/cshaptx4869) +- 表单项支持tips配置([#133](https://github.com/youlaitech/vue3-element-admin/pull/133)) [@cshaptx4869](https://github.com/cshaptx4869) +- 前端导出支持全量数据([#134](https://github.com/youlaitech/vue3-element-admin/pull/134)) [@cshaptx4869](https://github.com/cshaptx4869) +- 支持选中数据导出([#135](https://github.com/youlaitech/vue3-element-admin/pull/135)) [@cshaptx4869](https://github.com/cshaptx4869) +- 表格默认工具栏的导出、搜索按钮增加权限点控制([883128](https://github.com/youlaitech/vue3-element-admin/commit/8831289b655f2cc086ecdababaa89f8d8a087c42)) [@cshaptx4869](https://github.com/cshaptx4869) +- 页签title支持动态设置([23876a](https://github.com/youlaitech/vue3-element-admin/commit/23876aa396143bf77cb5c86af8d6023d9ff6555a)) [@haoxianrui](https://github.com/haoxianrui) + +## ♻️ refactor +- 默认工具栏支持自定义([#136](https://github.com/youlaitech/vue3-element-admin/pull/136)) [@cshaptx4869](https://github.com/cshaptx4869) +- 未配置全量导出接口时选项隐藏([eab91ef](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869) + +## 🐛 fix +- 修复注销登出后redirect跳转路由参数丢失([5626017](https://github.com/youlaitech/vue3-element-admin/commit/562601736731afd20bb1a5140d856f6515720159)) [@haoxianrui](https://github.com/haoxianrui) + +# 2.11.1 (2024/6/6) + +## ✨ feat + +- 增加pagination、request、parseData配置参数([#119](https://github.com/youlaitech/vue3-element-admin/pull/119)) [@cshaptx4869](https://github.com/cshaptx4869) +- 增加返回顶部功能([#120](https://github.com/youlaitech/vue3-element-admin/pull/120)) [@cshaptx4869](https://github.com/cshaptx4869) +- 支持前端导出([#126](https://github.com/youlaitech/vue3-element-admin/pull/126)) [@cshaptx4869](https://github.com/cshaptx4869) + +## ♻️ refactor +- 重构布局样式(解决页面抖动问题)([#116](https://github.com/youlaitech/vue3-element-admin/pull/116)) [@cshaptx4869](https://github.com/cshaptx4869) +- 修改CURD示例编辑弹窗尺寸([#121](https://github.com/youlaitech/vue3-element-admin/pull/121)) [@cshaptx4869](https://github.com/cshaptx4869) +- 统一注册vue插件([#122](https://github.com/youlaitech/vue3-element-admin/pull/122)) [@cshaptx4869](https://github.com/cshaptx4869) +- 默认主题跟随系统([#128](https://github.com/youlaitech/vue3-element-admin/pull/128)) [@cshaptx4869](https://github.com/cshaptx4869) +- 增加"scss.lint.unknownAtRules": "ignore"代码,解决style中使用@apply提示unknow at rules@apply提示问题([Gitee#22](https://gitee.com/youlaiorg/vue3-element-admin/pulls/22)) [@zjsy521](https://gitee.com/zjsy521) + +## 🐛 fix +- 修复左侧布局移动端菜单弹出样式 ([#117](https://github.com/youlaitech/vue3-element-admin/pull/117)) [@cshaptx4869](https://github.com/cshaptx4869) + +- 修复编辑后未清空id再新增菜单覆盖的问题([0e78eeb](https://github.com/youlaitech/vue3-element-admin/commit/0e78eeb75008fa8e9732b1b4e7d7a1ea345c7a1b)) [@haoxianrui](https://github.com/haoxianrui) +- 修复水印层级问题([#123](https://github.com/youlaitech/vue3-element-admin/pull/123)) [@cshaptx4869](https://github.com/cshaptx4869) +- 修复混合布局样式问题([#124](https://github.com/youlaitech/vue3-element-admin/pull/124)) [@cshaptx4869](https://github.com/cshaptx4869) +- 修复关闭弹窗时没有clearValidate问题([#125](https://github.com/youlaitech/vue3-element-admin/pull/125)) [@andm31](https://github.com/andm31) + + + +# 2.11.0 (2024/5/27) + +## ✨ feat +- 菜单添加路由参数设置(author by [haoxianrui](https://github.com/haoxianrui)) +- 增加列表选择组件(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 增加列表选择组件使用示例(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 增加defaultToolbar配置参数(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 表单弹窗支持drawer模式(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 表单项增加computed和watchEffect配置(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 支持switch属性修改(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 表单项增加文本类型支持(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 列表列增加show配置项(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 支持搜索表单显隐控制(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 支持input属性修改(author by [cshaptx4869](https://github.com/cshaptx4869)) +- search配置新增函数能力拓展(author by [xiudaozhe](https://github.com/xiudaozhe)) +- 表格新增列设置控制(author by [haoxianrui](https://github.com/haoxianrui)) +- 搜索添加展开和收缩(author by [haoxianrui](https://github.com/haoxianrui)) +- watch函数增加配置项参数返回(author by [cshaptx4869](https://github.com/cshaptx4869)) + +## ♻️ refactor +- 重构图标选择组件(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 重构列表选择组件默认样式 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 加强对话框表单组件和列表选择组件(author by [cshaptx4869](https://github.com/cshaptx4869)) +- routeMeta增加alwaysShow字段声明(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 分页组件增加溢出滚动效果(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修正登录表单的Ref类型(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 点击表格刷新按钮不重置页码(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 筛选列超出一定高度滚动(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 优化加强initFn函数,表单项增加initFn函数(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 重构watch、computed、watchEffect调用(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修改操作成功提示(author by [cshaptx4869](https://github.com/cshaptx4869)) +- PageSearch 改用card作为容器,样式改用unocss写法(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 优化首页 loading 动画效果author by [haoxianrui](https://github.com/haoxianrui)) + + +## 🐛 fix +- 路由是否始终显示不限制只有顶级目录才有的配置,开放至菜单 (author by [haoxianrui](https://github.com/haoxianrui)) +- sockjs-client 报错 global is not defined 导致开发环境无法打开 WebSocket 页面问题修复 (author by [haoxianrui](https://github.com/haoxianrui)) +- 发送用户重启密码功能,最少为6位字符(小于6位登陆时不允许的问题) (author by [dreamnyj](https://gitee.com/dreamnyj)) +- 修复系统设置面板滚动条问题(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修复表单插槽失效问题(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修改tagsview刷新丢失query问题(author by [xiudaozhe](https://github.com/xiudaozhe)) + +## 📦️ build +- 升级 NPM 包版本至最新 (author by [haoxianrui](https://github.com/haoxianrui)) + +## ⚙️ ci +- 规整脚本执行命令(author by [cshaptx4869](https://github.com/cshaptx4869)) + + +# 2.10.1 (2024/5/4) + +## ♻️ refactor +- 抽离CURD的使用部分代码为Hooks实现(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修改CURD导入权限点标识名(author by [cshaptx4869](https://github.com/cshaptx4869)) +- cURD表单字段支持watch监听(author by [cshaptx4869](https://github.com/cshaptx4869)) +- cURD表单input支持number修饰(author by [cshaptx4869](https://github.com/cshaptx4869)) +- cURD表单组件支持checkbox多选框(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 优化axios响应数据TS类型提示(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修改CURD表单组件自定义类型的attrs传值(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 同步重置密码按钮权限标识重命名(author by [haoxianrui](https://github.com/haoxianrui)) +- 重构API为静态方法实现模块化管理,并将types.ts重命名为model.ts用于存放接口模型定义(author by [haoxianrui](https://github.com/haoxianrui)) + + +## 🐛 fix +- sockjs-client 报错 global is not defined 导致开发环境无法打开 WebSocket 页面问题修复 (author by [haoxianrui](https://github.com/haoxianrui)) +- 主题颜色设置覆盖暗黑模式下el-table行激活的背景色问题修复 (author by [haoxianrui](https://github.com/haoxianrui)) +- 修复因API接口调整而影响的调用页面的问题 (author by [haoxianrui](https://github.com/haoxianrui)) + +## 📦️ build +- 升级 NPM 包版本至最新 (author by [haoxianrui](https://github.com/haoxianrui)) + + +# 2.10.0 (2024/4/26) +## ✨ feat +- 封装增删改查组件(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 集成 vite-plugin-vue-devtools 插件(author by [Tricker39](https://github.com/Tricker39)) +- 增加CURD配置化实现(author by [cshaptx4869](https://github.com/cshaptx4869)) + + +# 2.9.3 (2024/04/14) +## ✨ feat +- 增加vue文件代码片段(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 菜单 hover 背景色添加值全局SCSS变量进行控制(author by [haoxianrui](https://github.com/haoxianrui)) + +## ♻️ refactor +- 加强基础国际化(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 增加语言和布局大小枚举类型(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 增加侧边栏状态枚举类型(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 使用布局枚举替换字面量(author by [haoxianrui](https://github.com/haoxianrui)) +- 控制台使用静态数据循环渲染(author by [april](mailto:april@zen-game.cn)) +- 本地缓存的 token 变量重命名(author by [haoxianrui](https://github.com/haoxianrui)) +- 完善 Vite 环境变量类型声明(author by [haoxianrui](https://github.com/haoxianrui)) + +## 🐛 fix +- 修复构建时提示iconComponent.name可能为undefined的报错 (author by [wangji1042](https://github.com/wangji1042)) +- 修复浏览器密码自动填充时可能存在的报错 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修复eslint报错(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 移动端下点击左侧菜单节点后关闭侧边栏(author by [haoxianrui](https://github.com/haoxianrui)) +- 添加 size 类型断言修复类型报错(author by [haoxianrui](https://github.com/haoxianrui)) + +## 📦️ build +- husky9.x版本适配 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 升级 npm 包版本至最新(author by [haoxianrui](https://github.com/haoxianrui)) + +# 2.9.2 (2024/03/05) +## ✨ feat +- vscode开发扩展推荐(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 完善基础增删改查Mock接口(author by [haoxianrui](https://github.com/haoxianrui)) + +## ♻️ refactor +- 修改login密码框功能实现(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 弱化页面进入动画效果(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 取消推荐TypeScript Vue Plugin (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 网站加载动画替换 (author by [haoxianrui](https://github.com/haoxianrui)) +- 优化主题和主题色监听,避免多个页面重复初始化 (author by [haoxianrui](https://github.com/haoxianrui)) + +## 🐛 fix +- AppMain 高度在非固定头部不正确导致出现滚动条问题修复 (author by [haoxianrui](https://github.com/haoxianrui)) +- 修复混合模式开启固定Head时的样式问题 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 设置面板统一字体大小 (author by [cshaptx4869](https://github.com/cshaptx4869)) + +## 📦️build +- 通过env配置控制mock服务 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 升级依赖包至最新版本 (author by [haoxianrui](https://github.com/haoxianrui)) +- 定义vite全局常量替换项目标题和版本 (author by [cshaptx4869](https://github.com/cshaptx4869)) + +# 2.9.1 (2024/02/28) +## ♻️ refactor +- 项目配置按钮移入navbar(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 优化user数据定义(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 统一设置栏的 SVG 图标风格 + +## 🐛 fix +- 规整一些开发依赖(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修复登录页主题切换问题 (author by [cshaptx4869](https://github.com/cshaptx4869)) + +## 🚀 pref + +- 压缩图片资源 (author by [cshaptx4869](https://github.com/cshaptx4869)) + + +# 2.9.0 (2024/02/25) + +## ✨ feat +- 引入 animate.css 动画库 +- 新增水印和配置 +- 动态路由菜单支持 element plus 的图标 + +## ♻️ refactor +- Layout 布局重构和相关问题修复 +- sass 使用 @use 替代 @import 引入外部文件指令 + +## 🐛 fix +- 修复管理页面部分弹窗无法打开问题 +- 主题颜色设置按钮 hover 等未变化问题修复 + + +# 2.8.1 (2024/01/10) + +## ✨ feat +- 替换 Mock 解决方案 vite-plugin-mock 为 vite-plugin-mock-dev-server 适配 Vite5 + +# 2.8.0 (2023/12/27) + +## ⬆️ chore +- 升级 Vite4 至 Vite5 + +# 2.7.1 (2023/12/12) + +## ♻️ refactor +- 将打包后的文件进行分类 (author by [ityangzhiwen](https://gitee.com/ityangzhiwen)) + +# 2.7.0 (2023/11/19) + +## ♻️ refactor +- 代码重构优化 +- 修改自动导入组件类型声明文件路径 +- 完善 typescript 类型 + +## 🐛 fix +- 修复管理页面部分弹窗无法打开问题 + + +# 2.7.0 (2023/11/19) + +## ♻️ refactor +- 代码重构 +- 修改自动导入组件类型声明文件路径 +- 完善 typescript 类型 + +## 🐛 fix +- 修复管理页面部分弹窗无法打开问题 + + +# 2.6.3 (2023/10/22) + +## ✨ feat +- 菜单管理新增目录只有一级子路由是否始终显示(alwaysShow)和路由页面是否缓存(keepAlive)的配置 +- 接口文档新增 swagger、knife4j +- 引入和支持 tsx + +## ♻️ refactor +- 代码瘦身,整理并删除未使用的 svg +- 控制台样式优化 + +## 🐛 fix +- 菜单栏折叠和展开的图标暗黑模式显示问题修复 + + +# 2.6.2 (2023/10/11) + +## 🐛 fix +- 主题设置未持久化问题 +- UnoCSS 插件无智能提示 + +## ♻️ refactor +- WebSocket 演示样式和代码优化 +- 用户管理代码重构 + +# 2.6.1 (2023/9/4) + +## 🐛 fix +- 导航顶部模式、混合模式样式在固定 Header 出现的样式问题修复 +- 固定 Header 没有持久化问题修复 +- 字典回显兼容 String 和 Number 类型 + +# 2.6.0 (2023/8/24)💥💥💥 + +## ✨ feat +- 导航顶部模式、混合模式支持(author by [april-tong](https://april-tong.com/)) +- 平台文档(内嵌)(author by [april-tong](https://april-tong.com/)) + +# 2.5.0 (2023/8/8) + +## ✨ feat +- 新增 Mock(author by [ygcaicn](https://github.com/ygcaicn)) +- 图标 DEMO(author by [ygcaicn](https://github.com/ygcaicn)) + +## 🐛 fix +- 字典支持 Number 类型 + +# 2.4.1 (2023/7/20) + +## ✨ feat +- 整合 vite-plugin-compression 插件打包优化(3.66MB → 1.58MB) (author by [april-tong](https://april-tong.com/)) +- 字典组件封装(author by [haoxr](https://juejin.cn/user/4187394044331261/posts)) + +## 🐛 fix +- 分页组件hidden无效 +- 签名无法保存至后端 +- Git 提交 stylelint 校验部分机器报错 + +# 2.4.0 (2023/6/17) + +## ✨ feat +- 新增组件标签输入框(author by [april-tong](https://april-tong.com/)) +- 新增组件签名(author by [april-tong](https://april-tong.com/)) +- 新增组件表格(author by [april-tong](https://april-tong.com/)) +- Echarts 图表添加下载功能 author by [april-tong](https://april-tong.com/)) + +## ♻️ refactor +- 限制包管理器为 pnpm 和 node 版本16+ +- 自定义组件自动导入配置 +- 搜索框样式写法优化 + +## 🐛 fix +- 用户导入的部门回显成数字问题修复 + +## ⬆️ chore +- element-plus 版本升级 2.3.5 → 2.3.6 + +# 2.3.1 (2023/5/21) + +## 🔄 refactor +- 组件示例文件名称优化 + +# 2.2.2 (2023/5/11) + +## ✨ feat +- 组件封装示例添加源码地址 +- 角色、菜单、部门、字段按钮添加权限控制 + + +# 2.3.0 (2023/5/12) + +## ⬆️ chore +- vue 版本升级 3.2.45 → 3.3.1 ([CHANGELOG](https://github.com/vuejs/core/blob/main/CHANGELOG.md)) +- vite 版本升级 4.3.1 → 4.3.5 + +## ♻️ refactor +- 使用 vue 3.3 版本新特性 `defineOptions` 在 `setup` 定义组件名称,移除重复的 `script` 标签 + +# 2.2.2 (2023/5/11) + +## ✨ feat +- 用户新增提交添加 `vueUse` 的 `useDebounceFn` 函数实现按钮防抖节流 + + +# 2.2.1 (2023/4/25) + +## 🐛 fix +- 图标选择器组件使用 `onClickOutside` 未排除下拉弹出框元素导致无法输入搜索。 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9825cba --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present 有来开源组织 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5342b7 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Vue 3 + Typescript + Vite + +This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 ` + + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..be095c0dbb21604a0cc8c49037bb014f29c07a2b GIT binary patch literal 52081 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
    l-Yc8NP){cWV)CJDlfBRR)I+9^tD|1F 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..6cc9b37 --- /dev/null +++ b/package.json @@ -0,0 +1,131 @@ +{ + "name": "jurs-zun", + "description": "Vue3 + Vite + TypeScript + Element-Plus 的后台管理模板,vue-element-admin 的 Vue3 版本", + "version": "3.4.2", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "dev:prod": "vite --mode production", + "build": "vue-tsc --noEmit & vite build", + "build:dev": "vue-tsc --noEmit & vite build --mode development", + "build:prod": "vue-tsc --noEmit & vite build --mode production", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit", + "lint:eslint": "eslint --cache \"src/**/*.{vue,ts,js}\" --fix", + "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\"", + "lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix", + "lint:lint-staged": "lint-staged", + "lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:stylelint", + "preinstall": "npx only-allow pnpm", + "prepare": "husky", + "commit": "git-cz" + }, + "config": { + "commitizen": { + "path": "node_modules/cz-git" + } + }, + "lint-staged": { + "*.{js,ts}": [ + "eslint --fix", + "prettier --write" + ], + "*.{cjs,json}": [ + "prettier --write" + ], + "*.{vue,html}": [ + "eslint --fix", + "prettier --write", + "stylelint --fix" + ], + "*.{scss,css}": [ + "stylelint --fix", + "prettier --write" + ], + "*.md": [ + "prettier --write" + ] + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@stomp/stompjs": "^7.2.1", + "@vueuse/core": "^12.8.2", + "@wangeditor-next/editor": "^5.6.47", + "@wangeditor-next/editor-for-vue": "^5.1.14", + "animate.css": "^4.1.1", + "axios": "^1.13.2", + "codemirror": "^5.65.20", + "codemirror-editor-vue3": "^2.8.0", + "default-passive-events": "^2.0.0", + "echarts": "^6.0.0", + "element-plus": "^2.11.8", + "exceljs": "^4.4.0", + "lodash-es": "^4.17.21", + "nprogress": "^0.2.0", + "path-browserify": "^1.0.1", + "path-to-regexp": "^8.3.0", + "pinia": "^3.0.4", + "qs": "^6.14.0", + "sortablejs": "^1.15.6", + "vue": "^3.5.24", + "vue-draggable-plus": "^0.6.0", + "vue-i18n": "^11.1.12", + "vue-router": "^4.6.3", + "vxe-table": "~4.6.25" + }, + "devDependencies": { + "@commitlint/cli": "^19.8.1", + "@commitlint/config-conventional": "^19.8.1", + "@eslint/js": "^9.39.1", + "@iconify/utils": "^2.3.0", + "@types/codemirror": "^5.60.17", + "@types/lodash-es": "^4.17.12", + "@types/node": "^24.10.1", + "@types/nprogress": "^0.2.3", + "@types/path-browserify": "^1.0.3", + "@types/qs": "^6.14.0", + "@types/sortablejs": "^1.15.9", + "@typescript-eslint/eslint-plugin": "^8.46.4", + "@typescript-eslint/parser": "^8.46.4", + "@vitejs/plugin-vue": "^6.0.1", + "autoprefixer": "^10.4.22", + "commitizen": "^4.3.1", + "cz-git": "^1.12.0", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-vue": "^10.5.1", + "globals": "^15.15.0", + "husky": "^9.1.7", + "lint-staged": "^15.5.2", + "postcss": "^8.5.6", + "postcss-html": "^1.8.0", + "postcss-scss": "^4.0.9", + "prettier": "^3.6.2", + "sass": "^1.94.0", + "stylelint": "^16.25.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recess-order": "^6.1.0", + "stylelint-config-recommended": "^15.0.0", + "stylelint-config-recommended-scss": "^14.1.0", + "stylelint-config-recommended-vue": "^1.6.1", + "stylelint-prettier": "^5.0.3", + "terser": "^5.44.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.4", + "unocss": "^66.5.6", + "unplugin-auto-import": "^19.3.0", + "unplugin-vue-components": "^28.8.0", + "vite": "^7.2.2", + "vite-plugin-mock-dev-server": "^2.0.2", + "vue-eslint-parser": "^10.2.0", + "vue-tsc": "^2.2.12" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "repository": "https://gitee.com/youlaiorg/vue3-element-admin.git", + "license": "MIT" +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/public/img/logo.png b/public/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..be095c0dbb21604a0cc8c49037bb014f29c07a2b GIT binary patch literal 52081 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
      l-Yc8NP){cWV)CJDlfBRR)I+9^tD|1F zjl7YevByo**{NH?l_Cjra~Eb_WfoRBj9Uy1E7Yh-tT`qV=l`-x^8thchB2egA8{=;dc*e*?EGvmi?;Av8rsyheF|^eVyQ6 zM(ya~j}SG|`$MFu-yt@=j@-uUqcdSsr%7$<=a^$*E{;71)m!_(qt$44pJV^#C~%pmTo)z{L6D1c1xW0 zm!@2HH-8Qd=w5sJsj;7yU3bL(5Ia2Nwx1qtan~WGGq4_Lpt8H$tGbK9CJaun+EVk_ zoT2OJM?ih44%OE?NV}V`|JZGG_D7SDw{vV~n`_}+i<8Io1=ypjNKF15c0P)14Wvv)Fehg>U{o7x|9rOK0|rQJ@5HKiYWyNE-Y?M0#`K5|jG z4D-=7Ml2%2g&QvRPrLE?96j7W?8i}vUfM7g`t*aHsW=obCVy}b5S=~f7aODeAdF)u zV*MmhOZ&l zb7u_oK=2)znp($jz{Gz88rH9>`G(W-34G4lVc|xe37L^HI&Q9~eeEYgbD-ME(ZW@n zItU)fNC&x;%SYk$jU^;pK&1&K{bB}0?B@B7&b2e@oU4EmgO@dPjmE_^Y4oVv&((%I zbFCmDbl?(UAOKuml zdva4@7_-Y;+@&AyEVa1L^YsqYu88ptBkt*!+$Ece!*V+}$0v1yMuMXZ7#U@%NiZ=2fw?3 zo5z89CjAWLX!TlteoMjA$Q_TD4^2{`j2~q^_4_(HeXdgKv`v}0{X{NP{XE}dz0Q@# z^2N9kdz8K_#jr;jI%A{EYCpb6L5W*;2 zto-}8!H%Wx1uCQQk^m!JqDI5bi*7rqyeqxM;7@uo&&P}1%IL0bV=dx`sz<~!brH^h zomN&+QT!LEJvz{@C_FsO@)l{KjeGNvH{+ysKScR7>g_e9x0);lqavq?8f6zFe-BDd z!5#fcV~AteDVW3fad>PHyXNU&paC%n)cL5ap<~gQm__dx+_ozC=zm!Ud=X}^T{w4~ z(zXTVNqs%Bmy+xG0yG%iU=dbC;Qi>q*V6o7RIxt^c=rU>e7B7+C$VI9B$|qk^^Q#C zQJs*6m`x@nBD$f98zV_TwehAMl~&o?hR+HA*WNnkeCQMFd7o>8)%nq! z<(vZwIz+Gb#OVH>0lf3G()lah3Tt70a{5er-{T?7Y_nKQsL%>OiZ3{+CZ}a$3S%=z z=v3^k&jxDQOEA1^BO4UCao-N*ISHXEeyb-`t5tuA#}kf-q^lg`{$6|bV?Dgj{rows< zLDh$6$f=>X5`V7MvNmpXPCnc9nKE_4uns4_iy{oG$1h!MX zpi4M48gwqX1%1b8~gL;|lQ{A`ZBa|PAar6Uw?`}LoO1?iXNIl6? zl<^ua25LP+3gzq;ogqQPJ?UOAn7(kpgs&+{n6>twPB2PFC>5u2WeBP>vj<9hWO}9KfRNt&JSyC4Tufzco7?@^GP5$!0pX`tHJ7HCM0qn zTp1KMLVmepqcvL=;Zq2wBf4Lqh|KzJfhlR75cC8*k(N|}cOQ&L*0cje^w{LNGi()*JcoPSEdktCM+sK~oR*7Y zf+=lZHP}F5Lhgg1EzBY%8tr`J4Nle%(*^1UGJ}0aQYkpNySTW!U<_Q`Z8HlCBV!|q z+#HI0L(=>lu&I86+5UoA{(|~x%_we!ed8%Tiqr4<-ot-LhZ+0G(m&8~9J$Zl*B2xf znNuQ!*eJl0f^s-prC+b`bXR1Y_Z7TYi~Ak$ryFA)E{#Z<(_33NR~y~8SiR@w-PY-% z89+%7A^ys!u|KuHX~qvLF*h`QTJJn)adogiQXAuSfES1G_=!iG0MP**yu#OZXEmIy zwb`E%bgq5uFFiN*Pj=()5DOR68g5Pd$5xc+-@~9|y~~q#J{!|x={xnmP7G#LDRg(T z{OF-IZGEtdy;FpLP{cK-O30Hjd8A2~)_9Vr;quhr*-gPpJKiG~aT&OdB!NQ;<&POW z0!|1;+20%(N^<1x6I;K-RQN@#Pdn=MO>VtXtUtAL1rDgsymud|8TS7c+`W=*%*`{3 z-ae6fGa8a`bd9trF!t42VR&(?tBgm)XZ}Ji&3_xA)sH% z$tNW-n4#J#wb$^P?y=_L>W#4e@z(BS#X^l`$E)E%K1UnD8v7RJ>1HZrEWhoK7s=X6 z@iF*I*XUst_wNY<7<(l9qRqV0I7(lYQJOJ1U7*8mAJ4a%}N`b2-2@(aWoyha`mP(|w~eiR*AlFi;< zP501Lr4!0#vgwHk!7<9slI$GwvAHdfN^ys=s4-PqyBxiFpg6e3c_UlCzjf&L9)294 ze?-POIU;*`ci(3M3?)0I!;m?vsvLeA9LP=2wS+4J=%te~Iw}ei>xbH6A%5V?FA(ev z3(2_rr8fVU-JD_V`QfOR@;v$>z~5jf4&U@Som5UjIyM-8h%(G zf#%$aXZ-$Lp#Si)CmL=xvF6m7oiAz3@c?GGS?pq^J5pE$H_1~6YM0tc%EzXMKTd1g zlVzGkr=@Y(6reU$@KP;f&wg2zqlAB)${_y5Gl=@8#eQ_&;r`tpb1zOVtvd_nk%urY z)lhOYf4BI@75`&{azRb5$FFiIM;9JB*Ica3K+E@NV#NoVgD+iBy?sgM5DDhCxW>auIs zlNZda-vH0Co6C9=#Ysg2=Nv_??0cPf_*2EUnRtZ(l_7%iq%!D9=Mp^a#pFI}>L()0 zc>zF!fqWVn|4WAekGJW3zJ=9be~%%U&XDXQ*9S)@b(a&bf-+qPJmi0c(<-{6X)FPA z@KjZoi<7CfSj}ht@)1>BjLDo%rFdQI2QQegiqOxW0(?qRxjutr(xg2!G4uH}7=?ve zth>N0Hjw)nl&Bc=xA$7KR~>3Q-#;{y@fo%{l=F9@HzZwZE$i*mU+lax1B_o=VoSJc zlx!O~@0JR!)F%~z6Pd6pq^TK_A>YXE_ZfxyYjxZ)aYUOei^y5?BX&MD6&vX=8i?`}t=! z$q5_OZiP8YtE?7tN<=!9$H5eO;cG9JqzpAavhkWth8M9=6E1Q`iK?qmhJqv4X#y~! zlmw);rv}Wvc)ZE<`ze*eoQZR!Hw#o)RG^BJZzrOqmofnakYSpW5#m%>Bg!Yl>I!?jNVi0P1v=gHTPt%}%A0szJ%**?}*;@2sK;0=?boRlbYmBlV=VXEUnke+`YWkf; zyX&S!9{yXa!^1CtfBB~cP$AJlk3bmr2eC@O;s;U!rXo7@wBq-djurzAZ>a5=nG|%I z^%q6nP*zm|_#Bj96!2mawD2Z|-yT;QsHDP2W+*)QYrC4cX3^>UQ}6xC#ogCCcXeN0 zzWMDlIk;~|sy6^Xp4Hv}NFB&&GvFyWpxmKEcw*)3>O-5p?#K7 zJEhzIF$%0br7ah`y_q*MiP`x^@+LZs*J$xY90zd3R3071Pi+)6(t1v2dbc;VKBlpY z6^?b`;cQ`jFNhi`Lu;YjrfBg88UlSr6VYF~mviFfK#@R+k1VHnCh7t&MZ?#U6DoceRzmcfaVXHSZ;Z!f@5-?Q~@l`!BAOiWgsMU!1a7 zIp&x8)f;KWpDVt5s-A%r2c!Th;{sxQ`9|g4Gx_3#Jpo9kp@zU!Irz`=E{n++;RN3# zx!pG2pH#>xfexe9rd~Yzl+Q;dOJ0R?ot=Eu$~Qdx@^tnA#2CV_FWqlDY!r#%6v4JD z5Y2z#uc6mE?Lx9_)*pfZgCwjU{78xV)9^hDfVpL$D}|ZNu?mSi7{ z(xR)o{CO3ZpD!pDFt2RNvis__8v7q7OEa--OL4V@<0KC=rLTgVb{l`H8`oubD;U^k zDSBgK3}u#%UN|~dZ9S0IO+IcK2gERBG4MPWXyiw4k~~%ql9?UF7j` zS5}guqAWc!SSSYU7HF@q{qqj{dONLiE$=HfL5g}c6_zcpB4_C8pti|nJa|{&0sBQ7 z&H-PwZy?fAfScme>*u5H*$w6P`b%_`+YZAWr%G|-Ux~38iegPl-pvSv6r7z}D`TmG z>&|&uiyB?W^A~sBnH$Dk6>Cw9)T>4wRW?L72UqJ7n>H`VQ7pO}0?Qai#({5P0180d zPYXKplWvXUh3WF=$BuW;ci!Ebo;7FPqx&Y2Swi5m_HvKTls0nc43G$Qx{tdo_G_bv zeb*fr?$~~wOT)`YyqkjAr#H>xpzFlLm5J;-CfnK@Z4oFj$5aFMMa~N-c(Ven0g04t zA<2ND!dRGE7ArsW;}dn-tLZ-0VIt6&Qm^(utQRYNy;R5R+HR*+hJyUc(S|!ia}Cr$ z1v>IW#xz_q+52;_{$a=$fEoC_-A$+gf*U)|2QxrFQAJw4>&S~zIPGrw;>=3@>+nTK zjsaq0*M*g*&evnxlDF30>)|Kw0YE;7TIU_0@|V;4SF% zn)$}Ly7?F6I#@7*HHvLmxzYg70wqTa5pYhZPZb!GU5bTGHiI)DQOi-Ih!D@emvaZH znD)mgh<-%8XXKYSJSKeUs^=beqelnb&D}>E4^`e)Qb7%DVY?b!6V;{G+Czo*xK^~> zuF8f0B=3AXH2Txia}NX0C{W_7RG`sy^x65L>2L$=Bhf#970D|hYqDvyCx90g$dofJ zhr9Gp87Ay$fJM!ff6%|5c@&%Y?jNI5V%#T2*PT zmXB~U&YEkon$^zI7fvwIVCVG~uvRm>OQpPfSo{c)0hv8jRd%hql)`q*YMH-{UoSJd z^Y^!C9c}UH0pp-ez?llMvM+#pegKo5(m{H@g zJUXSUR2{p32JBU!ux`VH%}kMM|JZ0|L$+7-CJHO4B8?XbJibpUs$)M)-@>!>`DOH+ zJ;-v2X@X1xx=1R1w6DbItGlN4Xr0ZU0|mbXX(2FpeKN6(g(V}cGF$O$MnLia#K0BD z>+m(|_z#nrf!a%apuR9%#DjKt`gp-eoaQ=vIcO2FJ)P>R{v~FOE;`GVkYD4ixmW=r zdS1Ye!2b-ufdbX|Xit|a|L1JBH;6C$?|MxKak)JwS0 zEH;M^zdcI*EQ0A*ab9$elD!4vR1_+KFiQj*#4cmHScSDmC%NurI#8PJ!WA)RI%&TI z3f?2PX1&EXPL--@8S>7Z`%p%PHw-mMuioOe7>2JU_t3zTyaEA`M*W3nass(qIP9t~ zUy9A>=+v!|y)0;c9q|{re?3E|+Nj4=OnLxc9m-~#Iy~Q-Tb$+-Aum4+{>Jxu@zVV^ z+;z54xsNV=95COglsAhOrCNU$8Y*D-=&l1PKZO;yMK_ru+9EUa5aYrHzgBBoW>Lr< zH8k2PKNQT!3qMrC^6-Xmuv+#JE)o|s`$7AWBv`k621_W%_4uLnpjImsO-&UxCaRwA z{3%yn1s{fw94#R!2}IPE+37!Q9{i!HZP5d!y=F3M+(PNDoMfcTh>X`Md0~g(P(&uD z5#U*CvPFYJ=!etkA+w%Ef_zGs%9=$NIP zC1$!5j6ngE|0joml?g%0KD&gr{f)r3;YW&8vQ~kH$gh{q#fp_odvyGM#t}yi=1Z@O z8#CHE_ngw<=Q$(hT!q%p+P7@DhvrE`X#xf96o$~-Dni@TPa79$8gob02&B=Y%ME+? zIrNgy9y201R&0`Z^U%TBtME>rJ^DRo2ky3!Hl4DBUL&JpPbv`C){vmDQT3&-VY`+6 zK{&fO7|!Ptsmxc0wy8Ly+Cfr31|B%6XtE;GPd4UCva&N2VesBF!#xA~i+-p{DY(@R z#e)rqkrfgobGi=}H32Tgqn)8g)V;iYoODAVDlry zkm!$G1_PXG5`S~MvFj8qT$E^6c5=`c0c%l^ z7~m?o+^gCW8H*dQ6_$rk4)868wVh6F-gI@g1&1p~=-O@@@y~3MwF3%9jh9}Cji62< z`!o)^(lizm`wt(3u#TxK{vOt~u^wIH5Au^#1|Ftlw%E5kJla#V$3aWGL=_T=+<#u* zG2!PRisfZ%jIDK#egA`Cb#R@cR3Y6P$HdCrw3EL!pEpsg@x_V0%96v?qNi$6KM6Ob z_7)?oqE%*3`DAizIBluUl%Lg4OTTT;wBjV$VArQBZj+ z_e}7NI>oQIs8Fxuz5R>RO8=U;_fV(`>uE#=kV)yLPq`rAi4n!4HY0z7=cD*kj`hQ@ zV{`^b^aYE3^PiOUwe|*Bjfu82tRZ&5j68Yl4IiXURa@~)HKRJ;&Skq5$yOe&rsO?& z)3EZyxXymzNv^U9Ke#famj8i=S!BEjyBv@n(EIwB$*ceOD^k7QtXfVDU(L&+7zHY2 z^832#Xnx|lTHy-WnUQr5F5syzdD15_h;X);*44cw=HzL zzd3PrSn@b}0FDZn^BW-iKx2|6f77;v%hLBPb|EidgQLyd4E`}9=^TfC_zLMB_0|fL zlC9!(owtnd+n5frF;3+%{uzh17gtoo?`fF6IAQyd_H*dP1-;6b6F;rCsHi|&TmUvS z9K=(xm@1zmJbTrGf&}2Tz4hCLb}nBhMY8J82T6S!gFwgkry=U8!t_BM&*-X*Kq|X9 zvDYHe>rx?HLhbr{Vfkcb@U!O>Zw9OtkgYJ~hFDM0SKdwXmhqNb3uLTwh<9l>lK0}I zQcN!MQILtu&m=mLbjsJNY&29;(RyfvL7dKMAQAGED)P8$$ zG#9*9*Mk!`+>e^B`W_rfPaSVKc=W+8?s~kzz5~_QHmScG-h{7xT%OLfaM8_|zuDET z1eG0oQdk8qykYp z@`r8n-px1HS%ti>n(-(UIw}b3EIpix*RqH(2u0Zi0%-9x{of{{%YSbM`16f&<_BE| zl!tybMH+*RUP;7qxhPbs?e9z2-|7ZQZL(D7m-EFpXx_V?eqRPaDm*NCdamCso!Rj_ z%g_}qX*KB(Hm+Z+y)IE>jf_tI_`mu<7@@JghTICDtiMXFe(6%sC|)sLPsas$0eh;Pr<`2sahYaIUu8@#K=ptKk7ka zoq+e~hIHi2?yvU72S6I$vsapTD}R-6o%)7en8N~E2$zbZsT$ssiZP;m#(0NI!1Sr* zsESE>9vi}MfiqzwqA?7A_X=^ZKK8SDQ8pum7Mki2ufgIAI1y(-LHFSET3N;IZE^26 zu2I|DrY@#>SMvic*7?{)I{WqIHeK6<8n}4dCsV(H7C|+2){~@RW!L38^;KBk^y|S) zc@<*AO3qurnCHX-n(g=W`?_y%V82H_WIlfQu%#XB>)hgojv-J`p-i@V7u91F$-8&1 zW?HljD`MTA^XW8`oQvzR3wcT#E;cD-<@cd<_ydQ;h5m2>f#N*-mKptre2?Lq`Vnj_ ztBeZysc_6})7UtqdZb_YE~kA$Q?&e0gIBB?!x>MhY$P$jv)@Z=u#pMQG6G?dwS4MMr zUV{E3gQ$(~1n2ZUyp%dLSDOG`x^1+rQgO1v27=KbbzJ^OMS%dwtDTvEb?H~u6^DY` zhO89D3e-jyerq<1px)`3D5vtV9(HtRZujJ+_Pj3cmS#Ww{sO@4&L#NLO~vP^>F8L0 z4>pBN4)2HU(Iuwa)9wA>6E>o3 zOA2B#TQQrSspgm!R+4s@q=}2H1-m0b*lO8}wy`m0x0y;#S{?^rk+()ambre2s$bJV zEm?Op8$X20%8K%v(VTGJBYO5 z#J|eXw3G=E@*)ze+2)0_{V^vStw}oQtLLLxb`KFf2{u)B+Oub&_OU)B%m*6wkc5M= zJRMM=b@WSO>`acD>yfSSbRXRyn)NGOQzL(MJ^8(BN(D5##_Ca1>XHdIcXEYVuGrS{ z3we{7rL2L4>)GWR;x5IimW{*%(oF#`TgBwKb3Y;XVqr|I4jWb8vXenBx-c6giOlb$hWHDe!;X0sKzy{V_JKRS6FpJ%I53e{!X>k^<*U%uC zR14G}#~-I?@4K@uwurNoiyM89+GER~)3nwdmh<_yBM#>Xos*-6ql2gWK{3frD;ipJ z70hnBA0Y9m)gIGJ+Q5*(zm*a^QA>OqLrYmj$_`m9~P^&ff)2;T$J-QC)Y*LKxB4#1O@DBJUnkB?1$x2%R%P&RlYi@|c z`ef9wwS@up5HgL?bFeA$*0xNYg@J;G7!0vxs#}r@d-LV}@~*b!J9Rssb)UyB1NV!0 zmg-#j_^$N8*MT9ySq-*(y|>LW#f_-wq0w^O%HlQ;sOjR~LHTIUZKSJop@k&&cnD?8 z*IMk(H!uAZN|9~^E(;*h$FXE$I=5@xkKi)HA8W-C&)UPGL2<3&%jZ?Bd!G+L#Z zsU6x_UhV+qobg1WqaRC0PW*hmQw)cVNOVRG?OT2_<_dD8P}{1`D+CMHS7B!61m>EC zgpJ!yoj15)eg7d{bEZfY1K2tT9L`6-srLW8C&yV8fss(V<*d$14B!aYEWaG+K6Wik zxNh6kp0=*L9WBybYAc5N!F94}Eb88{9zoYAx#gwyUi$Xp$W;%CwUQHdwsN3U5%71f|s z0Tiq^qhLS#^*p9rjrk+z2W5^!H%RRc+G3J&iyh&7#=oV|okOan~31KMW28c&Jc>&_oSn zKC-i4jo`BRTgsW9M>xD@s%MFX%PMyTzN$M|h^fI=C~$#x)eLgUaPE&?^+f`Ox8s6+Z{ksgT|8pDsFNT?N3@`83m0n6%MQz@ZmwIVEyVi zOJ%SJQd$ltRy?j3=&hgK6K34b55YV~iqgAlG&%v{o!g{Lan{wZi(N-@tE=S3C=&h$t|doCmB==b1WC&J`$)Nq zPW>+6+9AM%cYF``{Z({1O(;NJf{L2lut&{DlEd`+V)3sTo2Yl-BaGx z*_EM%J-%vY@oe;tS8zgH1b`Dm2&qy{h8e%TV)uSe%Tu}BagZka!mW`wGLY!`j8({$ zhVFna#kEdO#d!#{UwIGSp?^eJIkZtiH2*NURmLk_aEk4{ zY#()*JqJ0aElG)#ZHr6MK|b43l%*=AI4|TFker<&?tAQ0((H9EyxMoYB3hk|+k>O0&Gv^Ia@OJ^H}dOtT;j2msbR%%vShtXRHF?!1G*VbGa z9&5>L8;DQUAi9FWb@|M_{V6=pf!s0M3;vFC3bQYHbO!C~S{;Br0owe#B^)zs6{w2V zu<$v(d%ug?MJDcd2pjE;0Ugh$%py&z1C1hG@Ao?OPU9LbhpnVKmkHG-pF+=pL_n(P z)P?5~F|GUX1Fx&cdec_K8H$oMZ>_{KCfMH%zL)G)YF3mRxub4B4|`Z0Zx4Z9 zxTC)X0nUQMmQ|ktBU+=taM2I44&>Td6{+^5KcrEYy@Gd{+{b}{Y?M;R_ihPlvD-4! zCA-F>(LG1f$Y@J$j&iS~hKi-H4|Vd*)a)9dS$)QTdTtLqunA2Ozm$T$499vi0N>wJ ze4_30xON)ed8H1@wWhR|{wFgk#dbh>*oE{0neRRO8Lh*PEZmmaa?>S8es}D*S1@81 zIE@>9e`aA!zP?VizcU(48n)4cV9;ESk;!C6-c%|ir{Z52Q2UIFy*A~r=!BOPBYgOE zR(MD9#JBl0G8Cns)l>VFGrzzmlb35U+9+IGb0HC@QT2$QOOYftILimL$UvH8xOYS% z*}~L6!e90y`@ri6A}H;(Q^tG+#NAIf4rm2_y#=_4Q)#m-JfDvS(V5`YoO7m9JSQ?-0AJ5TPqN3(EAD);x|W;#3)h1RDaEGh}6Y%$KNcuN{VV?3zoZ-uOeQ zf$lCrqJ5sdD*F_*VCLjFz_#VKCO+;%qGGeKVW|0PR%<>z7Z!TD(mn!kj_SSp5A)KF z{Glwfvu03L?-}Q{t|I73b(D2H-Aq0Uw;fQQd(-Vs?-3~i)tmc=3t=~Yf;5i9Bqr{dtSy3pkOcZEmwuo1Go^c zkHXhie8?8vTO0gv4OOKU+OvMnZK2`HPxteD-E?jjM`NZd6U6)N_9s*0t-~HVE+=GT zUkgG66qvBApfCW6x*xi`d|GqYZ!mMa&72aCw085YlEM)bg%0=$v8lW^7drsM0-~s6 zp{uSR8k5z(?)$i=VU+vYO^yg}UTW7}oaa7K_#vMk8Xh?was2Tk^0ZSe{ri4}JGq#~ zJtb%`W#&f^>3x0hc1Pt?bN83DQfE+pk_hpTK=Y6Yc|E@oYM!VXh^-2Zo$z@Ikrv;q z>>UoVPPTgD)beqqxAnn1Mm)@E!Hw6vy0%=42 zN*I63z|WuOuL+R~qxmRmOoG1Ne?r72V*WtEB-&a2#ZS5;pj>P4-Lg}A%iB>qB@~)_ z-8YPdfVw*1@s3!lv&YJd$CT7mq0w%LwbSxc0Mg^`U{^^WX(Z%xK%|IdGsPl_@{jVP z#b*OJ1W5c3@5b4t!pf&x+ztYp2YYVYVOZ;FGaaWcCoI7xI_kw=Gf>UI>A8^qvdC?-$-RLn&7EmpL;0(s{Ib#)*&hkPbbOVX^TH?fudll{5GRhlpkU>baI%m;?>E^A!uUm-bZHalS!6XAt{i zN5gj?FlMFCpc>$pj4gk*dNUp>_sbWy`p77t?a{%AN$Ki3J@HOT-Sbv157=q{)DfgX zu}S?5nj=8EP?DqA@Rco0`)6Ei|ieL4mG^d0I}N(SaeF; za~x>2q@|Gnnymn$Xsy8wC1|RddwF?_HBspbBM`P4ABOKwgw|b4$G%jr8HaY>+Fmz+ z!{$;N*#_O-T`TjbKj{xdBaO}w;nxy4kL{oeyjaiT{jm?^{Y&>DixHM-Jq_ReCJHqK z_Ss9s&Mi)+Z%@FYjG5ZnD)jZ$T;9up$0p5nqHUM_D?V(Y{N2(J1g>CjxBTf{8f+(b z#Lq#TdAHCW*=3$`>xG|Cw5`_}HEc`0h$w6WXm9QxzC7t0{&YDQNsV*Jtwc84QRp&p zerR&rv7HL@T<-Cvt@-XOSnV_21@vpcYs+J8ujux$0zR;R`0#jXwM18Isq&PqN#3Vt zF)LHD>#D1M3Bo+LmQ>95+S*lT$NrozJRP?ht=`_GT)=kHVB=Iz2CcSExS#f2Gd?inXb_()ol4 zLm=DQ+1Y)TpXxLCLX->CZm*|$x2`^wsaL<)(}CNY?p*1e-0NhaGuRksdlYxd0;&}7}oZug_qrO=!aJ( z?BIE;Eh!6zQ4&nR&n`9@;_T72*bD_&zuStRsH~>>>rhqw%OL*mUyne5!L++LT5C@q zoUejC`=+mw33N~Z&gH20GA}VF-mZZFvXHO7;oj$}3pIRpt&w_|zC^bXVNVqDm#eC} z8tZ9+Yr>)*#KKRZoeg4MxDIPTZOQAP(YxF68R$%V?x?Nt-ZnR1=jkks7U-}vh9Oh}e+Ngc?eRUDyUSppNC=VVU9;7R~U5sX+ z)6nA+!Sh}BM;MW=f&Jw@t|0p^HS(J?<4t_x+>^M4oO{=$iZKQ(tE$pT=?|jt(R&2 z!Ks6pa+UdVWw$>UD|VeqVJn%ZyxP=T=Dpc4?#J%GwZypef`<3R8qK3$#xmuP>hC*^ zzu19FSqReyRdfJFIm~9hx87b}KfN*mF!&oaz*0zLgt7WKn z30+%IynwU^1f@|izJw#?o%;hQEo3D4genP1;07Jyw(|7NKiF&lC7 zk4w4RRg>p`w7;BVXxPtA_-yhdt%SS@vFj^u3lslg0&_YE(- zS9m{@c@cCudQUCQw@2^PN-fFPv$b=6mF34yOz@X8Es~iMD-b}_6=LA_tfd!gaM(yj93_zh$8U}qb(3d!pgC}~E}Duu`oXm<_* zJs|>Q!8y>V{a6_P&a3>YjeQJaJ+2cO`lqS%q81Qqp{@Q?Y$p5nU+fK&oO>RJ#bt5) zO;i(9IQSjJ$NI%i(#E)~an~$MSG>+l{t9~0(Ld18*{xt={RBhcm>F{Bci ziJv`e{QgU~lI8@J`yNw-tpr;KgwjZfHn`ag{C~b`^n!i&tk&hgz|o2dz|%cKDk`s2B%U}Apwx?R zHEK@BJMB6*d-pJqM8?RkUms)D($am(JQ-yk^cz_Lw(tC7rl4Qz=ALJ^vvNrNKaLRbwHUVN6J;}rW{$(OP9R>k| z)bwi)Yu^MAKzLv(&ta(WL^Q^WH9?BCD)cd-$Ysb%nqpN_b%tVwfw2PU~Vi1>gT zwS=jejxS$Pk@~f3+qOt2RM_$Pu|(%L$)VKbrEf-+oRrmhc>?Hb{C9`{o>8^yW=CIw zI}KwT19Kt+8`>Q4Ge>cd@C1>QGyx~@!*tyUj@_=Tgiyy|C6tFHNLvN z|84rwX3X6oqHO>D-IEV_=N@4-L)}(pjl5g;EBdbfnm*^f9JpUcM<+?doSu&EBAy7g zD_l9NxU6ip_rdGV6p7!ngICvA=A@lRb8~Zx=V_w8-afX;s_0u47}yxkqshg^Wr(LT zwPyV{tx-c2j^C%J+g(R)?US>8lipslZ~bHou&JH3wMubmLc+NWML(Ha zSFc~6?|nei*wOIJ=l8t({K(NCuO2>pcsf<`a&KpVsy5Cfv;!;!X9UM0PsCzHZ9PwG1b^2ISa%Bfe z6fL|Uy}iB0Wp;~yT-MiryeKj*kGg#BZ0ufXr(g3Q<+dJutZ_Ffv(tHW=H4l@KLaJ! z)-Pk)x2C829);8GHTYR7?urZ1OcMUP^!8Z6-aqG+l$7R%%3mx$k(HIbxmWtB`HT9w ztta|V67S#Vc%eYnAbpcVVXY;SDRgTCKev-j(r$C(&-LrqCD((v3`=d=YuvwY{{Ebp zn0WQun|;zw>7k*N(e)&mcCjJRnOZ60berbSy_fU#pV(f$++LGc6n*QK)BA$Kt*063 zn4UoL?W{*AKZ)72#V&n&>%Z8@cEn!Lw8~|oCDPcq)i=>{xWW**Ia{zfoR9uI%`(ALN(C1BeU{{5K0V%?-N=dSI>O^1}Y&YNUCc@oX~($>}% zTLc4N*xTFNP;H1DyRlLH?AetcYzo*m3kxnhJUnsVgRTXnr<+<^Tl@UrS}A_;fG1z9 z(&^*Oj~{P;7dm2cKGpAgt~EXJ^5$L%+s-k?3%G*Kzw0OURdagm#Kpx2gg>g^=2%`? z;ZFGH*I9P;D2_+h_cQpR4|9;!mwYx+#!oulO zJ)N8V=?ulcjltUktc6V~v=;2l%*-hK1xvo+z}dNXJ2*Hf-oWFn1n|(e*k=!fn#1T0 zp07^+In{HJg(XSBXQid3#m3e)UT8blsH&Qp6ZW`XPWImX z^z@&ZcZW+uRj9sg{8>^7r~iV-+r7Mb(1hW-uz7u_jra@SKgtyZtH4Cj9mR74*IZoe z92|DtY^th~QPP)^k~(?vWVQ|^X=7Ix-A3+A-y>}o7njqgZw8RwyLaz+SgM@IQ>tSR zl6#~K?`bM2kwi9aqh%$zQL#{Z=`9E2#b}X~l$63-xivLw-B~K<3ys}3Ru?hTe>eUl zINT(ZjK6zwo^{j9d8F!ig^Nb?*r!l>qZc<627ew{?d$KqhJ8Uv!@Tg3or8%QJ!}C6fx_twqs8{IafvL_bbXDJ({x1GJ#L~gil4cT^WyNVr;g7ILXnb_wV0h zMNP}?-<4Ruva+^rPo&K{G(A0Sx8+rYXP8ykxA{*GSBiwa79qQLhR;NiLRyCX&E3Q2 zO)p%yd-pD-?=&XO=CImvWo6}Im-+H5AL0w6l`?r^$U4&mB*eud-iHa56h=iw$p{;q z|CYpnaF(BtKTNwHaEO_ra~4%8!aqW-y_^2*1nWGgOFEWulTjkq9K4(kU6@p zS?NH9<8atacV1pzXjoX3mf6{}gsKVz#7M1Vism$BcJ|Y!Pse?U42WQ0W@Zj})SV)6 zsg0TDkBCX7Q>zO@W{=%=dUhN$iaVvHrMLGVGkb9Zj}0nJ;62^+#8}{6MFbxo-}?G` zj`8?C1s|!weCjMFVe{fU0q=)bRu`?+)zzCOH+J5-bqfazcY7smkTT@Y(l=>oX%WNX z0FukG^i(u7)+L5^J~#Kttu%$w6waC*K75!t`iNCa#K3?d?J0bq!o`j|;XC)A@_HVQ zx!WgqJ#$7^N2kQ1F^EOPEM3vh*uWrB^zxRPrY23Dm|$%X4d#;7+i|c|jp7@*>TGv+ zH;Kcbl%GP#uF_}EJm!W%v#bpbS@UC(2@KsMBk36#=4a0~{7t)09GjnC-3|&Ok7y=ICKo3W=<&EW28Vkt)|8qtEA_ug?xX+kM!8UBP=- zaB@fpMGNbE|4DBV`ec?(6%`f#ziZ30gA#SS%eUR-akz9zJ(u}1sUyF$O{gm}^T;E2 zcXzBv??wjYI9dIWosErZF*6mR_WGQ?lhcXe??1o1es<|C?+J#)pF|U$!)XFe>aea2+fYyYogQT>y|LVdxHY;x3XYCjN5I1F^VU^2x z=JDX-LYi~Ts|hh=dL9{=Cq7*oe@RKn`^XT^qw)^aWz0gx{r&xAo{LswxyOlHd3#w{ zLSlpHzhYl#@g_7{h_ceUmL(iz zr%&q=7Wy}Yg@pwL1rHr+d;R)#SJ!E6?XBhS=WT2z0n*wMPYN~oHU0eglj2I?wa(W` zBHH(4Ln}T!Je`)gN09Bt=0>&a=M$y+*Oc)6bHkM-cW;n9Dk%};=bz5--~6*MPX4np zFOT}wVv>-_3g9LCcJ6(P>peUS3%gh(*-j_RuPeKaH&J3~9E2MYV=xe|ep>ef##{GSixCd4+{@h*r^2QQi49KEIzvM{9a|$_(9_ICJ@O-tF7BV}mVT zKMvfnaD^ebkr2pnnPe_8&~JTZ(eHUhg_I(3-2YLVpPw%vAkfj#VP(0cr>D1zh2_JC z4+%UL?+c8O6>*@ye*Jpl#0kt)yA2yF>*L3dV|l0v3w@Cjm&nq(P9z@txkMa&y}kAR zTcZd{4zCyQTm)9MtJO-Eas562v864r?B&o_7>*x@J{wF88rx$0Q_{5Qz|C zH8eFW;YGwbRu3ZR;^7`nRod3n>t4{B;^o<4o5p}NQ?BV%~{_;^Dg zIqMp5KtXl&;Op1XBp3Rd-n}a?E&VzFvDSwAaY2FVMe5<9A+pp)s^INAZ!C;8-r+wd zYQu6nA_DIz>%S>Kbj#`{CP_49U>c1!mR-l9JT(RhDeE7&i1=$Q?Q|j``CM z^uBua=-i_-*Tx%o<25f_xS;=U;J}Lu)$$2G~x z$>|C{iK1$;?<1mlHm3XdocfdTapmPlf*TQv&g$qK7d|^PGlR8j+x@VPj-5W-!}+SC zqY!_F6|fsQ;_Xp4ZvJPVcaj=aaql4~Cy&jsx3oN7(aTKxz`&CsdgfA-sBI_fWCA_A zOl+5JLiRXr>~Z}aKw`_}FW)~DVbKT91S7N%7OMGo7;njo;Es{vxa8unTS!8er+QQ* zioAjni;CRmUsu7 z9?-iQt;`KRKoPQfI;oL|ylgX!xuM_Vn z`ms5y>5acVwl!J9VD{Ym2v9w?P|s_ni{7{p^0a3?@83)D@;(HHV_n;}eY>VEZ43!e zYH_O?c*PVB>G>kFdq=N6#J(r(T@5JM6U=#Q{Y)OUKt^$4p-`MLC*jn=I+j#f3&Vz= zD=YWKFYXX?b9tDZJ+c1#2go0?6=1N|<;(noQN`Bw_NOW+IO~1?{9IeUBKFgIiRb81 zuRlM(*gR?2?wJrm4}8s@7D<8~PDx4W5J|x-Vpj0@v5d#ODcLpjeH*0&2dkVqwc|(h zr+UA=ioTwno~y-{&8iP4dvesbHrGWweMY&<>`ID@BO)VHjX|L?<-FY&ap%!Gdh)g}U%u$;>CMj078DlVym^!Ezd=izti_w(Pd;$y@Zsi* z?A(WSkv}mOoiQ=7&rgz)lE$Am%G&){(9*Sd?tg2ksmcJ*-goRxD{+>?7&EZs|jd*(^$&PqgvjV>+D!J>ku>o=_ z$npn;gl;-e8xmf!-Uk#wFafwf9UvTsWSg|L@$FcU^V9`X(;YN4YJ{JQ*zuViopqP{ zc*ptXMxXm2rL@Hz6ypws&bYQRx3_|DCF<(e&XgBhn|mFLz|YiAoY-;H-P-zKbgNZ~ zq4KSR0lmGt47=nrXKy?L{=i#&-d+B*pn%qnIyk&Yg_B^v zq-1*Zc`de4dAU0XVgI{#d-v|``!_%$SoZC^U}knpvf|k@l@ZNkrmC~yNNUJ$IwR7S zk(J&(eNWC8t>l|81Z8c=_E1^V-rghe6_ka18!e|&QiLa#X{<5$xbaiNlAtbI=i!Q* zXBD>ZXX%rnvGESG-0fw~c?o=EWPuiAtBarc&ZKji zghnhZEErb1@eB&Mka#rfo;%lvHHW_jufSBZ(i#pD#$mL3#BJILqvsDfQKuy*v+mjB zKHaNkZl2LRp8N1&z1Px1{k{JF{_*|e)MX*mfkBFvA#{7C`rp6LcJn-wE+Zx@o2sGH z;x^sO*z40?Qa-!#K{(ElmxYGp%ImlwFx~)~m zOre3{D2Q60g{N^kA4}GGFYoZ6qvE_W8Wi-__Vw zt2oa?ZIo*vJ*2EFT1FY7$`%$DfM|MpcgAdy>vq!9Gv7U=+;Em9JPE50Qhq{BElVJi zi&c-_^8ESp6BE5jf=2O)iPy&(0$145W!-l)a~PL2w6{+WKD%V+b5vILluMjr=h>s$ zr4|{*ByLx)lFpg2_h1iw_<$s2Y;5dH{wB>!sW-CHZ^sR*N&z^o?52pOd`@ zS;oxJ5WCb^U;l1eT6CXSfKppaU!Mf@1LA>0AZILANT#XMu6po5?GBH`unK+xjG}E= zZ=o8CRP*;`M@K#~w#?{Y)RAt+6WcQ*qq`W-XlQ^#SOQQMB7o7-#zy5(RbAq`851*$ z-%RlP$EV>d#m=7;eJdYveWya^{qyG!Ua`3}>?Yo39J}-;efrzC@E@jB;NBJG<@aRW z1->%G)(s-c^}c(z+id61z9vB8CT)R$hK2?Nb?Cpvb0%?daq%BZffEZ z6r^pw#<;)Xiu#xU#^fH@2hGDscR>H#k~=_$$@ug#TT zdN#y3)$M8dGVh_4yZD_a^~#U>uKXKmsi^}FQf53I{1HV(BKfKGR$b7RiyyRS z3KI1{O{JR%ob!pr;Pi|i9l4*8Smby zow_Y>_%OH+257c4;?EJ{$nxVHFu(>n<~G>|rUl&bz2=;0b?n#^{iLXJ+U8g>=NvcK!; zIqvkKil|-flf5&Qcd~JCa8GmnZzSH(&`_NU141<`8(X|k)&>&m^|%f@y} zQ}Z>zOGig+`R$2`iTHm5I(U3MqnMCXHCzBVb#aBts-E87h#q$xcFU08M+Ya}&PY%p zi|=g~qO|_O1&`gNkE&>k7(=$AFU`(@7=rIZMRi3o^?j}H>S^X7ns z6(W_Wa{W9%QhiXXVG<&>&e0IHgr`QDnun2@K1^=@{GxfALxC!F9!J_H)LP%b;7#fg z8sEbVB9@kxkV+{c;wc`p@55mzc5U<`^K+$t8{ZgLntB+ z?FxeYQJ)pT3buraHheCX@$5ks*SdxvlP17@urPV6HQlT3+P>rGwsvrlDCguH2w~i_ zhhK~Kx|P)gQcS2(jM9gGa9#8v|=iWJE-?JteAusnL&sScw{lJaz_v5T8~u zLXy&>KAa8xvu6Xs?XN*&MINC|+f(Q9!;FQ6g-rD%Rh0ufC%s-4`?t3lTPPYt-XRZE zdfZxQQV9`2wPFWD599~jd_rLmfqR&dL&{+QI2pH_xhF=hn=gjGy)Bui9IUCYsmW@z z&TF6sg2!&^go?5<)nRFFZf-?Ij=I0keST{Uns-Romw<`=l;;dw1a}|N{Rs^FA5hj1 z^+lj+3#H*wR2+Tx?udxUPL)sCJdEECgKKp|wP95~r2AukQxZpy;)-vNBp4n$I`e{Qj;TVb~*V zVr=YrZUV;%$H%hI!NS7j&(Djvnh`Z!QWs4_vh17#GGs!Q*OW3u86tk>PtMOjee_5g zFt4NI6z#`zy1E4GftN40)t$!?JI>#zssa%o!`=PJpmH&Rfnj`RCTflyLC(y=A~Ti@ zNChkM;n^i^oJ|D<1-vpbF@X}|BQ3?b14ZuC`Tk*SUPTdWX?dCQGQSKZ!*qZ%$6iT0 zLZdOK-Z&~ffXc`(sN-ny_Mt*zZf<`5e9{D;M30M1I+QM(T4y9YBtlFEgvYk`YP&!* zP&a@u-qI~CFJI^Usea@UWvOU}6z@VZf zl`AnUb@K_5#_W|AmXf*-ggc;o536u5J)J||ljMH%+qZ9va&sw~O$i4EOE13u?qK(LaR}9=Ob2N^kftlGGtCAsM z0!;>Ad?~6Kh-@jXR+dx59d9NvFgz^B%^h+g2LLoBH5CDw`TT>ex7g?<7sX=l-hB?7 zWpk;Ppsr~Mq1b_%0A@hZ-O`#a$Hv75CP%Pd1+ZnU*;-vB7YjSBr6udYzJLGijT6eM zC%wJbaLXLWZp0M&peln3;{r2J3PiM6q2q)xMN4W;G75$ONKT(VK+_6uK6(1|?&|F8 zM$c^NBA;z7zS__I<_tU3!!&s>@rvBt!yJB@&EuKk)0zI~Ph zSW43S*VNFEF|D8T#~KK#R3ki*{QZ%)zB-8o$5L(3ca!=xF$SYV2JsECq1b&3ECgQw zGsrpczjy7i>Hz%TTFTh6kP1eQKgh*pF~G>#U;Si^=7B)B(xITccXtmmwvb24pA|8$ zXP0(jmy+;=_5>N@vm%n7L#?f>g#huC?hStBX6X19T^t% z(g@|DwR%dT9Lp0Uq_~}n8}S?&;hvm_aK${4Wy^cI5bI`A(3|`>H;x(v5&&lU3!o)H zNDO+xLi#u_Pfb-dz;pe86={_=tMFURoqEDSr}?>^UpF8Mp$r!8*w?Y&f1~lEcpsty z<_Lc(924W>0*@Z0G{Q0}5c`YuzwLw!jhs(RKOG%7KV-9pN=%iI-4HYC>pA#4O>}e`{mvR2rvfhkN^Ph) zSnwvKWMtg_9v&9p23`WK!Qsm?5{<8)AGEYLzdz!>+`qr479x;!v_`X_f<$R1Um=9= z0FXg-wQ43PmbywC5?Y{z5FtMk`c-~+SwEMh<~A`gF;sV+78SkfKCh|y9W@V)Gv(Ou zK0ZFAY0n{ODS}0#TuSvQ=|oP?Q`72+uV2Foe?W^DD_YhR?O!Zv`f^@?b9bVsGJ=d1 zab#0{6%Z5zdth!^>bS}18&sc^S5)}smmJSpa}bLUQV zMiKw@m995$o;LrjL7505E(ELOOSwIEQ2x!z?RQtyUwV7L)l`Bph=!g18xB~G`faBo z(qGi)Ncl@ccxWNa34Hqk7R*r8)r+4o(A6EPbl&foM8hInRa;y6dme=lAef*8*$B== z;S1gn6~xG>f$Uw{M-H+6=5gQy6M)CO0vs+6D>X*|TR?-P};Hz`!X$ zgg3FNRt=zGTMNk}DGSMq0O|yy!Uzl`U6S_p_C|=;P_3-1lQ&maf0DXmCx?8);j}Gi zt>!Oo6h3@tpr=PMbrjj)_>AP8J9jvjsRtdd_c-s^u_M+vw5qK+6v}C;1(3@92~5k`+nuaQ#B6UeAm#>8a^uM zT=h%TYFkY*kE4>ZfZ=;`iOiYjfx8PP0WXcFoO`tx}qXikr!NSU3D4LtJ$^zVYXvNG;$;ilJSw922f#c=O_}~m-&|WD= z>9iIncgTziWZJOIj-jHyS4mSd8jK9n1UX7KwqTCDejo6*{go@EaaeHBVY^4)Nyp++ZSoP%B zuPC@p$YjROeylyeko4fmleF}7L5q;mQgJ8{b#EEk4;(v|k@-XT&>^bfxMKispGa|E z!!J?VUjMVi_h|({(3AZ@dw3>OfCzLCO1<3-3xIVui7d^0+l{PG~Gp!H{TgvI5% zr>~7ZYTFYLP;fi(&YfaIKt4g8he1n=i@(fhb0hq+dM0U&1>PXBX_V# zS$6F*t-XF4hu`Hj*hg22#CDTZJK1|T;JY{=xphKNTreO-C3JSq&KdXb3o9*QQAC~D!RLH7 zR>dsC&(UT+)RvLOLw0Pjf@Ap$bH5_hs_VDx zyu8Tu{%oQ_NBQ9ltEdGEY22SG>z+SH@dSbL_VrG1{hfQIW^wSLE84Bk|BqSbq+nM= z1E+vMK1ka@jY>pwOG}6Yb?hz9;ax10j6ax8+&vKM(HBiZa={+U{;IAdWzA)0!HXCE z;Wx1#1t)h%iZFg=;!VoT%v@3YfkF@EHQ%jGg;hNB{Y^S{cw!=RC-s?JI)?k0Kdnft z$Vjcn?G|#%`~FoAA8z|SNc!0a3Ruv3ziL8aq7+oN2J2xCtQHP1LB0n#NbL3tu%FQ^ zL%~GR$~@HbSATw?X?&$mT%6?QajI@wza~ABuRoZ5vU76Y-jmxtswgU2jBOkABpH$l z1iGLY<~@6k+I44^l}WmgTD3s1d2~9B8Bs7x!N|}MbhksgkugNo*Vh-21Z;m~W;r&N zP`~GERX`CZ=blr5bHvhdSlG6LQwInU8Vu&)kJ<>eD%cG$QvI1|rLo*bEImX;%@nWQ zd-CLp&zaBk*z)_1-SE}XVPHIE>*)A*tdZP71v1gg)>i6)wrvL({v%VR`bx4S#!Xgl zQ4H=+&x007C_CnO@`49boy%arbG zsGEZ4?K(IJ4hnG2Sz+3Mm2iH6)<4g@Bi2aB*v{+gmnR*qQ#sqJr;?jeTr4{I^dlr) z2n&rCb`WkZUv}NH|CyPat6E{kcJsuOqM~muGVJt^5bLV`jmM9vyucLhLMFO666J=wp%lO?63-UBX2Si~?ugNa3LaXG=+uwti= zvSo?IF)^LUBCGYpIxP$xRwdBlFo;;Hv=sKT2zy_)n=fsjHA*xtwD$Eq({DAl`5cv# z^BFS{?bwML4rb@TRjDWj|E<5j5klGxNJ*8|)E=lv$Q(FuTq$0Nib4e7zAaISyv(78 zOW1_j)%0$n9+j*LRTJ{>;NVe|garj>QBiGeZGBwxsmA?EO9aEf!_CjPj}fe8#Kahe zsUBA$>q1FZ6=hJ>uo$Z`1m-S|6|M#6&z|G5@VJRfuKqVf<72ZGA%U{W4h;>_<1B#K z!Aaxz33CBfXY{OM{?uwrRtc%FetREJYrtxzw%$u&G2VFAkv zbb6a70b}jp4kXS5FEMFpBKXCOLMj$Et!gL_!Lbq&6lA5{#ux~_PuzV*Z{NQGK?oKR zj84~F_*(*^yHZnA3w!ZFeT(}t35+j2(_>;HW)h*NFi2GF)_M9{XUt%8(2oHKgrXm z+JFa<7vGIJDEWB;%8QiKNJK@_ix|@i!DeiG^WAYVmK6J1q4oMYwuL? zhS3+n%Iz58b3w_-h{*3y%>6M?@)`P( z?=ajd)UPzGW$$q?FqGJ|GmbP*pc1A0xY8(}kP?`mnaPRO%2-64SOt0c?(3?=LgG|U4$qILANu;3QX3gj;Rqf3+2or75fX{k zYikf|iDXQ)*3Be0_6MGM1PazVI(h^6#O6vC#e|huwwjsQeV9@#&g|q@$oy!pc^Z-I z7EH-tpRS)@mV1B4?uPEx==YMyi!anNzamAL+Sz5nj=FUSiygHGNyUr|g;5hA5K4F` z3_bRC94ey@-kI_m$4czdjJB2*`w{#84@Kr;Z_gb0`sSX`*Umk(`>_9*7#XR~QZxnv zr)7Vn=z)d^Uq{ofzb80#|cMlIfyBgFe za5O$2F*Y;fs5mW5sE25#j_?mF6$gR9Suf1Rx=$zqJJx|zPt~vq+ZmT=R}=;o`ti9> zW$ZcUf#SeMi%!rYzoA})3dU$P<8bieZ@R3o8|XG&b)LSRb;rwHHQQQ z!otEPbQ>AD$~EkbbzB#zo2RDe;fy)V&mWgta135?cG+u>pFLZ8b5E}1V{ZU~K!9eu zjeWcCuc^2P557#aM*rQ|sJS*B^tr7!vhAskOyV6xJm}@webZdEi*~rkjnk+f7#Z!+ z)v5=|35Q|NCii(#Qj%TBIDJ_NOvCWI1;h2R_XC^90N>y^wHFLIHh1Xdu6hVVms>K;RiOiurmR7`A#sX8jJp8+6N+{ z_vRKC$2w43Dc!hH&u!Gy=BI;$eLNWIgcpu2g+A3W_Zc|P9=*qkf%r#uHjpupg!#o0 zlq{~5aiaPgUU+a`(Gs#dN>Vv9(BC~kd)bdb3g#6Segg$a!(2trLz=r}F>Y6ne%JQuKGzy{u?NQ84rB*SGK0PP1>C+<19U?mNTwUA*V z)Jc}UK5>OwJ55SRR+i1N2rAdg{KviOdnDNkar9sWK-L|Im)5!`dqhxBtInXp_U-+f zLjE#MI2D!7pKfIlb9Rpt)i;{e2@hA-MHJ$Jl;q@nAaJwYKm{K1NfXF;RFwFQ=DpW7 zz9!%V!eGN+L8){|Ek> zvuC?;l+>iGFI@N-cToKn`_Ty~wA0i4Has>|svUueF11wW80d>+HbJ#v^u|?W&#@8+ zqOjYTN|pt++W@3i*VKfChg(l6`aS(h|G!=UfO>e&VV5Rw(p^Hu2OCMG)2vj?`lW?t zky=iV>JN9{)rA8UTGK&Uv$jLzaRGm9kKQd-FBoD@=^R z!NFp`mKPQt7ZsV8+EA0$EVMK?tFTH`finy+({OT)fp@X`E(1?NQ&1T`{7-KxpZk7I z_5##c45X+zY5W)QA!sXmcXy?;dD#UR%yf=Vt;X;0d|7}1C48Y~SFzaY5R<^cZ_v;| z>c-~y&p0^=6NOvWpw~$Vx)hX@I+J{xsAC;AC@_3+nfULqDmy#fLNlpuZm-M*jBAaX9EakKZh-LN@MG(tmE^ij*}6LSeUytZ zH^`?64hIV1*rcN&4iuD*Ygq|6pXOo-+Hp_0WtScGlDcR30#Xkw@7)HMj#r^SCy0^; z7YS5*#Qx49a(8Go+=mbQj5Uzb`l0(LFT?#~W8*k}1e1fULFw`4bz2*>jINH3C<}Lj z;Ks(EzShkmkS%G*BN0<2e6HrH6__6Z9e&)wlEwVwUCRT^0S5AY9g?*4j|w{zOwo=H}r!Amsu-->v4E z>FIq(-C6*Ca10URvF^>TE+ODy;G76jh9^$g_B&%SLf2KTwwHw*sB54iRUCQYzrV;g z5)%)zB}D=G28^3vfYuVh0G1$28hPuLh@t?&G$!WG&OgAx>vl5)y?C*Ogd{t{%LeD# z?5v=G4&fU1KjI9jin?7C70Fsi@0%(8|JuJ<2&}YD}Bhum~A~YXJN7w}DYo8G*c2VG%(2s((<4$fwHWCyX3HV2bLp|hze`>0!l6>PKE^c>Porap9- z9zL@p*||}W_>Orl1UT$wjQG`W)EN7emL-#iHZURvH(Yq3wgM{&2Ht$OT16CG15(%7f(HU^(zwHXuY52uev*A#!Un>ix8zN zs3V2u?H@+s;{X%!?Ry8o|J|!s+q<1dnDzDF;oL!1-BGA>==%H!1VFyB@2R7sqtKLi zw03QZvc@0@qX(tNF~1nOKU&TG(6mi!)NZsG!7gRPrQmJ^Iaecv#ki?y0V)#q7$}(g z+|WDp4L}ox*MTS?UeJ4lDh;u}uUJ%0F0IuHjzs&*mxavh(;Y6T#-*fq!A=kF6@o5< zSUr?T97)PMl~^8>B{I-kf^et$OhJy1?;A36o2?Rbds6akzvhNNpmzqAb_l18=f3SE zW1uYyG3m=%VwQkmu&uMx;Yj`M+i#KfLiR`r2+Y6^xXbt+CxA05FYhfs=AcyGJN*l< zWc0Pxujm(E-kAW+K~}318^;0JCUTXGK@7{G;PsPxP=GrT#hg-}2&yS2=5sJ3piCq* zdVcC!4OR&)C-MBoDBBFV}M-7QmfdWz}`-u6~R1Wc?4oDzX;NZ-$;iK7jJ{~UG_ z3xN=up01UiYXHRo-lmujJl0N5Z_1+_;bKONF)LzKNi-5`g4we-_oSUq>#JAT>V(_} z<>kvn7hZZ1VmJ!<04qQ$7|;l!b?p`oFqL^L5$o#TrD zE&289+NeN06q%Uq$W|y1K;ze)ELST%JWTAIMDfDTXYSLB{oRZerKQSpH!uLeKnJiO zh&bK$IrQC(oBMIM4d?Ak820X!L(IYApgP5W`0%G%4-wSaaX?wmW`?YRPFCH< zjz%D!27P{tQ}moO1JMpLRb#~~{4<)F&IB07m7>#+?Pg+XDjR{IRKZN;Pn2VU$V>G# zWMFXs;y88clnu=Rb~|d7+&$Q0r94!xao+@T);d8hTPQ-n7m}Fa&V!yPmwS2bL$wxk zN06E3yt|=AiJ>4f(Gl}o4J-@01Mi3IRQ`!|?_Tub00upU4$fk9u@o7pA#nnwO;Ew^ zMX`+z)_+4rMuw21aZO2c)t|HfKqm^O7a^!%z{H2vr2m4vCn^e7MuD8ju0Uh(HdTJ$ zl$RfAZKY%>2X-zr`}1o$p_Vw-A@F_t_;DRemfr|u3U~`d_=kRVk1TQYUGkf=@#l;;6+5_tkOrF>b;k0|; zB#gKt;^yK~TvF0Vu&A@`N+mW1$+(PxCs(oTXTA@t1Gj$tAQx=nuwJM>q!{6&HG6$e z76x%9*?P&MN@dN}EqJtEDrj5y6c5fK?wn9n?MxOU_qvFniQ+Lr2^tZfjnzY7LdqZC zyXzjB+*ny71|&Rp`ho`CHt zFImGMHXdI@HaJU46}AT;xbwHWRN6hv_?d#wug;Bf4Y%A zLno68o8b7`nz!!PyI*~FQMoH!caNk5ZoBgL#-E>RHcOBIyaBXVR#rx~P_iF_N6qY& z5?Yy{5joxfi0K&B=^txZN^Po zx9!-Q>N?YpCV14&X21`OKwec{4Z}g%2es3u=MWE3(z~+k4T51|!Rg%a7PCd^Fh$AD z&!5+voDK*H2P@Izj%x2UWK6iPH}i0n{Wo()<(WJgh-R6U8XGt92+JKazHZNI_gdiB7w6x=hg!v$gIFNJ@K;9RZV z9nCECb9woQwDgbSFKC$eym=L;750G^0SYunn_uD$7HpJN_A z{0d@-rT|s>BAfLP$ZxRjl;FU=z$V1`jJ{zeB2t8lbK3ER=m885hkm|v^V~&IR_qi+ zq{(_*F*_@(C*TTh$lxZTmOvek?~hT1hD0cxjsFJpE;BT308~MM#aD1$kM{Qd{FdPl z7lE;cSU)NMn#bQY_otfsl^L*(-_Z9II^aMNd2|k!1nux~0=wETJ$-%BiWa}ul0bsF z%|w%bcPgUuPWgaZip7h-kDxzx76@A-upZ|J4pI6xR8YIdkqqZ8yKjA0igIn z5FrfVcV3$BPfF9KjmY7lz5jzv-*^8uY?>2cP;JJ+wQMe0e5!CvL#ge@VQe+$wCDj` zGvbb^#41QMTJU@N`atH*O(a-LLP9_Aa!S@HU}q40nX|K|hK4}rsj;zRlat(HDVUKdeB{ulZ0-70+7RYxx^%rWJEiI}jwntTcNBBpc zIw=;cqo&T}s2@aLWy(eN{X(k$#Z2wwu#pKWqyF<6x6l!aj?&$`Tm3E~DdKRCn5u96 zdFy{)-V4P*BXe_qtSg$hn0N1{<{;KtV6{-*re{$c!T^H+@H&=zW^Bx5eZ|pI@@y2& zj(N2krPl^}BVj3`x^*!qg4W#p6Tn?uAsJUmzI&l?oh#K<8FsfLBXwR^;Y6>~mjxu01^{8Z`E_avE4Lkt8}iBFLx&uO z%JNhsSo0!;CN~@4>1gnCCAK%F7*4t?sXZCenH0`cB7kk4xXnS=WkHn{JH-^Hq@iwN zl2(wXdFm8+N?Yo^Botggia5o@bc*-H*mN{VwL<+C{e=n(bb)rF8L-Y1-L7Q1`4ksw zu3bhGes)F@O%+c;W~Nec;#RL^yxM|~gB>*!7|vzDmfTr_lcusV(VgWSmJh>|{u2X* z8yl=%ve%~0>b36;Eek2a?)If?;9D=?s8-F|jy#HG;8?O>Rocp5Oiy zWCy4g9{Xe`_$qT|hpTF`<|zzH={#=3ej32+z;vRj+5#o+c#Xxga;Rz|Hb3LA#u;)R zx0=V%@(7c#YZGl~6e>IrwA5|-7AZFvg~T5L9xSDFk+c-FWR7-6N&lbv3L{~nppo*#4`v2vR*TE<{=LZ9D?-O0 zM#C&9gXTb#KXgey1*P}~Dq5B(0ZIjLM<_!a`0^=pxdF%?tObM0vtb=ob+(0=neM-b zXc29J>-K4Ax(Qj@6eXsmrA7EVq5?7lwPJq$R>i?>JMV{wZ}(71kA0kXl*RQkIhmP*QL>%7o>E4!AxI+B}|NGBq|9;7Cz+J-Vc15rL|ti-!d7fq<}o zKe2%rDGEic@1SEWRy_2vc?gVAh@ z(?f@+0H7M|S}@O|ixUibs=wHq8*Iortiu*VLp z8U!f$s@L@h^yxy%k(Zap&xGvR?zkaT$xC_M(8OdNO$vYhK%s#mjo4S~xvv?OEK4(y z1{N31Rcs52kAEt&n*-(mT^h0=_${98hb=_#0TzzH)wNR2<9aN_vX;2Vnky-wPD0eT%bsvtqafs5IUu#C#;X?4S}&j?a&ToJk|qG41wOZ zgNh0zK)I`5X+2-TXu)(RD3sVsR)CCA=1uHi1uq5$`}bHG6a4j`Y!-Uvk@S{`1{IHp z@+sp|o5)UQY=?_O-Q(I80PFZJ45%$>s2&eOWXrv@XO~zvW0;1-u96V682(4XmJt@& z!{4E{>+XIJtQkSybq`_g=^H@xF>&W`LnEU?xLCVVkAND8MgI6$`@u5m^Gb1LC5x{I zw>YFXBgzMq=}B-h5F^kTl{Le4qQDrxbk8FBbea_Y(CI5mP??FQbizM^ zw(sR4=sR#Z$T@6HwliTB;xyL%QM$x{h^Hzq0_NyGb0>oDt_B2>INK2O_+}aeer#2U zIt1V5AQE2f`?E-;kafb#w$Mk%puY}EHhwr$g_ZUtgmx$u#BBl$k8JU(wKePJJv=yo z$C9ik@O68DT#yjl-u6mq%Ba#!Jvel^B811~7#}ARqU`lq)_h^?>Soa%RP}&o4_9>| zB%YlsgiJ)?OgpdiHK>CTCBjD9*vUmi=ahiU^@qqHo0^50lL#EqaJQf5ntzlmq>(o&BcU*nQo<`_Q zF!iC52UBFzN7`-t+^?g-6?-_2k)oU=d-Ui&a1?-I%MwFGNeH(n$^0m`#%VbDmZghQ zUefpu)zje9`@)Owm^Af%XF9Q$#nHkdpxVk4Ee7yHL;YB-2hunKx~@l#;wk6RzsK3Y z8_MO2&Q)kY<*HFNVYfwE&3}t*G2vvBo$9*3Ql^;Qk=Cn*K0DZba0y znF@+f2>4)fvOFZuE8`ma@#iO!;NA)*J}dweLc5LF%mYM>#uBg0wHF#QW*iJ>VT(ti zKw;MQ84@$v;!wjv4-CdX)f64G8rs_6EXVWp7`f5oxQNLEAXa(zy1SbY8N@3qNOe~m zfj8#=wD%p}T=wzbk&2{jCCaXhjEInxt%Q&eePv}tWtMgGHOeL@vSaR1zgZ zMn*+u$t=Iub>Gi(et*RCT<1QHb02Yiuj@13@7MZhi|v2c-Hp8z!uLb*S9cNWP!y(W zYSe@0ilOCC%#K(7P)7dbQW4}0ts~V-s`d-PyPT|sH9~HhQ}E=_l&W=Uw1}2t(Co$I zMFVDD`yeOhiRYy0wvAXIe855nMWefCxJbA6$YmPKu#k|AOp}GIBs6*$J=AUpa3J9` zQtb}=?je84dov>$cG^#UeSM!kl}m-4ye+4ivB@RE2jJHy($Q@P)y8|Xg+A1ug!&WL zGTNa}YVKit8r3jA_ghhIdSV97j}ZQF$-N`#iHVf}(M2uqi9&=gUh`PFLy~;5ggDwY z(36C0`%5iCX#hZXVWxp9g|q7Vb-G?xZ?f76J-I`NN->^l<4(@%7DI`(--wgD)_>K@ zcnT}{fzhowVPVa{DEovAdn#OaS2W#0j6%@9eQ2Th&gIkq-l&Bg-Le^*P{Zy>4$KUN ztH1X5S_R7n>;5d)yoreq%wd2p4qby@vM8?!t9lfbH|=}JGT;=9g`r$S<@9Q*NRzVnE+i@-)`qgP^C(tI!%`eMlUf(oZU1$U{D?#Y-U?}ic-Z}Dqe5|QEaW>^r! z1rSccH0!dLm6e9MD)JStB5V{5Z+dLBP_1LKcl<`$l`-5OL7Nl&Wr6M!@^M^1P;cJ+ zh4KRWOmsI1W1H*Uhl3LmI$IO>LFyYZ+(%Za3bLS}xn(|+YyRZnS;o0B4APRr<&eB$ z!Slp`?Du!5ad1QX78M;GFa1}(UX}X0OkhX_r@KYK+7i{k65^N`FF&n7jRXx8?}c^W zb+972VFsF-o9BaMIeMYvP}z(`kC$g3#etznbfH^oee(utK9Rvnv;}u*3}Hew zJ`2BAOn3bsRPvZ?-amT&QPfCUDiXsgF_er(BfPuBVqec0Or-P1g5v@`yzqvK`Gja$ zVDUtcxv7$>{^PmGTW)$zPQvG2|A!2I@^b8|0$TWA`(9IX+rl&-tN}-x91%?g_K&t+ z)+{Yh;6(;7(P0btqc9#fyU_ikLWVPQ9Jmi0?s)0Mygqo4y5@y6=M)xnyiztrp$z>L zSXe(4vdmZ33VZwb)41%}B%d)*A+U!z!UDyVg+-*7%D{f%$NUElT*n7zit`FLQX*Zb zYmJxJ+Zi%f0j&eqCYQmwqgZ7X6)g=7FA?IYZFX?hOiu>@RcrVSS!M9iWFDP_>TSCZ zyP)P9nre!G`E6St1>j|Fe*V5HhNbdxlxU8dXm!BS$$0nD(jAW%91Hqd!}h3rhoW^* zt~sUCFOw!)4juc%zp(><<~Wf8b`Lv{tDpcA(mXmpe4wf8?B6`lb&=#mJWy8hTup8?S56Gul zZ%kt#WsO>i#^JMgV3K|^7>Ii@P{Z4Dm(4f+6G!di&rOvqu@uI6yVIfsXO)H_ zY=`-;L6zV%R9MjdMYOG(lQvotJUT%UeE49z&M^2i58ZK%!uus9c+xkuL;>+a z)jA5q>umGq^PA_h&<&!Nf`$$kr0G-aS1(g7(qSIX`E#3uX)yIaiKDLZQsrSj4p!tv zWy@jVOn1l_SlQSl+wURkpz}T`4iS+_ycz5{<9$2;UZC@`-4Epr3b~t7y!+$W^?bk` z6kJy2MY*}T&m*TMYYna<6qPJ-f9u%5#GdYRj3$^bzh>+i<>mJTH@Mg(!XSlNN7u7+ z;Dqi*?4i)UW5>2I}~8;4n2XkMQe4QfkqD6S7bkyfpwiF83UdPF?tjhA4hld9$03slg*xwO3@Gppqwt&sIVKt>+EFPc(guPLO z6Ts5alE5Y65}*M<-xZh4%gc+pz!j@F#^e3_Ky$&VV6{RkfrPSu`8ZUlV_(1iJZl(i zz%{o(clXs)Xa32(eC1i7R0~7M78EFe8!NQ*TcQkdPBOtUTaH@DedZ#}t#FcIAK_qP zqEdPdGs82_$uJXsjUy-~u!eU0MkSq^oz2^8CE+bi!>1eYckO`f2>6TO*x8qR1A}D4 zXk#%nY!<;lqIwQ>IYQ0?=yyKgA5+M2Hdd&cGJF`;c9F9Vs)!cnd!*^ zOmOWNPXU)#zL*>pMfknL`Qye``oR4IR&D63JS{^O2lWI{C)LA{6%lNJyvk~ka0n7V z!c}wjSu2E3r1Ts%5WyRyH^as6!x2PGZ(G~m)jn)G|K3dyxs`WAGEI<*x+W}1MZZx| z)+7@vyrD1Hm2L6H0t+p-Ra|*yf4==A~ zq=IyxwzjUX^#&``QVrwt8>t{s3LE@bSWDUU6BwU;JX!SQtnn4q8bA@%;NhkdM3 zdsI^Kl_0Zakn7p{DlhLuN{e1WV)OVOnNKx_r2@kq>EM&AV&K@a>1- zC53MgJ5ruM$Df+xHl6>H0~aA13rk4FY0#1YNhMDTe>yD)IOOQjTGPU&pY!uAr)c86 zfJhmZyCq4}oA4c_^P-JhC>IeFB*)n1Hb)+r?;eE-NJY2 z-zkmG0BYj~XYI?9Qc@EV8&X%2C76f&0U9kW`P6K_DG@#kovFkR-bx%bEp(7mb*mVu zHLF(qAV2?ebcLFCFE}}&@brR?6;hqXb370B;SFHV1TTfnIbKibA6Uo!WpJRoJ0ogJ zu)qNUD3)Q~h9>XH1+hbii2B7g0qFL4ouf!GB6vA}T7K9v72_-ny{|F4WivDF3Pv9E z$t}qnsHjbDe8gB_KJ8HgKZ41%(bLDdRFmYcA~;0diE5J~XUD*R6-x8e>Y=0iXcO$| zR##Ro(+>y<2*B@8s1AgLHjh4i=kzI6JV@wZb@|o{(&k{etg^47oJ|;=Y|X(E5%iBC)%S-DwtM{pp2>W}LxtL4sl3wYa(<-bQux=1y1 zo@z($wZ~ne*a&!zu)c^`cb*v-xm>fZ95mP_nVsin3v%OZOBx3*l6 z&Ud}66rIo_prcuyP6Gv{I$d*jb?DAOuTO~VbQP}?V>BDxo&Z&!5K z;MjMwv1#306=2a^udd z+>s+trPSQfc+?u-&-0E48JjEL^D`ZtLA%{wk(a+cY@u`K@6t#tmczJp`_iB_@+*}8 zfopSG^YdKt-dqgSO%&Av&Iu8ZDUo$C#Mcus&3t(%#~wzFSY5Y z9Xx6b-swD>i(aJsO1LEy$8i6@m{LmE23HwWBJ?C8;q zijM$)eX8?=+ypnAP_1LT@e1_Vg&nF@nF1y{MdG_aiuXR7K9KYZ9^a#20)eJtRsyHZ zJsTKrII#T--(Z4P7|f6-x_Ivw7c0Q6?KNj$M@hRDjmA=Po`Ml6dY;!4>ce@;#3J2B zx;d7^|6p~5=VC6!I)FePC$}J3PkX5(SM}B-wd^GnqFsfkAk?a;Vl|iWR7bviadeh} zT^%VL4|m4V=`jp26-AMlwfg=v@P`mnv3f#|{m{a6X5cX>?3bN_quPT|o5vw{JZ_EM#_si*PX~X!nrw zi;Lk~{escdRRK_`gUlvlcju8VP$1OoM@|nuB5iy)9B~1F&%#XX0@xWzr|GYz+;vI0 zntZY)Dn^boyme^fxMDD%&QlUbj~tpxfh)KPZF75;gj+f_TBPxwmY8U_-0oJgO+jTiwUP*{sFv$WqggS=M*8ci_%uJ(n zh0L~91EG%CL#n%KfyWFENAUCV5_xW1!r%b|@NRuqGKQbKh45@@Ay3|ni+hCNWF*{7Pv2kMQxDy`F9}^)^5aKpNpyO) zq`>*@q;1jc%nzv9)|Xx?x6nIV3UTe)rBg0w?}?}7bTLz86YzYb(+nGQ^z@`IW`}eh zkyP2VD3rCisKkJEs|Iislz-tH;~gl**K*$7=|nZ57|=fX?HeS!kqRke2Ujp<0gi&UdX$qJ`$aH9ji;PCl!JfUrm`Yw~|ft z=sHb(6G_j*=DJw5w?1o~NP}%C%In@QFbP-EkE=qOY+*bmBgv9yE~3YGYZhV-D5hxX z5y++qpz=@1m(EZcDUv+rhka;dO6)oBC)~U_IytFV7g*>44QcdG{TA44{1!iZFUQah z>L&1q#tLh1nfgMDxjUqoJ_+=NELeEeNA6o!aSD0@@e_p=V&vw0INE+N=GvjEhP*Gk zq-oWb+}rYAtSc=Gepk`wq+s*|kS()%-ROlc+;e2Zt8}GV2X4(m84St( z>(kD7FR=6Ul{&;C`O@0;i5E8U?ZwqaNY{Xhyl{#PYwU9IUwFCas=wwx8eNl zZV2=N3ZE|HtZ9_mcYvFj87np=+r|?+Az%u_Ql@{(FMc++uy_Vi^Wl2&O6E%7izMB9 zMuqnw^P5xlR5JT&jYv~;!1~PD07e5(R&K}gEOelG#Vg8wc{K4PbRK#Zub~4P9LzkS z9(Z?x1@cR~UGyv<^RToEh>M4AA4ZD2-_JH#awxbLlj7xIi8#9V9TCTUfcZjfKVI1e zgCdATBw23nv)_t7yT7e1dRG2!1!CDWXL#lh7Bcm6M=^M^@CU$f6olII~&woLfP@x8v8w}UZ|vPLJ46NWlz&g-Rc9Z-cb|) zkT19F^;0ndLNJeIx~6?+r#c9-;yQ+X)d-9@M4OLrGR1jHS`TjxV%m!4jata9-UWvH zSy4-n7mw~HJL@yIkiTmO-kF7)1D(^$oH6Jbe0_Xe@KP3~)4idIMt{~n0rimko>QNB z{6usZCcSg9kmHuH-62rwqPtr|QGLNmjMIAHLLl?S=B$SgXTXf-)k9c`t9Id|h535% zZ~g^k&^~8OO@E{52et-LUIDtRzxSBWAaMi@W?@mmm9wkwPbwQG)q75|lVFF`?LK%I z0ef$qX;8YD29YxRA+fb}aERm%RmaW{RO1e-q|`8^dzV&+!cml$2G5}!#d2mTbl+IG zP>O`crh|j@wyo`+`MKLKx05hBRfLXfeetKDMIDu}34a9*BBqlgM-o#f*bV%Abr-j^ z<2$qj%Q4^FFf2K(l!~zFmSOz@aRBZZixSxR1-!8jh4|Bc>iu_0<-e(FP!dTPqwvkZ)0#ZgR zATxc{+=F>pEbA2+1OTAWGhzImIQRjC99{SgpeLFBzjuX1OsFK$p*AUv3JpZhnA2;W z@iVPZF=v7ij(#+pUnbu%FW-rmv!!^@DqIUyUWS@PnnWQJ;gr-|&KSJAcJ0E-Y1C6# zQ-KeH5;Cn%Ms{+W_4xTq@bN&gOMs!6xbU#gUpVxmU^xb1aJqhy)p;j0mJ(!vJV;3R ztWH(R*hqz{Wv4=6i0M#JRB+_o92PI*F~Bz`%o46-5+XoMVpF{Tz$SbQRPxx436T#5 z7}-#jf%((9FOcsDiNEp9H5rkB9jLb?#M%PpgP54@gG^n#aNz*pt*L4L{5*%7Q|zjc z-&C(mQL5xIGaq6EA4)uewhiHhUC}Bsh%!UQ2M|;b82N*_Z^oskTU@&II5zeqN@tTM zppC$Mq$2fHJtGAmwZ^7yupWVz#4qX#-6X^%g`1z%nJ{z>V-5L_10-Sxe~1HY7e9Cq z@oGvY>md@Hf4EN>N|D-_nV3}jT$SwKq@w&PzVZ&B0GO4o<$U>>lG(b;ZW#`BiHYyQ zVmK@IWFOlCZV?v(QK7_qV%4A{Brx9yoJT-hv{BrhG&(kR;*KJmqmdR1C{v>MD&LvO zn(zWzTr>}}<{O(~YY1Px2mQ6I29V|RE4u?rQ?78kn-Drgkv$g9_XXz8-d7b!r7Nz# zAmISe3LI~2Ow9EA{Mh|eHP>Te-XbicFz-0%YOt@7mn>O*yHS?xdT9iBlKrdo)wZs0 zi!8}4n<{O=Wp`Q?;sg#`i~y%S|3{)}QW`H`c;x{Y!^->j-(YRR1>G~iCEQ;s=c{1# z6*6(AfmLeJ#_En9s5;@7f<~*ggh&Jly?w%TRp0avyz7mk?0+vHnNUfm4W59SS%T8f;H_S)fi|hlX(Y0R^&1X}5vfR}@6= zpMCc6)hj1VNQPwOC)t*{8Zo4$8_1X@tf-d;9k1F0Ro$pWH$-6z=soRN;@v0K4)(%k z0)P7pS}iYu1iHcMD_Yp@a14+isKl~Qr#(^Qe|~&^%R^O}AxH^ELlMz{%84SyrNqX{ zihjt*gy|?tUTvGa|B6SzMih<8ORkR$DH_b&-z{YW7rIU()T4|(&UCRuYp*L{^lSrS z6M^%|*x~62T23&>kKo<}H1;<75hG#zKei<>a2?A1+B-H7`mo!7rnRLQz5h1IWJ__I z?{lk%n9IP`F4tf`fy2ORE4qJ=`<;E;)DpCX2(iQCV{rwj=>Yu|9O3bUxJhlhaviWj z@UX3~>n{X{Np~DP@)15t2L}iHVQCity~ZOypm{^Vw-ilvOM#bR6w2=G* zXG)h&2#1(?u5Jk1)+W$2U=v`=>$+Y921ZWK2PtkwyGfH%QxK0@wQm*x&juGeqtH%E zf8A(SrH62;kExuMwsA?5|&=;t#?w z=8TeNkYZPA<^xAaIojzWVTAF#phpFhRmEQE9@B+VB#h2Z^>h+1#GSVyFG#vfqfFduYuF4_={CaGpY2!D?=%5{WxsdZ5LGKP?7*E%r zz8vJb{w=A{*sA?M2J6?cyBV8E-#aNMBI1m-+1Lcyk;z?jE#_)!6f9$S$T%Kir-T$K zi|3W+Pi)Tqetzh67Y5h=o^H#7T+p!na4JCm!GP5%42v$s8?P{rHKTUom>x!}0P_zH zHa3?r8z&~`mseKw)1-A;c1~nI*hX>wbxY#I?7+1dsJA;lPN0TC_Ck7uN{stHH9qK7 zp{zj>+jVAF@l)udpoN?5WUY>h+Sc19emf(B@KT8KDP1rPcMk~o3rhWh^PJMLRCXy6 zkKnB5PjsL-lIi>O^qqu+9Q#vP;PhbmWMC+%H$%H#zdW7}Uz*I0MRWA(c!dc5Q&%T{ zdu}d6@lgW z-$-J4!{^jMe1?a=fYDrCoNnXWIlqRe>R^oxX{vP6wD~5eb_#s603}`Z@tIvX2(1&q zV1rGF25ju^C`Mtc2ixDzCd2pmbF5pN(4mo)iBpZtYKk~@m%8f@L%)(>05@|}%&)+; z-+gXm@ylQ>UTimI1_N&E=GH!?eiqwI&+sXp+tb-NJIE{diRJ+JB2cf#A+CzBWk8pS z6zeK~whzawt(64@HCnzRyaO7~E=W}`Qh|ffXmLC;GE4LP`SYe5K(y|04Mai_AbBHv zO?Tk=^AN*ro#9Q!-xy;qjw!wwLN5bG3VdDz24q#aLHfgoa41<~-z{_k-q8=g+v3@H zYzQzlkdQw*Q%^e>Lb`a-3R<97Raxt+_3JEuvkZ1nWW;iRH^+;!XN^Chj(!fAVMuUr zRoe;L?b{zXXkMqmfl@beQ;iA{?T*}eu}@kbdi>^^IMDgSV_^WC5Z078DAExK6&;OZ zBxo&YckB=s6SLbbX#DfD_aGJ`ToCXiX{=%efcLOy*nSjtEl6(6p?`to;IL~i9P?;# zkS)@UMGj-@Utahbb`{-wPr%;c0&xnr(Z8o8tOGwqHF$fY*QHCO4|J`F{fd~WU%yk4 zY}V(CKZYZTZdZIPFuV)BXR{FB4Ly#+v!k?hwq4cTK{3AcLwOv zwJ+0;QZfy^9%P}A{%!5@W8Bx#vmiv5kf7lKL-kr4##<@`+G`m^Q{pi8KD!3ZIC1&2f2MRo%mYGBbO4wpFv-t6!1 zhravWwAaQmlvQY1u#Kk>BwM_yQbbq?Ty+=we7TeZYOrE$<8v|#VHwMXiNhJFb6GhS zmPX&DSa}rcRdz#opCig?^>OQ5Emx=S^kj5$hDliPbH%6Lxl@Ng0?pTpkVp8#MmnK9 z20bt&f9y;j(UmBBe0v9-A*N~J-U=GS&wE>OCY6GfC&tPIdSH|YiYVz(*+8l#2eIme z@|w+-0%PGMY()HiS_pq_+Wy;Z^yqG&XiN%b9XNwZUQRA9@SBr_q-3@KYDLe<;I~j+ zw`QJ0x!E)4R0KcV+plb5Q87XwG5~@m#>GjXSO7UdN**W9IKv<7qO-W^nE)|v!ytWo z9HwBngZJ!^s$tGpgw7zS_gG@WJ*dmDwLADcljHx89oP+iYhSsZ|S4*ayGenK-EfC0Tl!o zFkFZj%^S>Om3E!@O^cA*<^4mPzO?zgy?wJZ^MPYR$MaEn;qyIsaB;yL501>%m7kxV zlA7A`r5ca!z5d}mJ3!B`)J!S(RG7T13IoltPTO6PgxE9pH`+<;gwBFJJrATh&{nT4 z6tX-MJf8XrZuGzy%zPFFD~9Bhl>G4{nmL#!Hp+%NI599~!;G3Mis{kG1DHP+EiIq> z`8g$jgh$doL-$!uRgYdx6RF(L4PhISJZ5;+Fdj{IGDw- zg&*2zaOwd7i2Y>b&^)2Ot21|I{Hno1wgsuc!ZI>EtorP;rnWY1Y1Rb$?%l6RNur@@ z!#J4g*(T{NqspN`T8oQ{Y`9|7d0FxdaLiRejv|VV6=nDUp)q|abTHKEi=6yMD&j5Z6tPB;W47w4E!ZMP+n9;0i7!(+aV=RIKRCD+*# zprbO_4*CMTRXO9>$VeiEHKdA0 z%~SUjTUalu7%h+kzZg+Mp6?A9kpRwxs``m@J@zBGuiDz%cWm2c+dzber1zbvm(pGu!`oLsCH>&K`a7PoST`t)RX#kC-}6q zTgV3t+I}LN!*FT~5CkyO?dP@e8E{sUnukZ(xu0<+D4?)#<3st$^x;6bW>jq37#MtS z=yf^`autSax~S%UF9;BrSW}M-o#4gGRc;oawso*|W>U8K* zpEYBHzED|^BnE|-=1~T;6A*k3AnD6^^_(Q(fAfmQq6+g)Eu=H&ylJUCru&zl?@+Em z?*>UJl-kgL8vJ>P4VL2Vjq)9oF9;`twmev2ES`CZy{`JJ=_#nQ@UtV8hb1p7KLO<{MsnT{Oy^|-sJv>HD{omd`f>LfwVgL7>S7f_uSM;^h z(&qb5xyD3A89PMbgpLKEA9pe{ z2^H3ri)3+N;P@LiA|kR%h@A*r=3!MG)~-K?Z7I}Qx=eze&sgbpRO3hhglkp~6wSOu zKD!RfONNh{p2@G)UJTLSow^BHqVu7(GP{_p4dl22EV`3m&5QBe**Q2c#WFs-#`OBk zu!9l@K!6X0QoeH8^$cI$@moYB7dS*16agPx3V|Sg9aA6{=Pj(i^g_=T{5fgq&*EUA zCYeCt?BqH_F|ow`DZ-G{S5{QyF*I~`N*zA@8zY)p^nV@7lA-a&+ATcsg4 zwGVU*Ad9bWZavovF&ZBpj>qABUaWzfI0h7m3QSLvHH*E30(r@&F*uvMMW`^6iUHe`1+@iEPuOWMY=Il40Ai!}vT#f_#{~Fp+V09qvinK)K z59`Kk=go?W-$0Gc&CGfnFSbPlKX*U0V@Irb{o0d)0>`;8O(;c3ET^A;xSfR}c@J_R zf!3oPf$RP+0m@qB%jH#8dPlz`!gp)5c?@VUR3GXko_AXZig`tS$x7Swq0ax>;}O?26tpdaOi(O59{B_BDwFRh zfK*lmE4sPG#nZK-KwlAWdcS9QHp)q79Z=tOZa4mH`*l~KmqcX$-o5K+d>?1(@~c*L zpNDW5)7gEbnE}6zrOPx@7@Zk3xL~KC#H;BUJ_e-O_39sexCJza?qc)DGzNgfCZ~4& zosn=-*FYPUh!3mn(8Pjlcr)Wi*~QVq|8%jJQDT3_t6{ z3%;enfQ3`PtgPho%QRQJ>;EpSY^T2txS^-F7u6tf$;(Tb3+s%-mT)H}<@DD~^KvH{ zpxDaF^=Pf>_nh8{jSF+|^vQqFg#B8I1k3^&C8&d{9Nh@m9oTz8vziYb6Su6AfKO#X zd2+4d;T(^v?Bx;cV8IU_9PFxK5qLO-N&)%&27ltAzeWj#3m{$6sZ{1C7;uQgG2R=p z0>1%@%t+3gj*eT%cz~-*OA_>33SLs{j5e!7*4`CffCp+U;?k;$T@^ceQ%rDRxvc%e zb%$?={Me~hSch=|i+1<&KI|(n0ivgbqD4fc7V*Hm>C(fG@}wu0qa5D*wqE6P_@V4%3_sPw0}gXAFI+%o!~Dt7Dly( z=#n=|2j59RW1rs}ZBS^uwc;9YkRqsK&;M=-TndOQd@Tw*e4!IZL5}LdkO6gZk!KXD zSs3B-QN4N@FaY~zc1hvhpdutnR3cVY?!4$0k)Za&%nTLYTVQSIqJ_P9aESK1CG>R2 z>#SLwerB_UdIOfioy6hVIFo$Sv;8>y92XASX4(AjcQ}Dv2uFnz*L?8*{@^Vfnoe9T z^isrG-?-v9Y>C6Yaq)<^;P7JQY;1^*AC4Jx<* literal 0 HcmV?d00001 diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..c45169e --- /dev/null +++ b/src/App.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/api/BoosAccountManagement/index.ts b/src/api/BoosAccountManagement/index.ts new file mode 100644 index 0000000..a747500 --- /dev/null +++ b/src/api/BoosAccountManagement/index.ts @@ -0,0 +1,65 @@ +import request from '@/utils/request' + +/* + * boos账号管理 + * */ + +// 查账号状态表(查所有) +export const ApiAccounts = () => { + return request({ + url: `/api/accounts`, + method: 'get' + }) +} + +// 查询指定电脑的账号状态 +export const ApiAccountsId = (id: string) => { + return request({ + url: `/api/accounts/${id}`, + method: 'get' + }) +} + +// 绑定账号到电脑 +export const ApiAccountsAdd = (data: any) => { + const formData = new FormData() + formData.append('browser_name', data.browser_name) + formData.append('worker_id', data.worker_id) + return request({ + url: `/api/accounts`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// // 编辑申请用印 +// export const BusinessEditApplication = (data: any) => { +// const formData = new FormData() +// formData.append('id', data.id) +// formData.append('Printingpurpose', data.Printingpurpose) +// formData.append('case_id', data.case_id) +// formData.append('Reason', data.Reason) +// formData.append('seal_number', data.seal_number) +// formData.append('seal_type', data.seal_type) +// if (isFile(data.file)) formData.append('file', data.file) +// formData.append('approvers', data.approvers) +// return request({ +// url: `/business/editApplication`, +// method: 'post', +// data: formData, +// headers: { +// 'Content-Type': 'multipart/form-data' +// } +// }) +// } + +// 删除指定账号 +export const ApiAccountsDelete = (id: string) => { + return request({ + url: `/api/accounts/${id}`, + method: 'delete' + }) +} diff --git a/src/api/TaskManagement/index.ts b/src/api/TaskManagement/index.ts new file mode 100644 index 0000000..0e944b1 --- /dev/null +++ b/src/api/TaskManagement/index.ts @@ -0,0 +1,67 @@ +import request from '@/utils/request' + +/* + * 任务管理 + * */ + +// 查询任务列表 +export const ApiTasks = () => { + return request({ + url: `/api/tasks`, + method: 'get' + }) +} + +// 查询指定任务的状态和结果 +export const ApiTasksTaskId = (task_id: string) => { + return request({ + url: `/api/tasks/${task_id}`, + method: 'get' + }) +} + +// 提交新任务 +export const ApiTasksAdd = (data: any) => { + const formData = new FormData() + formData.append('task_type', data.task_type) + formData.append('worker_id', data.worker_id) + formData.append('account_name', data.account_name) + formData.append('params', data.params) + return request({ + url: `/api/tasks`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// // 编辑申请用印 +// export const BusinessEditApplication = (data: any) => { +// const formData = new FormData() +// formData.append('id', data.id) +// formData.append('Printingpurpose', data.Printingpurpose) +// formData.append('case_id', data.case_id) +// formData.append('Reason', data.Reason) +// formData.append('seal_number', data.seal_number) +// formData.append('seal_type', data.seal_type) +// if (isFile(data.file)) formData.append('file', data.file) +// formData.append('approvers', data.approvers) +// return request({ +// url: `/business/editApplication`, +// method: 'post', +// data: formData, +// headers: { +// 'Content-Type': 'multipart/form-data' +// } +// }) +// } +// +// // 删除指定账号 +// export const ApiAccountsDelete = (id: string) => { +// return request({ +// url: `/api/accounts/${id}`, +// method: 'delete' +// }) +// } diff --git a/src/api/ai/index.ts b/src/api/ai/index.ts new file mode 100644 index 0000000..5705c5f --- /dev/null +++ b/src/api/ai/index.ts @@ -0,0 +1,191 @@ +import request from '@/utils/request' + +/** + * AI 命令请求参数 + */ +export interface AiCommandRequest { + /** 用户输入的自然语言命令 */ + command: string + /** 当前页面路由(用于上下文) */ + currentRoute?: string + /** 当前激活的组件名称 */ + currentComponent?: string + /** 额外上下文信息 */ + context?: Record +} + +/** + * 函数调用参数 + */ +export interface FunctionCall { + /** 函数名称 */ + name: string + /** 函数描述 */ + description?: string + /** 参数对象 */ + arguments: Record +} + +/** + * AI 命令解析响应 + */ +export interface AiCommandResponse { + /** 解析日志ID(用于关联执行记录) */ + parseLogId?: string + /** 是否成功解析 */ + success: boolean + /** 解析后的函数调用列表 */ + functionCalls: FunctionCall[] + /** AI 的理解和说明 */ + explanation?: string + /** 置信度 (0-1) */ + confidence?: number + /** 错误信息 */ + error?: string + /** 原始 LLM 响应(用于调试) */ + rawResponse?: string +} + +/** + * AI 命令执行请求 + */ +export interface AiExecuteRequest { + /** 关联的解析日志ID */ + parseLogId?: string + /** 原始命令(用于审计) */ + originalCommand?: string + /** 要执行的函数调用 */ + functionCall: FunctionCall + /** 确认模式:auto=自动执行, manual=需要用户确认 */ + confirmMode?: 'auto' | 'manual' + /** 用户确认标志 */ + userConfirmed?: boolean + /** 幂等性令牌(防止重复执行) */ + idempotencyKey?: string + /** 当前页面路由 */ + currentRoute?: string +} + +/** + * AI 命令执行响应 + */ +export interface AiExecuteResponse { + /** 是否执行成功 */ + success: boolean + /** 执行结果数据 */ + data?: any + /** 执行结果说明 */ + message?: string + /** 影响的记录数 */ + affectedRows?: number + /** 错误信息 */ + error?: string + /** 记录ID(用于追踪) */ + recordId?: string + /** 需要用户确认 */ + requiresConfirmation?: boolean + /** 确认提示信息 */ + confirmationPrompt?: string +} + +export interface AiCommandRecordPageQuery extends PageQuery { + keywords?: string + executeStatus?: string + parseSuccess?: boolean + userId?: number + isDangerous?: boolean + provider?: string + model?: string + functionName?: string + createTime?: [string, string] +} + +export interface AiCommandRecordVO { + id: string + userId: number + username: string + originalCommand: string + provider?: string + model?: string + parseSuccess?: boolean + functionCalls?: string + explanation?: string + confidence?: number + parseErrorMessage?: string + inputTokens?: number + outputTokens?: number + totalTokens?: number + parseTime?: number + functionName?: string + functionArguments?: string + executeStatus?: string + executeResult?: string + executeErrorMessage?: string + affectedRows?: number + isDangerous?: boolean + requiresConfirmation?: boolean + userConfirmed?: boolean + executionTime?: number + ipAddress?: string + userAgent?: string + currentRoute?: string + createTime?: string + updateTime?: string + remark?: string +} + +/** + * AI 命令 API + */ +class AiCommandApi { + /** + * 解析自然语言命令 + * + * @param data 命令请求参数 + * @returns 解析结果 + */ + static parseCommand(data: AiCommandRequest): Promise { + return request({ + url: '/api/v1/ai/command/parse', + method: 'post', + data + }) + } + + /** + * 执行已解析的命令 + * + * @param data 执行请求参数 + * @returns 执行结果数据(成功时返回,失败时抛出异常) + */ + static executeCommand(data: AiExecuteRequest): Promise { + return request({ + url: '/api/v1/ai/command/execute', + method: 'post', + data + }) + } + + /** + * 获取命令记录分页列表 + */ + static getCommandRecordPage(queryParams: AiCommandRecordPageQuery) { + return request>({ + url: '/api/v1/ai/command/records', + method: 'get', + params: queryParams + }) + } + + /** + * 撤销命令执行(如果支持) + */ + static rollbackCommand(recordId: string) { + return request({ + url: `/api/v1/ai/command/rollback/${recordId}`, + method: 'post' + }) + } +} + +export default AiCommandApi diff --git a/src/api/auth-api.ts b/src/api/auth-api.ts new file mode 100644 index 0000000..a0074ac --- /dev/null +++ b/src/api/auth-api.ts @@ -0,0 +1,86 @@ +import request from '@/utils/request' + +const AUTH_BASE_URL = '/api/v1/auth' + +const AuthAPI = { + /** 登录接口*/ + login(data: LoginFormData) { + const formData = new FormData() + formData.append('username', 'admin') + formData.append('password', data.password) + formData.append('captchaKey', data.captchaKey) + formData.append('captchaCode', data.captchaCode) + return request({ + url: `${AUTH_BASE_URL}/login`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + /** 刷新 token 接口*/ + refreshToken(refreshToken: string) { + return request({ + url: `${AUTH_BASE_URL}/refresh-token`, + method: 'post', + params: { refreshToken }, + headers: { + Authorization: 'no-auth' + } + }) + }, + + /** 退出登录接口 */ + logout() { + return request({ + url: `${AUTH_BASE_URL}/logout`, + method: 'delete' + }) + }, + + /** 获取验证码接口*/ + getCaptcha() { + return request({ + url: `${AUTH_BASE_URL}/captcha`, + method: 'get' + }) + } +} + +export default AuthAPI + +/** 登录表单数据 */ +export interface LoginFormData { + /** 用户名 */ + username: string + /** 密码 */ + password: string + /** 验证码缓存key */ + captchaKey: string + /** 验证码 */ + captchaCode: string + /** 记住我 */ + rememberMe: boolean +} + +/** 登录响应 */ +export interface LoginResult { + /** 访问令牌 */ + accessToken: string + /** 刷新令牌 */ + refreshToken: string + /** 令牌类型 */ + tokenType: string + /** 过期时间(秒) */ + expiresIn: number +} + +/** 验证码信息 */ +export interface CaptchaInfo { + /** 验证码缓存key */ + captchaKey: string + /** 验证码图片Base64字符串 */ + captchaBase64: string +} diff --git a/src/api/calibration/accountAdjustmentApplication/index.ts b/src/api/calibration/accountAdjustmentApplication/index.ts new file mode 100644 index 0000000..b1f52b6 --- /dev/null +++ b/src/api/calibration/accountAdjustmentApplication/index.ts @@ -0,0 +1,74 @@ +import request from '@/utils/request' + +/* + * 调账申请 + * */ + +// 新增调账申请 +export const FinanceLoan = (data: any) => { + const formData = new FormData() + formData.append('times', data.times) + formData.append('case_id', data.case_id) + formData.append('amount', data.amount) + formData.append('situation', data.situation) + return request({ + url: `/finance/loan`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑调账申请 +export const FinanceEditAccounts = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + formData.append('case_id', data.case_id) + formData.append('amount', data.amount) + formData.append('situation', data.situation) + return request({ + url: `/finance/editAccounts`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 调账申请展示 +export const FinanceLoandisplay = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('CustomerID', data.CustomerID) + return request({ + url: `/finance/loandisplay`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除调账申请 +export const FinanceDeleteAccounts = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteAccounts`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/announcementManagement/index.ts b/src/api/calibration/announcementManagement/index.ts new file mode 100644 index 0000000..8268885 --- /dev/null +++ b/src/api/calibration/announcementManagement/index.ts @@ -0,0 +1,75 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 公告管理 + * */ + +// 新增公告 +export const BusinessBulletin = (data: any) => { + const formData = new FormData() + formData.append('title', data.title) + formData.append('content', data.content) + formData.append('file', data.file) + formData.append('state', data.state) + return request({ + url: `/business/bulletin`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑公告 +export const BusinessEditBulletin = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('title', data.title) + formData.append('content', data.content) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('state', data.state) + return request({ + url: `/business/editBulletin`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 公告展示 +export const BusinessBulletindetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + // if (data.times && data.times.length) { + // formData.append('times', data.times[0]) + // formData.append('end_time', data.times[1]) + // } + // formData.append('unit', data.unit) + return request({ + url: `/business/bulletindetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除公告 +export const BusinessDeleteBulletin = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteBulletin`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/applicationForSealApproval/index.ts b/src/api/calibration/applicationForSealApproval/index.ts new file mode 100644 index 0000000..af74b0f --- /dev/null +++ b/src/api/calibration/applicationForSealApproval/index.ts @@ -0,0 +1,82 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 申请用印 + * */ + +// 新增申请用印 +export const BusinessApplication = (data: any) => { + const formData = new FormData() + formData.append('Printingpurpose', data.Printingpurpose) + formData.append('case_id', data.case_id) + formData.append('Reason', data.Reason) + formData.append('seal_number', data.seal_number) + formData.append('seal_type', data.seal_type) + formData.append('file', data.file) + formData.append('approvers', data.approvers) + return request({ + url: `/business/application`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑申请用印 +export const BusinessEditApplication = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('Printingpurpose', data.Printingpurpose) + formData.append('case_id', data.case_id) + formData.append('Reason', data.Reason) + formData.append('seal_number', data.seal_number) + formData.append('seal_type', data.seal_type) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('approvers', data.approvers) + return request({ + url: `/business/editApplication`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 申请用印展示 +export const BusinessApplicationdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('seal_type', data.seal_type) + formData.append('CaseNumber', data.CaseNumber) + return request({ + url: `/business/applicationdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除申请用印 +export const BusinessDeleteApplication = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteApplication`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/approval/index.ts b/src/api/calibration/approval/index.ts new file mode 100644 index 0000000..5850a75 --- /dev/null +++ b/src/api/calibration/approval/index.ts @@ -0,0 +1,56 @@ +import request from '@/utils/request' + +/* + * 审批 + * */ + +// 审批接口 +export const UserApprovalProcessing = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('type', data.type) + formData.append('state', data.state) + if (data.rejection_reason) formData.append('rejection_reason', data.rejection_reason) + if (data.settlement_salary) formData.append('settlement_salary', data.settlement_salary) + if (data.allocate) formData.append('allocate', data.allocate) + return request({ + url: `/user/approval_processing`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 查询待办审核状态接口 +export const UserApprovalStatusCheck = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('type', data.type) + return request({ + url: `/user/approval-status-check`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 审批分页接口 +export const UserRoxyexhibition = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('type', data.type) + formData.append('status', data.status) + return request({ + url: `/user/roxyexhibition`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/bidRegistration/index.ts b/src/api/calibration/bidRegistration/index.ts new file mode 100644 index 0000000..58bf77f --- /dev/null +++ b/src/api/calibration/bidRegistration/index.ts @@ -0,0 +1,78 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 投标登记 + * */ + +// 新增投标登记 +export const BusinessBid = (data: any) => { + const formData = new FormData() + formData.append('BiddingUnit', data.BiddingUnit) + formData.append('ProjectName', data.ProjectName) + formData.append('times', data.times) + formData.append('BiddingAnnouncement', data.BiddingAnnouncement) + formData.append('approvers', data.approvers) + return request({ + url: `/business/bid`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑投标登记 +export const BusinessEditBid = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('BiddingUnit', data.BiddingUnit) + formData.append('ProjectName', data.ProjectName) + formData.append('times', data.times) + if (isFile(data.BiddingAnnouncement)) + formData.append('BiddingAnnouncement', data.BiddingAnnouncement) + formData.append('approvers', data.approvers) + return request({ + url: `/business/editBid`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 投标登记展示 +export const BusinessBiddetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('client_username', data.client_username) + return request({ + url: `/business/biddetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除投标登记 +export const BusinessDeleteBid = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteBid`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/caseLabel/index.ts b/src/api/calibration/caseLabel/index.ts new file mode 100644 index 0000000..44f10d2 --- /dev/null +++ b/src/api/calibration/caseLabel/index.ts @@ -0,0 +1,128 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 标签管理 + * */ + +// 新增标签 +export const BusinessCaseTagCreate = (data: any) => { + const formData = new FormData() + formData.append('name', data.name) + formData.append('color', data.color) + formData.append('description', data.description) + return request({ + url: `/business/case-tag-create`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑标签 +export const BusinessCaseTagEdit = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('name', data.name) + formData.append('color', data.color) + formData.append('description', data.description) + return request({ + url: `/business/case-tag-edit`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 标签下拉列表 +export const BusinessCaseTagDropdownList = () => { + return request({ + url: `/business/case-tag-dropdown-list`, + method: 'post', + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 标签列表查询 +export const BusinessCaseTagList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('name', data.name) + return request({ + url: `/business/case-tag-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 标签详情查询 +export const BusinessCaseTagDetail = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/case-tag-detail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除标签 +export const BusinessCaseTagDelete = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/case-tag-delete`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 按标签筛选案件 +export const BusinessCaseListByTag = (data: any) => { + const formData = new FormData() + formData.append('tag_ids', data.tag_ids) + formData.append('page', data.page) + formData.append('per_page', data.per_page) + formData.append('times', data.times) + formData.append('end_time', data.times) + formData.append('type', data.type) + return request({ + url: `/business/case-list-by-tag`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件设置标签接口 +export const BusinessSetCaseTags = (data: any) => { + const formData = new FormData() + formData.append('case_id', data.case_id) + formData.append('tag_ids', data.tag_ids) + return request({ + url: `/business/set-case-tags`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/caseManagement/index.ts b/src/api/calibration/caseManagement/index.ts new file mode 100644 index 0000000..6f07532 --- /dev/null +++ b/src/api/calibration/caseManagement/index.ts @@ -0,0 +1,272 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 案件管理 + * */ + +// 新增案件 +export const BusinessCasemanagement = (data: any) => { + const formData = new FormData() + formData.append('project_id', data.project_id) + formData.append('times', data.times) + if (isFile(data.AgencyContract)) formData.append('AgencyContract', data.AgencyContract) + if (isFile(data.Contractreturn)) formData.append('Contractreturn', data.Contractreturn) + if (isFile(data.Closingapplication)) + formData.append('Closingapplication', data.Closingapplication) + formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/business/casemanagement`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑案件管理信息 +export const BusinessEditCase = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + if (isFile(data.AgencyContract)) formData.append('AgencyContract', data.AgencyContract) + if (isFile(data.Contractreturn)) formData.append('Contractreturn', data.Contractreturn) + if (isFile(data.Closingapplication)) + formData.append('Closingapplication', data.Closingapplication) + formData.append('ChangeRequest', data.ChangeRequest) + formData.append('paymentcollection', data.paymentcollection) + return request({ + url: `/business/editCase`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件分页查询 +export const BusinessCasemanagementdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('type', data.type) + formData.append('contract_no', data.contract_no) + formData.append('client_name', data.client_name) + formData.append('party_name', data.party_name) + if (data.tags && data.tags.length) formData.append('tags', JSON.stringify(data.tags)) + return request({ + url: `/business/casemanagementdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除案件管理 +export const BusinessDeleteCase = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteCase`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件已收款(累加) +export const BusinessAccumulate = (data: any) => { + const formData = new FormData() + formData.append('user_id', data.user_id) + formData.append('paymentcollection', data.paymentcollection) + return request({ + url: `/business/accumulate`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 上传发票 +export const BusinessUploadinvoice = (data: any) => { + const formData = new FormData() + formData.append('user_id', data.user_id) + formData.append('amount', data.amount) + formData.append('file', data.file) + return request({ + url: `/business/uploadinvoice`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 发票详情 +export const BusinessInvoicedetail = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/invoicedetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件日志 +export const BusinessLog = (data: any) => { + const formData = new FormData() + formData.append('content', data.content) + formData.append('file', data.file) + formData.append('case_id', data.case_id) + return request({ + url: `/business/log`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件日志导出Excel +export const BusinessExportCaseLogExcel = (data: any) => { + const formData = new FormData() + formData.append('case_id', data.case_id) + return request({ + url: `/business/export-case-log-excel`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件日志分页查询 +export const BusinessLogdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('case_id', data.case_id) + return request({ + url: `/business/logdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 案件材料更新 +export const BusinessCaseAttachmentUpdate = (data: any) => { + const formData = new FormData() + formData.append('type', data.type) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('case_id', data.case_id) + if (data.Contractreturn) formData.append('Contractreturn', data.Contractreturn) + if (data.approvers) formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/business/case-attachment-update`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 创建案件下拉列表接口 +export const BusinessCaseDropdownList = (data: any) => { + const formData = new FormData() + formData.append('state', data.state) + return request({ + url: `/business/case-dropdown-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 创建申请变更 +export const BusinessCaseChangeRequestCreate = (data: any) => { + const formData = new FormData() + formData.append('case_id', data.case_id) + formData.append('contract_no', data.contract_no) + formData.append('change_item', data.change_item) + formData.append('change_reason', data.change_reason) + formData.append('change_agreement', data.change_agreement) + if (data.approvers) formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/business/case-change-request-create`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 申请变更列表 +export const BusinessCaseChangeRequestList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('case_id', data.case_id) + return request({ + url: `/business/case-change-request-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 申请变更详情 +export const ChangeRequestId = (change_request_id: string) => { + const formData = new FormData() + formData.append('change_request_id', change_request_id) + return request({ + url: `change_request_id`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 利益冲突检索接口 +export const BusinessConflictSearch = (data: any) => { + const formData = new FormData() + formData.append('client_info', data.client_info) + formData.append('party_info', data.party_info) + return request({ + url: `/business/conflict-search`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/conflictOfInterestSearch/index.ts b/src/api/calibration/conflictOfInterestSearch/index.ts new file mode 100644 index 0000000..3594beb --- /dev/null +++ b/src/api/calibration/conflictOfInterestSearch/index.ts @@ -0,0 +1,110 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 立项登记 + * */ + +// 新增立项登记 +export const BusinessProject = (data: any) => { + const formData = new FormData() + formData.append('type', data.type.split(',')[0]) + formData.append('ContractNo', data.ContractNo) + formData.append('times', data.times) + formData.append('responsiblefor', data.responsiblefor) + formData.append('charge', data.charge) + formData.append('contract', data.contract) + formData.append('client_info', data.client_info) + formData.append('party_info', data.party_info) + formData.append('description', data.description) + if (data.approvers) formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/business/project`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑立案登记 +export const BusinessEditproject = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + formData.append('responsiblefor', data.responsiblefor) + formData.append('charge', data.charge) + if (isFile(data.contract)) formData.append('contract', data.contract) + return request({ + url: `/business/editproject`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 立案登记类型查询 +export const BusinessProjectquerytype = (data: any) => { + const formData = new FormData() + formData.append('year', data.year) + formData.append('type', data.type) + return request({ + url: `/business/projectquerytype`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 立项登记展示 +export const BusinessProjectdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('client_username', data.client_username) + return request({ + url: `/business/projectdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除立项接口 +export const BusinessDeleteProject = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteProject`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 获取已通过审核的立项登记列表 +export const BusinessProjectDropdownList = (exclude_has_case: string = 'true') => { + const formData = new FormData() + formData.append('exclude_has_case', exclude_has_case) + return request({ + url: `/business/project-dropdown-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/department/index.ts b/src/api/calibration/department/index.ts new file mode 100644 index 0000000..ed5d053 --- /dev/null +++ b/src/api/calibration/department/index.ts @@ -0,0 +1,63 @@ +import request from '@/utils/request' + +/* + * 部门管理 + * */ + +// 公司部门列表 +export const UserDepartment = (name: string) => { + const formData = new FormData() + formData.append('name', name) + return request({ + url: `/user/department`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 添加部门 +export const UserAddDepartment = (name: string) => { + const formData = new FormData() + formData.append('name', name) + return request({ + url: `/user/add_department`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除部门 +export const UserDeleteDepartment = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/user/delete_department`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 部门分页查询 +export const UserPersonlist = (data: any) => { + const formData = new FormData() + formData.append('per_page', data.pageSize) + formData.append('page', data.pageNum) + formData.append('name', data.name) + return request({ + url: `/user/personlist`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/departureFinancialRegistration/index.ts b/src/api/calibration/departureFinancialRegistration/index.ts new file mode 100644 index 0000000..e87cc01 --- /dev/null +++ b/src/api/calibration/departureFinancialRegistration/index.ts @@ -0,0 +1,72 @@ +import request from '@/utils/request' + +/* + * 离职财务登记 + * */ + +// 新增离职财务登记 +export const FinanceUserDeparture = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('Dateofdeparture', data.Dateofdeparture) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/user-departure`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑离职登记信息 +export const FinanceEditUserDeparture = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('Dateofdeparture', data.Dateofdeparture) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/editUserDeparture`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 查询离职登记列表 +export const FinanceUserDepartureDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('username', data.username) + if (data.state) formData.append('state', data.state) + return request({ + url: `/finance/user-departure-detail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除离职登记并恢复用户状态 +export const FinanceDeleteUserDeparture = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteUserDeparture`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/inventoryRegistration/index.ts b/src/api/calibration/inventoryRegistration/index.ts new file mode 100644 index 0000000..7369881 --- /dev/null +++ b/src/api/calibration/inventoryRegistration/index.ts @@ -0,0 +1,77 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 入库登记 + * */ + +// 新增入库登记 +export const BusinessWarehousing = (data: any) => { + const formData = new FormData() + formData.append('unit', data.unit) + formData.append('mark', data.mark) + formData.append('lawyer', data.lawyer) + formData.append('deadline', data.deadline) + formData.append('contract', data.contract) + return request({ + url: `/business/warehousing`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑入库登记 +export const BusinessEditWarehousing = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('unit', data.unit) + formData.append('mark', data.mark) + formData.append('lawyer', data.lawyer) + formData.append('deadline', data.deadline) + if (isFile(data.contract)) formData.append('contract', data.contract) + return request({ + url: `/business/editWarehousing`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 入库登记 展示 +export const BusinessWarehousingdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('unit', data.unit) + return request({ + url: `/business/warehousingdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除入库登记 +export const BusinessDeleteWarehousing = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteWarehousing`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/invoiceApplication/index.ts b/src/api/calibration/invoiceApplication/index.ts new file mode 100644 index 0000000..9f11c91 --- /dev/null +++ b/src/api/calibration/invoiceApplication/index.ts @@ -0,0 +1,81 @@ +import request from '@/utils/request' + +/* + * 开票申请 + * */ + +// 开发票申请 +export const FinanceIssueInvoice = (data: any) => { + const formData = new FormData() + formData.append('case_id', data.case_id) + formData.append('amount', data.amount) + formData.append('type', data.type) + formData.append('unit', data.unit) + formData.append('number', data.number) + formData.append('address_telephone', data.address_telephone) + formData.append('bank', data.bank) + return request({ + url: `/finance/issue-invoice`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑开票申请 +export const FinanceEditInvoice = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('case_id', data.case_id) + formData.append('amount', data.amount) + formData.append('type', data.type) + formData.append('unit', data.unit) + formData.append('number', data.number) + formData.append('address_telephone', data.address_telephone) + formData.append('bank', data.bank) + return request({ + url: `/finance/editInvoice`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 开票分页查询 +export const FinanceIssueDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('unit', data.unit) + formData.append('ContractNo', data.ContractNo) + return request({ + url: `/finance/issue-Detail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除开票申请 +export const FinanceDeleteInvoice = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteInvoice`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/lawFirmStandardDocuments/index.ts b/src/api/calibration/lawFirmStandardDocuments/index.ts new file mode 100644 index 0000000..943a5de --- /dev/null +++ b/src/api/calibration/lawFirmStandardDocuments/index.ts @@ -0,0 +1,45 @@ +import request from '@/utils/request' + +/* + * 律所标准文件 + * */ + +// 新增律所标准文件 +export const BusinessLawyerflie = (data: any) => { + const formData = new FormData() + // formData.append('title', data.title) + formData.append('file', data.file) + return request({ + url: `/business/Lawyerflie`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 律所标准文件展示 +export const BusinessLawdisplay = (data: any = {}) => { + const formData = new FormData() + if (data.title) formData.append('title', data.title) + return request({ + url: `/business/lawdisplay`, + method: 'post', + data: formData + }) +} + +// 删除律所标准文件 +export const BusinessLwaDetail = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/LwaDetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/lmportantScheduleManagement/index.ts b/src/api/calibration/lmportantScheduleManagement/index.ts new file mode 100644 index 0000000..ab6ce48 --- /dev/null +++ b/src/api/calibration/lmportantScheduleManagement/index.ts @@ -0,0 +1,85 @@ +import request from '@/utils/request' + +/* + * 重要日程提示 + * */ + +// 新增重要日程提示 +export const BusinessSchedule = (data: any) => { + const formData = new FormData() + formData.append('title', data.title) + formData.append('tiems', data.tiems) + formData.append('end_time', data.end_time) + formData.append('remark', data.remark) + return request({ + url: `/business/schedule`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑日程 +export const BusinessEditSchedule = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('title', data.title) + formData.append('tiems', data.tiems) + formData.append('end_time', data.end_time) + formData.append('remark', data.remark) + return request({ + url: `/business/editSchedule`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 日程展示 +export const BusinessScheduleDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.title) formData.append('title', data.title) + return request({ + url: `/business/ScheduleDetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除日程 +export const BusinessDscheduledetail = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/scheduledetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 完成代办 +export const BusinessHandleSchedule = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('state', data.state) + return request({ + url: `/business/handleSchedule`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/login/index.ts b/src/api/calibration/login/index.ts new file mode 100644 index 0000000..99483ed --- /dev/null +++ b/src/api/calibration/login/index.ts @@ -0,0 +1,40 @@ +import request from '@/utils/request' + +/* + * 登录 + * */ + +export const userLogin = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('password', data.password) + return request({ + url: `/api/auth/login`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 用户信息 +export const UserGetInfo = () => { + return request({ + url: `/api/auth/get_info`, + method: 'get' + }) +} + +export const BusinessPropagandaEit = (data: any) => { + const formData = new FormData() + formData.append('content', data.content) + return request({ + url: `/business/PropagandaEit`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/onboardingRegistration/index.ts b/src/api/calibration/onboardingRegistration/index.ts new file mode 100644 index 0000000..3567588 --- /dev/null +++ b/src/api/calibration/onboardingRegistration/index.ts @@ -0,0 +1,54 @@ +import request from '@/utils/request' + +/* + * 入职财务登记 + * */ + +// 入职财务 +export const FinanceUserRegister = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('Dateofjoining', data.Dateofjoining) + formData.append('salary', data.salary) + if (data.approvers) formData.append('approvers', JSON.stringify(data.approvers)) + return request({ + url: `/finance/user-register`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 未入职财务登记查询接口 +export const FinanceUserRegisterDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + return request({ + url: `/finance/user-register-detail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 已入职登记的用户 +export const FinanceRegisteredUserList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('username', '') + formData.append('department', '') + return request({ + url: `/finance/registered-user-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/paymentApplicationForm/index.ts b/src/api/calibration/paymentApplicationForm/index.ts new file mode 100644 index 0000000..5d6e3b4 --- /dev/null +++ b/src/api/calibration/paymentApplicationForm/index.ts @@ -0,0 +1,84 @@ +import request from '@/utils/request' + +/* + * 付款申请单 + * */ + +// 新增付款请单 +export const FinancePaymentRequest = (data: any) => { + const formData = new FormData() + formData.append('payment_reason', data.payment_reason) + formData.append('amount', data.amount) + formData.append('times', data.times) + formData.append('payee_name', data.payee_name) + formData.append('payee_account', data.payee_account) + formData.append('payee_bank', data.payee_bank) + formData.append('payment_description', data.payment_description) + formData.append('approvers', data.approvers) + formData.append('applicant', data.applicant) + return request({ + url: `/finance/payment-request`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑付款申请 +export const FinanceEditPayment = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('payment_reason', data.payment_reason) + formData.append('amount', data.amount) + formData.append('times', data.times) + formData.append('payee_name', data.payee_name) + formData.append('payee_account', data.payee_account) + formData.append('payee_bank', data.payee_bank) + formData.append('payment_description', data.payment_description) + formData.append('approvers', data.approvers) + formData.append('applicant', data.applicant) + return request({ + url: `/finance/editPayment`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 付款申请展示 +export const FinancePaymentDisplay = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('payee', data.payee) + return request({ + url: `/finance/PaymentDisplay`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除付款申请 +export const FinanceDeletePayment = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deletePayment`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/permissionManagement/index.ts b/src/api/calibration/permissionManagement/index.ts new file mode 100644 index 0000000..0b4e22c --- /dev/null +++ b/src/api/calibration/permissionManagement/index.ts @@ -0,0 +1,72 @@ +import request from '@/utils/request' + +/* + * 权限管理 + * */ + +// 新增权限 +export const BusinessAddRermission = (data: any) => { + const formData = new FormData() + formData.append('permission_name', data.permission_name) + formData.append('permission_logo', data.permission_logo) + formData.append('parent', data.parent) + return request({ + url: `/business/addRermission`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑权限 +export const BusinessEditRermission = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('permission_name', data.permission_name) + formData.append('permission_logo', data.permission_logo) + formData.append('parent', data.parent) + return request({ + url: `/business/editRermission`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 权限展示 +export const BusinessDisplayRermission = () => { + // const formData = new FormData() + // formData.append('page', data.pageNum) + // formData.append('per_page', data.pageSize) + // if (data.times && data.times.length) { + // formData.append('times', data.times[0]) + // formData.append('end_time', data.times[1]) + // } + // formData.append('unit', data.unit) + return request({ + url: `/business/displayRermission`, + method: 'post' + // data: formData, + // headers: { + // 'Content-Type': 'multipart/form-data' + // } + }) +} + +// 删除权限 +export const BusinessDeleteRermission = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteRermission`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/personnelManagement/index.ts b/src/api/calibration/personnelManagement/index.ts new file mode 100644 index 0000000..806ef77 --- /dev/null +++ b/src/api/calibration/personnelManagement/index.ts @@ -0,0 +1,125 @@ +import { isFile } from '@/utils/auxiliaryFunction' +import request from '@/utils/request' + +/* + * 人事管理 + * */ + +// 人员分页查询 +export const UserPersonnelList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('username', data.username) + if (data.position) formData.append('position', data.position) + if (data.team) formData.append('team', data.team) + if (data.academic) formData.append('academic', data.academic) + if (data.endtime) formData.append('endtime', data.endtime) + formData.append('department', data.department === '全部' ? '' : data.department) + return request({ + url: `/user/personnel-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 人事管理-人员添加 +export const UserCreateUser = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('account', data.account) + formData.append('password', data.password) + formData.append('nation', data.nation) + formData.append('IdCard', data.IdCard) + formData.append('department', JSON.stringify(data.department)) + formData.append('role', JSON.stringify(data.role)) + formData.append('mobilePhone', data.mobilePhone) + formData.append('position', data.position) + formData.append('team', data.team) + formData.append('Dateofjoining', data.Dateofjoining) + formData.append('Confirmationtime', data.Confirmationtime) + formData.append('Practicingcertificatetime', data.Practicingcertificatetime) + formData.append('AcademicResume', data.AcademicResume) + + formData.append('academic', JSON.stringify(data.academic)) + formData.append('contract', data.contract) + formData.append('ApplicationForm', data.ApplicationForm) + return request({ + url: `/user/create-user`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 人事管理-人员编辑 +export const UserEditorialStaff = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('username', data.username) + formData.append('account', data.account) + if (data.password) formData.append('password', data.password) + formData.append('nation', data.nation) + formData.append('IdCard', data.IdCard) + formData.append('department', data.department) + formData.append('role', JSON.stringify(data.role)) + formData.append('mobilePhone', data.mobilePhone) + formData.append('position', data.position) + formData.append('team', data.team) + formData.append('Dateofjoining', data.Dateofjoining) + formData.append('Confirmationtime', data.Confirmationtime) + formData.append('Practicingcertificatetime', data.Practicingcertificatetime) + formData.append('academic', JSON.stringify(data.academic)) + if (isFile(data.AcademicResume)) formData.append('AcademicResume', data.AcademicResume) + if (isFile(data.contract)) formData.append('contract', data.contract) + if (isFile(data.ApplicationForm)) formData.append('ApplicationForm', data.ApplicationForm) + return request({ + url: `/user/editorial-staff`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 人员列表 +export const UserPersonneldisplay = () => { + return request({ + url: `/user/personneldisplay`, + method: 'get' + }) +} + +// 修改密码 +export const UserChangePassword = (data: any) => { + const formData = new FormData() + formData.append('old_password', data.old_password) + formData.append('new_password', data.new_password) + return request({ + url: `/user/change-password`, + method: 'post', + data: formData + }) +} + +// 未进行入职登记人员查询接口 +export const FinanceUnregisteredUserList = (username: string) => { + const formData = new FormData() + formData.append('page', '1') + formData.append('per_page', '9999') + formData.append('username', username) + return request({ + url: `/finance/unregistered-user-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/preRegistration/index.ts b/src/api/calibration/preRegistration/index.ts new file mode 100644 index 0000000..ae5d532 --- /dev/null +++ b/src/api/calibration/preRegistration/index.ts @@ -0,0 +1,96 @@ +import request from '@/utils/request' + +/* + * 预立案登记 + * */ + +// 新增预立案登记 +export const BusinessRegister = (data: any) => { + const formData = new FormData() + formData.append('times', data.times) + formData.append('client_username', data.client_username) + formData.append('client_card', data.client_card) + formData.append('party_username', data.party_username) + formData.append('party_card', data.party_card) + formData.append('description', data.description) + formData.append('Undertaker', data.Undertaker) + return request({ + url: `/business/register`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑预立案 +export const BusinessEditRegistration = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + formData.append('client_username', data.client_username) + formData.append('client_card', data.client_card) + formData.append('party_username', data.party_username) + formData.append('party_card', data.party_card) + formData.append('description', data.description) + formData.append('Undertaker', data.Undertaker) + return request({ + url: `/business/editRegistration`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 预立案登记展示 +export const BusinessRegisterdetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + if (data.Undertaker) formData.append('Undertaker', data.Undertaker) + return request({ + url: `/business/registerdetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 预立案登记列表接口 +export const BusinessRegistrationlist = () => { + return request({ + url: `/business/registrationlist`, + method: 'post' + }) +} + +// 预立案关联立案列表接口 +export const BusinessPreFilingLinkedCases = () => { + return request({ + url: `/business/preFilingLinkedCases`, + method: 'post' + }) +} + +// 删除预立案登记接口 +export const BusinessDeleteRegistration = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteRegistration`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/registrationPlatform/index.ts b/src/api/calibration/registrationPlatform/index.ts new file mode 100644 index 0000000..339b82e --- /dev/null +++ b/src/api/calibration/registrationPlatform/index.ts @@ -0,0 +1,74 @@ +import request from '@/utils/request' + +/* + * 注册平台登记 + * */ + +// 新增注册平台登记 +export const BusinessPlatformRegistration = (data: any) => { + const formData = new FormData() + formData.append('platform', data.platform) + formData.append('number', data.number) + formData.append('password', data.password) + formData.append('username', data.username) + return request({ + url: `/business/PlatformRegistration`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 注册平台登记编辑 +export const BusinessEditPlatform = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('platform', data.platform) + formData.append('number', data.number) + formData.append('password', data.password) + formData.append('username', data.username) + return request({ + url: `/business/editPlatform`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 注册平台登记展示 +export const BusinessPlatformDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + // if (data.times && data.times.length) { + // formData.append('times', data.times[0]) + // formData.append('end_time', data.times[1]) + // } + // formData.append('unit', data.unit) + return request({ + url: `/business/PlatformDetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 注册平台登记删除 +export const BusinessDeletePlatform = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deletePlatform`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/reimbursement/index.ts b/src/api/calibration/reimbursement/index.ts new file mode 100644 index 0000000..937f5e2 --- /dev/null +++ b/src/api/calibration/reimbursement/index.ts @@ -0,0 +1,78 @@ +import request from '@/utils/request' + +/* + * 报销 + * */ + +// 新增报销 +export const FinanceReimbursement = (data: any) => { + const formData = new FormData() + formData.append('person', data.person) + formData.append('times', data.times) + formData.append('reason', data.reason) + formData.append('amount', data.amount) + formData.append('FeeDescription', data.FeeDescription) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/reimbursement`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑报销申请 +export const FinanceEditReimbursement = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('person', data.person) + formData.append('times', data.times) + formData.append('reason', data.reason) + formData.append('amount', data.amount) + formData.append('FeeDescription', data.FeeDescription) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/editReimbursement`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 报销展示 +export const FinanceReidetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('person', data.person) + return request({ + url: `/finance/reidetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除报销申请 +export const FinanceDeleteReimbursement = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteReimbursement`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/revenueRecognition/index.ts b/src/api/calibration/revenueRecognition/index.ts new file mode 100644 index 0000000..af15927 --- /dev/null +++ b/src/api/calibration/revenueRecognition/index.ts @@ -0,0 +1,76 @@ +import request from '@/utils/request' + +/* + * 收入确认 + * */ + +// 新增收入确认 +export const FinanceConfirm = (data: any) => { + const formData = new FormData() + formData.append('times', data.times) + formData.append('case_id', data.case_id) + formData.append('CustomerID', data.CustomerID) + formData.append('amount', data.amount) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/confirm`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑收入确认 +export const FinanceEditIncome = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('times', data.times) + formData.append('case_id', data.case_id) + formData.append('CustomerID', data.CustomerID) + formData.append('amount', data.amount) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/editIncome`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 收入确认展示 +export const FinanceConfirmdisplay = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('CustomerID', data.CustomerID) + return request({ + url: `/finance/confirmdisplay`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除收入确认 +export const FinanceDeleteIncome = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteIncome`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/roleManagement/index.ts b/src/api/calibration/roleManagement/index.ts new file mode 100644 index 0000000..b4d5481 --- /dev/null +++ b/src/api/calibration/roleManagement/index.ts @@ -0,0 +1,94 @@ +// roleManagement +import request from '@/utils/request' + +/* + * 角色管理 + * */ + +// 添加角色 +export const BusinessAddRole = (data: any) => { + const formData = new FormData() + formData.append('RoleName', data.RoleName) + formData.append('remark', data.remark) + return request({ + url: `/business/addRole`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑角色 +export const BusinessEditRole = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('RoleName', data.RoleName) + formData.append('remark', data.remark) + return request({ + url: `/business/EditRole`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 角色展示 +export const BusinessDisplayRole = (data: any) => { + const formData = new FormData() + formData.append('RoleName', data.RoleName) + return request({ + url: `/business/displayRole`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除角色 +export const BusinessDeleteRole = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/DeleteRole`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 赋予权限 +export const BusinessModifypermissions = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('permissionId', JSON.stringify(data.permissionId)) + return request({ + url: `/business/modifypermissions`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 根据角色查询权限 +export const BusinessGetRolePermissions = (roleId: string) => { + const formData = new FormData() + formData.append('roleId', roleId) + return request({ + url: `/business/getRolePermissions`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/salaryBonusAdjustment/index.ts b/src/api/calibration/salaryBonusAdjustment/index.ts new file mode 100644 index 0000000..c3d4638 --- /dev/null +++ b/src/api/calibration/salaryBonusAdjustment/index.ts @@ -0,0 +1,74 @@ +import request from '@/utils/request' + +/* + * 工资/奖金变更 + * */ + +// 新增工资/奖金变更 +export const FinanceChange = (data: any) => { + const formData = new FormData() + formData.append('username', data.username) + formData.append('type', data.type) + formData.append('Instructions', data.Instructions) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/change`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑工资/奖金变更 +export const FinanceEditBonusChange = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('username', data.username) + formData.append('type', data.type) + formData.append('Instructions', data.Instructions) + formData.append('approvers', data.approvers) + return request({ + url: `/finance/editBonusChange`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 工资/奖金变更展示 +export const FinanceChangeDetail = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + if (data.times && data.times.length) { + formData.append('times', data.times[0]) + formData.append('end_time', data.times[1]) + } + formData.append('username', data.username) + return request({ + url: `/finance/ChangeDetail`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除工资/奖金变更 +export const FinanceDeleteBonusChange = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/finance/deleteBonusChange`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/systemManagement/index.ts b/src/api/calibration/systemManagement/index.ts new file mode 100644 index 0000000..8cac8f5 --- /dev/null +++ b/src/api/calibration/systemManagement/index.ts @@ -0,0 +1,75 @@ +import request from '@/utils/request' +import { isFile } from '@/utils/auxiliaryFunction' + +/* + * 制度管理 + * */ + +// 新增制度 +export const BusinessAddSystem = (data: any) => { + const formData = new FormData() + formData.append('title', data.title) + formData.append('content', data.content) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('state', data.state) + return request({ + url: `/business/addSystem`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编制制度 +export const BusinessEitSystem = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('title', data.title) + formData.append('content', data.content) + if (isFile(data.file)) formData.append('file', data.file) + formData.append('state', data.state) + return request({ + url: `/business/eitSystem`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 制度分页 +export const BusinessSystemList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + // if (data.times && data.times.length) { + // formData.append('times', data.times[0]) + // formData.append('end_time', data.times[1]) + // } + // formData.append('unit', data.unit) + return request({ + url: `/business/SystemList`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除制度 +export const BusinessDeleteSystem = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/business/deleteSystem`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/calibration/teamManagement/index.ts b/src/api/calibration/teamManagement/index.ts new file mode 100644 index 0000000..952d283 --- /dev/null +++ b/src/api/calibration/teamManagement/index.ts @@ -0,0 +1,82 @@ +import request from '@/utils/request' + +/* + * 团队管理 + * */ + +// 添加团队 +export const UserAdd_team = (data: any) => { + const formData = new FormData() + formData.append('name', data.name) + formData.append('team_type', data.team_type) + formData.append('description', data.description) + return request({ + url: `/user/add_team`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 编辑团队 +export const UserEdit_team = (data: any) => { + const formData = new FormData() + formData.append('id', data.id) + formData.append('name', data.name) + formData.append('team_type', data.team_type) + formData.append('description', data.description) + return request({ + url: `/user/edit_team`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 获取团队列表(下拉选项使用) +export const UserTeam = (data: any) => { + const formData = new FormData() + formData.append('name', data.name) + return request({ + url: `/user/team`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 获取团队列表(分页) +export const UserTeamList = (data: any) => { + const formData = new FormData() + formData.append('page', data.pageNum) + formData.append('per_page', data.pageSize) + formData.append('name', data.name) + return request({ + url: `/user/team-list`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 删除团队 +export const UserDelete_team = (id: string) => { + const formData = new FormData() + formData.append('id', id) + return request({ + url: `/user/delete_team`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} diff --git a/src/api/system/config-api.ts b/src/api/system/config-api.ts new file mode 100644 index 0000000..f7da4c8 --- /dev/null +++ b/src/api/system/config-api.ts @@ -0,0 +1,70 @@ +import request from '@/utils/request' + +const CONFIG_BASE_URL = '/api/v1/config' + +const ConfigAPI = { + /** 获取配置分页数据 */ + getPage(queryParams?: ConfigPageQuery) { + return request>({ + url: `${CONFIG_BASE_URL}/page`, + method: 'get', + params: queryParams + }) + }, + /** 获取配置表单数据 */ + getFormData(id: string) { + return request({ + url: `${CONFIG_BASE_URL}/${id}/form`, + method: 'get' + }) + }, + /** 新增配置 */ + create(data: ConfigForm) { + return request({ url: `${CONFIG_BASE_URL}`, method: 'post', data }) + }, + /** 修改配置 */ + update(id: string, data: ConfigForm) { + return request({ url: `${CONFIG_BASE_URL}/${id}`, method: 'put', data }) + }, + /** 删除配置 */ + deleteById(id: string) { + return request({ url: `${CONFIG_BASE_URL}/${id}`, method: 'delete' }) + }, + /** 刷新配置缓存 */ + refreshCache() { + return request({ url: `${CONFIG_BASE_URL}/refresh`, method: 'PUT' }) + } +} + +export default ConfigAPI + +export interface ConfigPageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string +} + +export interface ConfigForm { + /** 主键 */ + id?: string + /** 配置名称 */ + configName?: string + /** 配置键 */ + configKey?: string + /** 配置值 */ + configValue?: string + /** 描述、备注 */ + remark?: string +} + +export interface ConfigPageVO { + /** 主键 */ + id?: string + /** 配置名称 */ + configName?: string + /** 配置键 */ + configKey?: string + /** 配置值 */ + configValue?: string + /** 描述、备注 */ + remark?: string +} diff --git a/src/api/system/dept-api.ts b/src/api/system/dept-api.ts new file mode 100644 index 0000000..537f7b7 --- /dev/null +++ b/src/api/system/dept-api.ts @@ -0,0 +1,75 @@ +import request from '@/utils/request' + +const DEPT_BASE_URL = '/api/v1/dept' + +const DeptAPI = { + /** 获取部门树形列表 */ + getList(queryParams?: DeptQuery) { + return request({ url: `${DEPT_BASE_URL}`, method: 'get', params: queryParams }) + }, + /** 获取部门下拉数据源 */ + getOptions() { + return request({ url: `${DEPT_BASE_URL}/options`, method: 'get' }) + }, + /** 获取部门表单数据 */ + getFormData(id: string) { + return request({ url: `${DEPT_BASE_URL}/${id}/form`, method: 'get' }) + }, + /** 新增部门 */ + create(data: DeptForm) { + return request({ url: `${DEPT_BASE_URL}`, method: 'post', data }) + }, + /** 修改部门 */ + update(id: string, data: DeptForm) { + return request({ url: `${DEPT_BASE_URL}/${id}`, method: 'put', data }) + }, + /** 批量删除部门,多个以英文逗号(,)分割 */ + deleteByIds(ids: string) { + return request({ url: `${DEPT_BASE_URL}/${ids}`, method: 'delete' }) + } +} + +export default DeptAPI + +export interface DeptQuery { + /** 搜索关键字 */ + keywords?: string + /** 状态 */ + status?: number +} + +export interface DeptVO { + /** 子部门 */ + children?: DeptVO[] + /** 创建时间 */ + createTime?: Date + /** 部门ID */ + id?: string + /** 部门名称 */ + name?: string + /** 部门编号 */ + code?: string + /** 父部门ID */ + parentid?: string + /** 排序 */ + sort?: number + /** 状态(1:启用;0:禁用) */ + status?: number + /** 修改时间 */ + updateTime?: Date +} + +export interface DeptForm { + /** 部门ID(新增不填) */ + id?: string + /** 部门名称 */ + name?: string + /** 部门编号 */ + code?: string + /** 父部门ID */ + parentId: string + /** 排序 */ + sort?: number + /** 状态(1:启用;0:禁用) */ + status?: number +} diff --git a/src/api/system/dict-api.ts b/src/api/system/dict-api.ts new file mode 100644 index 0000000..5c3d4dd --- /dev/null +++ b/src/api/system/dict-api.ts @@ -0,0 +1,145 @@ +import request from '@/utils/request' + +const DICT_BASE_URL = '/api/v1/dicts' + +const DictAPI = { + /** 字典分页列表 */ + getPage(queryParams: DictPageQuery) { + return request>({ + url: `${DICT_BASE_URL}/page`, + method: 'get', + params: queryParams + }) + }, + /** 字典列表 */ + getList() { + return request({ url: `${DICT_BASE_URL}`, method: 'get' }) + }, + /** 字典表单数据 */ + getFormData(id: string) { + return request({ url: `${DICT_BASE_URL}/${id}/form`, method: 'get' }) + }, + /** 新增字典 */ + create(data: DictForm) { + return request({ url: `${DICT_BASE_URL}`, method: 'post', data }) + }, + /** 修改字典 */ + update(id: string, data: DictForm) { + return request({ url: `${DICT_BASE_URL}/${id}`, method: 'put', data }) + }, + /** 删除字典 */ + deleteByIds(ids: string) { + return request({ url: `${DICT_BASE_URL}/${ids}`, method: 'delete' }) + }, + + /** 获取字典项分页列表 */ + getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) { + return request>({ + url: `${DICT_BASE_URL}/${dictCode}/items/page`, + method: 'get', + params: queryParams + }) + }, + /** 获取字典项列表 */ + getDictItems(dictCode: string) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items`, + method: 'get' + }) + }, + /** 新增字典项 */ + createDictItem(dictCode: string, data: DictItemForm) { + return request({ url: `${DICT_BASE_URL}/${dictCode}/items`, method: 'post', data }) + }, + /** 获取字典项表单数据 */ + getDictItemFormData(dictCode: string, id: string) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`, + method: 'get' + }) + }, + /** 修改字典项 */ + updateDictItem(dictCode: string, id: string, data: DictItemForm) { + return request({ url: `${DICT_BASE_URL}/${dictCode}/items/${id}`, method: 'put', data }) + }, + /** 删除字典项 */ + deleteDictItems(dictCode: string, ids: string) { + return request({ url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`, method: 'delete' }) + } +} + +export default DictAPI + +export interface DictPageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string + /** 状态(1:启用;0:禁用) */ + status?: number +} +export interface DictPageVO { + /** 字典ID */ + id: string + /** 字典名称 */ + name: string + /** 字典编码 */ + dictCode: string + /** 状态(1:启用;0:禁用) */ + status: number +} +export interface DictForm { + /** 字典ID(新增不填) */ + id?: string + /** 字典名称 */ + name?: string + /** 字典编码 */ + dictCode?: string + /** 状态(1:启用;0:禁用) */ + status?: number + /** 备注 */ + remark?: string +} +export interface DictItemPageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string + /** 字典编码 */ + dictCode?: string +} +export interface DictItemPageVO { + /** 字典项ID */ + id: string + /** 字典编码 */ + dictCode: string + /** 字典项值 */ + value: string + /** 字典项标签 */ + label: string + /** 状态(1:启用;0:禁用) */ + status: number + /** 排序 */ + sort?: number +} +export interface DictItemForm { + /** 字典项ID(新增不填) */ + id?: string + /** 字典编码 */ + dictCode?: string + /** 字典项值 */ + value?: string + /** 字典项标签 */ + label?: string + /** 状态(1:启用;0:禁用) */ + status?: number + /** 排序 */ + sort?: number + /** 标签类型 */ + tagType?: 'success' | 'warning' | 'info' | 'primary' | 'danger' | '' +} +export interface DictItemOption { + /** 值 */ + value: number | string + /** 标签 */ + label: string + /** 标签类型 */ + tagType?: '' | 'success' | 'info' | 'warning' | 'danger' + [key: string]: any +} diff --git a/src/api/system/log-api.ts b/src/api/system/log-api.ts new file mode 100644 index 0000000..9c061ec --- /dev/null +++ b/src/api/system/log-api.ts @@ -0,0 +1,89 @@ +import request from '@/utils/request' + +const LOG_BASE_URL = '/api/v1/logs' + +const LogAPI = { + /** 获取日志分页列表 */ + getPage(queryParams: LogPageQuery) { + return request>({ + url: `${LOG_BASE_URL}/page`, + method: 'get', + params: queryParams + }) + }, + /** 获取访问趋势 */ + getVisitTrend(queryParams: VisitTrendQuery) { + return request({ + url: `${LOG_BASE_URL}/visit-trend`, + method: 'get', + params: queryParams + }) + }, + /** 获取访问统计 */ + getVisitStats() { + return request({ url: `${LOG_BASE_URL}/visit-stats`, method: 'get' }) + } +} + +export default LogAPI + +export interface LogPageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string + /** 操作时间 */ + createTime?: [string, string] +} +export interface LogPageVO { + /** 主键 */ + id: string + /** 日志模块 */ + module: string + /** 日志内容 */ + content: string + /** 请求路径 */ + requestUri: string + /** 请求方法 */ + method: string + /** IP 地址 */ + ip: string + /** 地区 */ + region: string + /** 浏览器 */ + browser: string + /** 终端系统 */ + os: string + /** 执行时间(毫秒) */ + executionTime: number + /** 操作人 */ + operator: string +} +export interface VisitTrendVO { + /** 日期列表 */ + dates: string[] + /** 浏览量(PV) */ + pvList: number[] + /** 访客数(UV) */ + uvList: number[] + /** IP数 */ + ipList: number[] +} +export interface VisitTrendQuery { + /** 开始日期 */ + startDate: string + /** 结束日期 */ + endDate: string +} +export interface VisitStatsVO { + /** 今日访客数(UV) */ + todayUvCount: number + /** 总访客数 */ + totalUvCount: number + /** 访客数同比增长率(相对于昨天同一时间段的增长率) */ + uvGrowthRate: number + /** 今日浏览量(PV) */ + todayPvCount: number + /** 总浏览量 */ + totalPvCount: number + /** 同比增长率(相对于昨天同一时间段的增长率) */ + pvGrowthRate: number +} diff --git a/src/api/system/menu-api.ts b/src/api/system/menu-api.ts new file mode 100644 index 0000000..7eaeb86 --- /dev/null +++ b/src/api/system/menu-api.ts @@ -0,0 +1,135 @@ +import request from '@/utils/request' +const MENU_BASE_URL = '/api/v1/menus' + +const MenuAPI = { + /** 获取当前用户的路由列表 */ + getRoutes() { + return request({ url: `${MENU_BASE_URL}/routes`, method: 'get' }) + }, + /** 获取菜单树形列表 */ + getList(queryParams: MenuQuery) { + return request({ url: `${MENU_BASE_URL}`, method: 'get', params: queryParams }) + }, + /** 获取菜单下拉数据源 */ + getOptions(onlyParent?: boolean) { + return request({ + url: `${MENU_BASE_URL}/options`, + method: 'get', + params: { onlyParent } + }) + }, + /** 获取菜单表单数据 */ + getFormData(id: string) { + return request({ url: `${MENU_BASE_URL}/${id}/form`, method: 'get' }) + }, + /** 新增菜单 */ + create(data: MenuForm) { + return request({ url: `${MENU_BASE_URL}`, method: 'post', data }) + }, + /** 修改菜单 */ + update(id: string, data: MenuForm) { + return request({ url: `${MENU_BASE_URL}/${id}`, method: 'put', data }) + }, + /** 删除菜单 */ + deleteById(id: string) { + return request({ url: `${MENU_BASE_URL}/${id}`, method: 'delete' }) + } +} + +export default MenuAPI + +export interface MenuQuery { + /** 搜索关键字 */ + keywords?: string +} +import type { MenuTypeEnum } from '@/enums/system/menu-enum' +export interface MenuVO { + /** 子菜单 */ + children?: MenuVO[] + /** 组件路径 */ + component?: string + /** ICON */ + icon?: string + /** 菜单ID */ + id?: string + /** 菜单名称 */ + name?: string + /** 父菜单ID */ + parentId?: string + /** 按钮权限标识 */ + perm?: string + /** 跳转路径 */ + redirect?: string + /** 路由名称 */ + routeName?: string + /** 路由相对路径 */ + routePath?: string + /** 菜单排序(数字越小排名越靠前) */ + sort?: number + /** 菜单类型 */ + type?: MenuTypeEnum + /** 是否可见(1:显示;0:隐藏) */ + visible?: number +} +export interface MenuForm { + /** 菜单ID */ + id?: string + /** 父菜单ID */ + parentId?: string + /** 菜单名称 */ + name?: string + /** 是否可见(1-是 0-否) */ + visible: number + /** ICON */ + icon?: string + /** 排序 */ + sort?: number + /** 路由名称 */ + routeName?: string + /** 路由路径 */ + routePath?: string + /** 组件路径 */ + component?: string + /** 跳转路由路径 */ + redirect?: string + /** 菜单类型 */ + type?: MenuTypeEnum + /** 权限标识 */ + perm?: string + /** 【菜单】是否开启页面缓存 */ + keepAlive?: number + /** 【目录】只有一个子路由是否始终显示 */ + alwaysShow?: number + /** 其他参数 */ + params?: KeyValue[] +} +interface KeyValue { + key: string + value: string +} +export interface RouteVO { + /** 子路由列表 */ + children: RouteVO[] + /** 组件路径 */ + component?: string + /** 路由属性 */ + meta?: Meta + /** 路由名称 */ + name?: string + /** 路由路径 */ + path?: string + /** 跳转链接 */ + redirect?: string +} +export interface Meta { + /** 【目录】只有一个子路由是否始终显示 */ + alwaysShow?: boolean + /** 是否隐藏(true-是 false-否) */ + hidden?: boolean + /** ICON */ + icon?: string + /** 【菜单】是否开启页面缓存 */ + keepAlive?: boolean + /** 路由title */ + title?: string +} diff --git a/src/api/system/notice-api.ts b/src/api/system/notice-api.ts new file mode 100644 index 0000000..ee90d0d --- /dev/null +++ b/src/api/system/notice-api.ts @@ -0,0 +1,121 @@ +import request from '@/utils/request' + +const NOTICE_BASE_URL = '/api/v1/notices' + +const NoticeAPI = { + /** 获取通知公告分页数据 */ + getPage(queryParams?: NoticePageQuery) { + return request>({ + url: `${NOTICE_BASE_URL}/page`, + method: 'get', + params: queryParams + }) + }, + /** 获取通知公告表单数据 */ + getFormData(id: string) { + return request({ url: `${NOTICE_BASE_URL}/${id}/form`, method: 'get' }) + }, + /** 添加通知公告 */ + create(data: NoticeForm) { + return request({ url: `${NOTICE_BASE_URL}`, method: 'post', data }) + }, + /** 更新通知公告 */ + update(id: string, data: NoticeForm) { + return request({ url: `${NOTICE_BASE_URL}/${id}`, method: 'put', data }) + }, + /** 批量删除通知公告,多个以英文逗号(,)分割 */ + deleteByIds(ids: string) { + return request({ url: `${NOTICE_BASE_URL}/${ids}`, method: 'delete' }) + }, + /** 发布通知 */ + publish(id: string) { + return request({ url: `${NOTICE_BASE_URL}/${id}/publish`, method: 'put' }) + }, + /** 撤回通知 */ + revoke(id: string) { + return request({ url: `${NOTICE_BASE_URL}/${id}/revoke`, method: 'put' }) + }, + /** 查看通知 */ + getDetail(id: string) { + return request({ url: `${NOTICE_BASE_URL}/${id}/detail`, method: 'get' }) + }, + /** 全部已读 */ + readAll() { + return request({ url: `${NOTICE_BASE_URL}/read-all`, method: 'put' }) + }, + /** 获取我的通知分页列表 */ + getMyNoticePage(queryParams?: NoticePageQuery) { + return request>({ + url: `${NOTICE_BASE_URL}/my-page`, + method: 'get', + params: queryParams + }) + } +} + +export default NoticeAPI + +export interface NoticePageQuery extends PageQuery { + /** 标题 */ + title?: string + /** 发布状态(0:草稿;1:已发布;2:已撤回) */ + publishStatus?: number + /** 是否已读(1:是;0:否) */ + isRead?: number +} +export interface NoticeForm { + /** 通知ID(新增不填) */ + id?: string + /** 标题 */ + title?: string + /** 内容 */ + content?: string + /** 类型 */ + type?: number + /** 优先级/级别 */ + level?: string + /** 目标类型 */ + targetType?: number + /** 目标用户ID(多个以英文逗号(,)分割) */ + targetUserIds?: string +} +export interface NoticePageVO { + /** 通知ID */ + id: string + /** 标题 */ + title?: string + /** 内容 */ + content?: string + /** 类型 */ + type?: number + /** 发布人ID */ + publisherId?: bigint + /** 优先级 */ + priority?: number + /** 目标类型 */ + targetType?: number + /** 发布状态 */ + publishStatus?: number + /** 发布时间 */ + publishTime?: Date + /** 撤回时间 */ + revokeTime?: Date +} +export interface NoticeDetailVO { + /** 通知ID */ + id?: string + /** 标题 */ + title?: string + /** 内容 */ + content?: string + /** 类型 */ + type?: number + /** 发布人名称 */ + publisherName?: string + /** 优先级/级别 */ + level?: string + /** 发布时间 */ + publishTime?: Date + /** 发布状态 */ + publishStatus?: number +} diff --git a/src/api/system/role-api.ts b/src/api/system/role-api.ts new file mode 100644 index 0000000..e76e63b --- /dev/null +++ b/src/api/system/role-api.ts @@ -0,0 +1,79 @@ +import request from '@/utils/request' + +const ROLE_BASE_URL = '/api/v1/roles' + +const RoleAPI = { + /** 获取角色分页数据 */ + getPage(queryParams?: RolePageQuery) { + return request>({ + url: `${ROLE_BASE_URL}/page`, + method: 'get', + params: queryParams + }) + }, + /** 获取角色下拉数据源 */ + getOptions() { + return request({ url: `${ROLE_BASE_URL}/options`, method: 'get' }) + }, + /** 获取角色的菜单ID集合 */ + getRoleMenuIds(roleId: string) { + return request({ url: `${ROLE_BASE_URL}/${roleId}/menuIds`, method: 'get' }) + }, + /** 分配菜单权限 */ + updateRoleMenus(roleId: string, data: number[]) { + return request({ url: `${ROLE_BASE_URL}/${roleId}/menus`, method: 'put', data }) + }, + /** 获取角色表单数据 */ + getFormData(id: string) { + return request({ url: `${ROLE_BASE_URL}/${id}/form`, method: 'get' }) + }, + /** 新增角色 */ + create(data: RoleForm) { + return request({ url: `${ROLE_BASE_URL}`, method: 'post', data }) + }, + /** 更新角色 */ + update(id: string, data: RoleForm) { + return request({ url: `${ROLE_BASE_URL}/${id}`, method: 'put', data }) + }, + /** 批量删除角色,多个以英文逗号(,)分割 */ + deleteByIds(ids: string) { + return request({ url: `${ROLE_BASE_URL}/${ids}`, method: 'delete' }) + } +} + +export default RoleAPI + +export interface RolePageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string +} +export interface RolePageVO { + /** 角色ID */ + id?: string + /** 角色编码 */ + code?: string + /** 角色名称 */ + name?: string + /** 排序 */ + sort?: number + /** 角色状态 */ + status?: number + /** 创建时间 */ + createTime?: Date + /** 修改时间 */ + updateTime?: Date +} +export interface RoleForm { + /** 角色ID */ + id?: string + /** 角色编码 */ + code?: string + /** 数据权限 */ + dataScope?: number + /** 角色名称 */ + name?: string + /** 排序 */ + sort?: number + /** 角色状态(1-正常;0-停用) */ + status?: number +} diff --git a/src/api/system/user-api.ts b/src/api/system/user-api.ts new file mode 100644 index 0000000..2068362 --- /dev/null +++ b/src/api/system/user-api.ts @@ -0,0 +1,384 @@ +import request from '@/utils/request' + +const USER_BASE_URL = '/api/v1/users' + +const UserAPI = { + /** + * 获取当前登录用户信息 + * + * @returns 登录用户昵称、头像信息,包括角色和权限 + */ + getInfo() { + return request({ + url: `${USER_BASE_URL}/me`, + method: 'get' + }) + }, + + /** + * 获取用户分页列表 + * + * @param queryParams 查询参数 + */ + getPage(queryParams: UserPageQuery) { + return request>({ + url: `${USER_BASE_URL}/page`, + method: 'get', + params: queryParams + }) + }, + + /** + * 获取用户表单详情 + * + * @param userId 用户ID + * @returns 用户表单详情 + */ + getFormData(userId: string) { + return request({ + url: `${USER_BASE_URL}/${userId}/form`, + method: 'get' + }) + }, + + /** + * 添加用户 + * + * @param data 用户表单数据 + */ + create(data: UserForm) { + return request({ + url: `${USER_BASE_URL}`, + method: 'post', + data + }) + }, + + /** + * 修改用户 + * + * @param id 用户ID + * @param data 用户表单数据 + */ + update(id: string, data: UserForm) { + return request({ + url: `${USER_BASE_URL}/${id}`, + method: 'put', + data + }) + }, + + /** + * 修改用户密码 + * + * @param id 用户ID + * @param password 新密码 + */ + resetPassword(id: string, password: string) { + return request({ + url: `${USER_BASE_URL}/${id}/password/reset`, + method: 'put', + params: { password } + }) + }, + + /** + * 批量删除用户,多个以英文逗号(,)分割 + * + * @param ids 用户ID字符串,多个以英文逗号(,)分割 + */ + deleteByIds(ids: string | number) { + return request({ + url: `${USER_BASE_URL}/${ids}`, + method: 'delete' + }) + }, + + /** 下载用户导入模板 */ + downloadTemplate() { + return request({ + url: `${USER_BASE_URL}/template`, + method: 'get', + responseType: 'blob' + }) + }, + + /** + * 导出用户 + * + * @param queryParams 查询参数 + */ + export(queryParams: UserPageQuery) { + return request({ + url: `${USER_BASE_URL}/export`, + method: 'get', + params: queryParams, + responseType: 'blob' + }) + }, + + /** + * 导入用户 + * + * @param deptId 部门ID + * @param file 导入文件 + */ + import(deptId: string, file: File) { + const formData = new FormData() + formData.append('file', file) + return request({ + url: `${USER_BASE_URL}/import`, + method: 'post', + params: { deptId }, + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + /** 获取个人中心用户信息 */ + getProfile() { + return request({ + url: `${USER_BASE_URL}/profile`, + method: 'get' + }) + }, + + /** 修改个人中心用户信息 */ + updateProfile(data: UserProfileForm) { + return request({ + url: `${USER_BASE_URL}/profile`, + method: 'put', + data + }) + }, + + /** 修改个人中心用户密码 */ + changePassword(data: PasswordChangeForm) { + return request({ + url: `${USER_BASE_URL}/password`, + method: 'put', + data + }) + }, + + /** 发送短信验证码(绑定或更换手机号)*/ + sendMobileCode(mobile: string) { + return request({ + url: `${USER_BASE_URL}/mobile/code`, + method: 'post', + params: { mobile } + }) + }, + + /** 绑定或更换手机号 */ + bindOrChangeMobile(data: MobileUpdateForm) { + return request({ + url: `${USER_BASE_URL}/mobile`, + method: 'put', + data + }) + }, + + /** 发送邮箱验证码(绑定或更换邮箱)*/ + sendEmailCode(email: string) { + return request({ + url: `${USER_BASE_URL}/email/code`, + method: 'post', + params: { email } + }) + }, + + /** 绑定或更换邮箱 */ + bindOrChangeEmail(data: EmailUpdateForm) { + return request({ + url: `${USER_BASE_URL}/email`, + method: 'put', + data + }) + }, + + /** + * 获取用户下拉列表 + */ + getOptions() { + return request({ + url: `${USER_BASE_URL}/options`, + method: 'get' + }) + } +} + +export default UserAPI + +/** 登录用户信息 */ +export interface UserInfo { + /** 用户ID */ + userId?: string + + /** 用户名 */ + username?: string + + /** 昵称 */ + nickname?: string + + /** 头像URL */ + avatar?: string + + /** 角色 */ + roles: string[] + + /** 权限 */ + perms: string[] +} + +/** + * 用户分页查询对象 + */ +export interface UserPageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string + + /** 用户状态 */ + status?: number + + /** 部门ID */ + deptId?: string + + /** 开始时间 */ + createTime?: [string, string] +} + +/** 用户分页对象 */ +export interface UserPageVO { + /** 用户ID */ + id: string + /** 用户头像URL */ + avatar?: string + /** 创建时间 */ + createTime?: Date + /** 部门名称 */ + deptName?: string + /** 用户邮箱 */ + email?: string + /** 性别 */ + gender?: number + /** 手机号 */ + mobile?: string + /** 用户昵称 */ + nickname?: string + /** 角色名称,多个使用英文逗号(,)分割 */ + roleNames?: string + /** 用户状态(1:启用;0:禁用) */ + status?: number + /** 用户名 */ + username?: string +} + +/** 用户表单类型 */ +export interface UserForm { + /** 用户ID */ + id?: string + /** 用户头像 */ + avatar?: string + /** 部门ID */ + deptId?: string + /** 邮箱 */ + email?: string + /** 性别 */ + gender?: number + /** 手机号 */ + mobile?: string + /** 昵称 */ + nickname?: string + /** 角色ID集合 */ + roleIds?: number[] + /** 用户状态(1:正常;0:禁用) */ + status?: number + /** 用户名 */ + username?: string +} + +/** 个人中心用户信息 */ +export interface UserProfileVO { + /** 用户ID */ + id?: string + + /** 用户名 */ + username?: string + + /** 昵称 */ + nickname?: string + + /** 头像URL */ + avatar?: string + + /** 性别 */ + gender?: number + + /** 手机号 */ + mobile?: string + + /** 邮箱 */ + email?: string + + /** 部门名称 */ + deptName?: string + + /** 角色名称,多个使用英文逗号(,)分割 */ + roleNames?: string + + /** 创建时间 */ + createTime?: Date +} + +/** 个人中心用户信息表单 */ +export interface UserProfileForm { + /** 用户ID */ + id?: string + + /** 用户名 */ + username?: string + + /** 昵称 */ + nickname?: string + + /** 头像URL */ + avatar?: string + + /** 性别 */ + gender?: number + + /** 手机号 */ + mobile?: string + + /** 邮箱 */ + email?: string +} + +/** 修改密码表单 */ +export interface PasswordChangeForm { + /** 原密码 */ + oldPassword?: string + /** 新密码 */ + newPassword?: string + /** 确认新密码 */ + confirmPassword?: string +} + +/** 修改手机表单 */ +export interface MobileUpdateForm { + /** 手机号 */ + mobile?: string + /** 验证码 */ + code?: string +} + +/** 修改邮箱表单 */ +export interface EmailUpdateForm { + /** 邮箱 */ + email?: string + /** 验证码 */ + code?: string +} diff --git a/src/assets/font-icon/Excel.svg b/src/assets/font-icon/Excel.svg new file mode 100644 index 0000000..5fe7a4a --- /dev/null +++ b/src/assets/font-icon/Excel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/PDF.svg b/src/assets/font-icon/PDF.svg new file mode 100644 index 0000000..48f0c5c --- /dev/null +++ b/src/assets/font-icon/PDF.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/Ppt.svg b/src/assets/font-icon/Ppt.svg new file mode 100644 index 0000000..c564d77 --- /dev/null +++ b/src/assets/font-icon/Ppt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/Txt.svg b/src/assets/font-icon/Txt.svg new file mode 100644 index 0000000..667df07 --- /dev/null +++ b/src/assets/font-icon/Txt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/Word.svg b/src/assets/font-icon/Word.svg new file mode 100644 index 0000000..f46a9fc --- /dev/null +++ b/src/assets/font-icon/Word.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/shiping.svg b/src/assets/font-icon/shiping.svg new file mode 100644 index 0000000..2750554 --- /dev/null +++ b/src/assets/font-icon/shiping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/tupian.svg b/src/assets/font-icon/tupian.svg new file mode 100644 index 0000000..05b9a23 --- /dev/null +++ b/src/assets/font-icon/tupian.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/weizhi.svg b/src/assets/font-icon/weizhi.svg new file mode 100644 index 0000000..4f772fc --- /dev/null +++ b/src/assets/font-icon/weizhi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icon/yasuo.svg b/src/assets/font-icon/yasuo.svg new file mode 100644 index 0000000..8c2d7c6 --- /dev/null +++ b/src/assets/font-icon/yasuo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/ai.svg b/src/assets/icons/ai.svg new file mode 100644 index 0000000..c3a1c1a --- /dev/null +++ b/src/assets/icons/ai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/api.svg b/src/assets/icons/api.svg new file mode 100644 index 0000000..0181bdd --- /dev/null +++ b/src/assets/icons/api.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/backtop.svg b/src/assets/icons/backtop.svg new file mode 100644 index 0000000..f8e6aa0 --- /dev/null +++ b/src/assets/icons/backtop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/bell.svg b/src/assets/icons/bell.svg new file mode 100644 index 0000000..262d0ac --- /dev/null +++ b/src/assets/icons/bell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/bilibili.svg b/src/assets/icons/bilibili.svg new file mode 100644 index 0000000..b86747c --- /dev/null +++ b/src/assets/icons/bilibili.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/browser.svg b/src/assets/icons/browser.svg new file mode 100644 index 0000000..15c3927 --- /dev/null +++ b/src/assets/icons/browser.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/captcha.svg b/src/assets/icons/captcha.svg new file mode 100644 index 0000000..8b1da30 --- /dev/null +++ b/src/assets/icons/captcha.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/cascader.svg b/src/assets/icons/cascader.svg new file mode 100644 index 0000000..57209bf --- /dev/null +++ b/src/assets/icons/cascader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/client.svg b/src/assets/icons/client.svg new file mode 100644 index 0000000..7373b3d --- /dev/null +++ b/src/assets/icons/client.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..e99c978 --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close_all.svg b/src/assets/icons/close_all.svg new file mode 100644 index 0000000..2005198 --- /dev/null +++ b/src/assets/icons/close_all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close_left.svg b/src/assets/icons/close_left.svg new file mode 100644 index 0000000..fc5cf71 --- /dev/null +++ b/src/assets/icons/close_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close_other.svg b/src/assets/icons/close_other.svg new file mode 100644 index 0000000..27ffc32 --- /dev/null +++ b/src/assets/icons/close_other.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close_right.svg b/src/assets/icons/close_right.svg new file mode 100644 index 0000000..b96dc1c --- /dev/null +++ b/src/assets/icons/close_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/cnblogs.svg b/src/assets/icons/cnblogs.svg new file mode 100644 index 0000000..4920a4c --- /dev/null +++ b/src/assets/icons/cnblogs.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/code.svg b/src/assets/icons/code.svg new file mode 100644 index 0000000..d8b546c --- /dev/null +++ b/src/assets/icons/code.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/collapse.svg b/src/assets/icons/collapse.svg new file mode 100644 index 0000000..1507568 --- /dev/null +++ b/src/assets/icons/collapse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/csdn.svg b/src/assets/icons/csdn.svg new file mode 100644 index 0000000..e16bad0 --- /dev/null +++ b/src/assets/icons/csdn.svg @@ -0,0 +1,6 @@ + + ic/csdn + + + + diff --git a/src/assets/icons/dict.svg b/src/assets/icons/dict.svg new file mode 100644 index 0000000..db60220 --- /dev/null +++ b/src/assets/icons/dict.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/document.svg b/src/assets/icons/document.svg new file mode 100644 index 0000000..aaa0574 --- /dev/null +++ b/src/assets/icons/document.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/down.svg b/src/assets/icons/down.svg new file mode 100644 index 0000000..5fc8b88 --- /dev/null +++ b/src/assets/icons/down.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 0000000..a8077dc --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/enter.svg b/src/assets/icons/enter.svg new file mode 100644 index 0000000..9e199df --- /dev/null +++ b/src/assets/icons/enter.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/esc.svg b/src/assets/icons/esc.svg new file mode 100644 index 0000000..2f85dd2 --- /dev/null +++ b/src/assets/icons/esc.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/file.svg b/src/assets/icons/file.svg new file mode 100644 index 0000000..fac9bf0 --- /dev/null +++ b/src/assets/icons/file.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/fullscreen-exit.svg b/src/assets/icons/fullscreen-exit.svg new file mode 100644 index 0000000..2452f2b --- /dev/null +++ b/src/assets/icons/fullscreen-exit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/fullscreen.svg b/src/assets/icons/fullscreen.svg new file mode 100644 index 0000000..4b6ee11 --- /dev/null +++ b/src/assets/icons/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/gitcode.svg b/src/assets/icons/gitcode.svg new file mode 100644 index 0000000..7a02760 --- /dev/null +++ b/src/assets/icons/gitcode.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/gitee.svg b/src/assets/icons/gitee.svg new file mode 100644 index 0000000..c799c2f --- /dev/null +++ b/src/assets/icons/gitee.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/github.svg b/src/assets/icons/github.svg new file mode 100644 index 0000000..1adfa4e --- /dev/null +++ b/src/assets/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/homepage.svg b/src/assets/icons/homepage.svg new file mode 100644 index 0000000..1e1feab --- /dev/null +++ b/src/assets/icons/homepage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/java.svg b/src/assets/icons/java.svg new file mode 100644 index 0000000..eaa93db --- /dev/null +++ b/src/assets/icons/java.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/juejin.svg b/src/assets/icons/juejin.svg new file mode 100644 index 0000000..937ace3 --- /dev/null +++ b/src/assets/icons/juejin.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/language.svg b/src/assets/icons/language.svg new file mode 100644 index 0000000..e754062 --- /dev/null +++ b/src/assets/icons/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/menu.svg b/src/assets/icons/menu.svg new file mode 100644 index 0000000..f5875d3 --- /dev/null +++ b/src/assets/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/message.svg b/src/assets/icons/message.svg new file mode 100644 index 0000000..deacdc3 --- /dev/null +++ b/src/assets/icons/message.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/monitor.svg b/src/assets/icons/monitor.svg new file mode 100644 index 0000000..f153b9c --- /dev/null +++ b/src/assets/icons/monitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/project.svg b/src/assets/icons/project.svg new file mode 100644 index 0000000..eaf6a12 --- /dev/null +++ b/src/assets/icons/project.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/qq.svg b/src/assets/icons/qq.svg new file mode 100644 index 0000000..a59086b --- /dev/null +++ b/src/assets/icons/qq.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/refresh.svg b/src/assets/icons/refresh.svg new file mode 100644 index 0000000..e598ed1 --- /dev/null +++ b/src/assets/icons/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/role.svg b/src/assets/icons/role.svg new file mode 100644 index 0000000..5d25278 --- /dev/null +++ b/src/assets/icons/role.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 0000000..2312daf --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/setting.svg b/src/assets/icons/setting.svg new file mode 100644 index 0000000..fbc4945 --- /dev/null +++ b/src/assets/icons/setting.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/size.svg b/src/assets/icons/size.svg new file mode 100644 index 0000000..f92f852 --- /dev/null +++ b/src/assets/icons/size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/system.svg b/src/assets/icons/system.svg new file mode 100644 index 0000000..2e6045b --- /dev/null +++ b/src/assets/icons/system.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/table.svg b/src/assets/icons/table.svg new file mode 100644 index 0000000..1a16abb --- /dev/null +++ b/src/assets/icons/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/todo.svg b/src/assets/icons/todo.svg new file mode 100644 index 0000000..f48e667 --- /dev/null +++ b/src/assets/icons/todo.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/tree.svg b/src/assets/icons/tree.svg new file mode 100644 index 0000000..51aea8f --- /dev/null +++ b/src/assets/icons/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/typescript.svg b/src/assets/icons/typescript.svg new file mode 100644 index 0000000..781d6f8 --- /dev/null +++ b/src/assets/icons/typescript.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/up.svg b/src/assets/icons/up.svg new file mode 100644 index 0000000..3b6c535 --- /dev/null +++ b/src/assets/icons/up.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/user.svg b/src/assets/icons/user.svg new file mode 100644 index 0000000..8e693ec --- /dev/null +++ b/src/assets/icons/user.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/visitor.svg b/src/assets/icons/visitor.svg new file mode 100644 index 0000000..1fd8dbe --- /dev/null +++ b/src/assets/icons/visitor.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/vue.svg b/src/assets/icons/vue.svg new file mode 100644 index 0000000..456f876 --- /dev/null +++ b/src/assets/icons/vue.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/wechat.svg b/src/assets/icons/wechat.svg new file mode 100644 index 0000000..2fc5803 --- /dev/null +++ b/src/assets/icons/wechat.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/xml.svg b/src/assets/icons/xml.svg new file mode 100644 index 0000000..f041213 --- /dev/null +++ b/src/assets/icons/xml.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/401.svg b/src/assets/images/401.svg new file mode 100644 index 0000000..6096d41 --- /dev/null +++ b/src/assets/images/401.svg @@ -0,0 +1,398 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/404.svg b/src/assets/images/404.svg new file mode 100644 index 0000000..b9bf23c --- /dev/null +++ b/src/assets/images/404.svg @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/login-bg.svg b/src/assets/images/login-bg.svg new file mode 100644 index 0000000..70f391f --- /dev/null +++ b/src/assets/images/login-bg.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/login-bg1.svg b/src/assets/images/login-bg1.svg new file mode 100644 index 0000000..a0fbc13 --- /dev/null +++ b/src/assets/images/login-bg1.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..be095c0dbb21604a0cc8c49037bb014f29c07a2b GIT binary patch literal 52081 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
        l-Yc8NP){cWV)CJDlfBRR)I+9^tD|1F zjl7YevByo**{NH?l_Cjra~Eb_WfoRBj9Uy1E7Yh-tT`qV=l`-x^8thchB2egA8{=;dc*e*?EGvmi?;Av8rsyheF|^eVyQ6 zM(ya~j}SG|`$MFu-yt@=j@-uUqcdSsr%7$<=a^$*E{;71)m!_(qt$44pJV^#C~%pmTo)z{L6D1c1xW0 zm!@2HH-8Qd=w5sJsj;7yU3bL(5Ia2Nwx1qtan~WGGq4_Lpt8H$tGbK9CJaun+EVk_ zoT2OJM?ih44%OE?NV}V`|JZGG_D7SDw{vV~n`_}+i<8Io1=ypjNKF15c0P)14Wvv)Fehg>U{o7x|9rOK0|rQJ@5HKiYWyNE-Y?M0#`K5|jG z4D-=7Ml2%2g&QvRPrLE?96j7W?8i}vUfM7g`t*aHsW=obCVy}b5S=~f7aODeAdF)u zV*MmhOZ&l zb7u_oK=2)znp($jz{Gz88rH9>`G(W-34G4lVc|xe37L^HI&Q9~eeEYgbD-ME(ZW@n zItU)fNC&x;%SYk$jU^;pK&1&K{bB}0?B@B7&b2e@oU4EmgO@dPjmE_^Y4oVv&((%I zbFCmDbl?(UAOKuml zdva4@7_-Y;+@&AyEVa1L^YsqYu88ptBkt*!+$Ece!*V+}$0v1yMuMXZ7#U@%NiZ=2fw?3 zo5z89CjAWLX!TlteoMjA$Q_TD4^2{`j2~q^_4_(HeXdgKv`v}0{X{NP{XE}dz0Q@# z^2N9kdz8K_#jr;jI%A{EYCpb6L5W*;2 zto-}8!H%Wx1uCQQk^m!JqDI5bi*7rqyeqxM;7@uo&&P}1%IL0bV=dx`sz<~!brH^h zomN&+QT!LEJvz{@C_FsO@)l{KjeGNvH{+ysKScR7>g_e9x0);lqavq?8f6zFe-BDd z!5#fcV~AteDVW3fad>PHyXNU&paC%n)cL5ap<~gQm__dx+_ozC=zm!Ud=X}^T{w4~ z(zXTVNqs%Bmy+xG0yG%iU=dbC;Qi>q*V6o7RIxt^c=rU>e7B7+C$VI9B$|qk^^Q#C zQJs*6m`x@nBD$f98zV_TwehAMl~&o?hR+HA*WNnkeCQMFd7o>8)%nq! z<(vZwIz+Gb#OVH>0lf3G()lah3Tt70a{5er-{T?7Y_nKQsL%>OiZ3{+CZ}a$3S%=z z=v3^k&jxDQOEA1^BO4UCao-N*ISHXEeyb-`t5tuA#}kf-q^lg`{$6|bV?Dgj{rows< zLDh$6$f=>X5`V7MvNmpXPCnc9nKE_4uns4_iy{oG$1h!MX zpi4M48gwqX1%1b8~gL;|lQ{A`ZBa|PAar6Uw?`}LoO1?iXNIl6? zl<^ua25LP+3gzq;ogqQPJ?UOAn7(kpgs&+{n6>twPB2PFC>5u2WeBP>vj<9hWO}9KfRNt&JSyC4Tufzco7?@^GP5$!0pX`tHJ7HCM0qn zTp1KMLVmepqcvL=;Zq2wBf4Lqh|KzJfhlR75cC8*k(N|}cOQ&L*0cje^w{LNGi()*JcoPSEdktCM+sK~oR*7Y zf+=lZHP}F5Lhgg1EzBY%8tr`J4Nle%(*^1UGJ}0aQYkpNySTW!U<_Q`Z8HlCBV!|q z+#HI0L(=>lu&I86+5UoA{(|~x%_we!ed8%Tiqr4<-ot-LhZ+0G(m&8~9J$Zl*B2xf znNuQ!*eJl0f^s-prC+b`bXR1Y_Z7TYi~Ak$ryFA)E{#Z<(_33NR~y~8SiR@w-PY-% z89+%7A^ys!u|KuHX~qvLF*h`QTJJn)adogiQXAuSfES1G_=!iG0MP**yu#OZXEmIy zwb`E%bgq5uFFiN*Pj=()5DOR68g5Pd$5xc+-@~9|y~~q#J{!|x={xnmP7G#LDRg(T z{OF-IZGEtdy;FpLP{cK-O30Hjd8A2~)_9Vr;quhr*-gPpJKiG~aT&OdB!NQ;<&POW z0!|1;+20%(N^<1x6I;K-RQN@#Pdn=MO>VtXtUtAL1rDgsymud|8TS7c+`W=*%*`{3 z-ae6fGa8a`bd9trF!t42VR&(?tBgm)XZ}Ji&3_xA)sH% z$tNW-n4#J#wb$^P?y=_L>W#4e@z(BS#X^l`$E)E%K1UnD8v7RJ>1HZrEWhoK7s=X6 z@iF*I*XUst_wNY<7<(l9qRqV0I7(lYQJOJ1U7*8mAJ4a%}N`b2-2@(aWoyha`mP(|w~eiR*AlFi;< zP501Lr4!0#vgwHk!7<9slI$GwvAHdfN^ys=s4-PqyBxiFpg6e3c_UlCzjf&L9)294 ze?-POIU;*`ci(3M3?)0I!;m?vsvLeA9LP=2wS+4J=%te~Iw}ei>xbH6A%5V?FA(ev z3(2_rr8fVU-JD_V`QfOR@;v$>z~5jf4&U@Som5UjIyM-8h%(G zf#%$aXZ-$Lp#Si)CmL=xvF6m7oiAz3@c?GGS?pq^J5pE$H_1~6YM0tc%EzXMKTd1g zlVzGkr=@Y(6reU$@KP;f&wg2zqlAB)${_y5Gl=@8#eQ_&;r`tpb1zOVtvd_nk%urY z)lhOYf4BI@75`&{azRb5$FFiIM;9JB*Ica3K+E@NV#NoVgD+iBy?sgM5DDhCxW>auIs zlNZda-vH0Co6C9=#Ysg2=Nv_??0cPf_*2EUnRtZ(l_7%iq%!D9=Mp^a#pFI}>L()0 zc>zF!fqWVn|4WAekGJW3zJ=9be~%%U&XDXQ*9S)@b(a&bf-+qPJmi0c(<-{6X)FPA z@KjZoi<7CfSj}ht@)1>BjLDo%rFdQI2QQegiqOxW0(?qRxjutr(xg2!G4uH}7=?ve zth>N0Hjw)nl&Bc=xA$7KR~>3Q-#;{y@fo%{l=F9@HzZwZE$i*mU+lax1B_o=VoSJc zlx!O~@0JR!)F%~z6Pd6pq^TK_A>YXE_ZfxyYjxZ)aYUOei^y5?BX&MD6&vX=8i?`}t=! z$q5_OZiP8YtE?7tN<=!9$H5eO;cG9JqzpAavhkWth8M9=6E1Q`iK?qmhJqv4X#y~! zlmw);rv}Wvc)ZE<`ze*eoQZR!Hw#o)RG^BJZzrOqmofnakYSpW5#m%>Bg!Yl>I!?jNVi0P1v=gHTPt%}%A0szJ%**?}*;@2sK;0=?boRlbYmBlV=VXEUnke+`YWkf; zyX&S!9{yXa!^1CtfBB~cP$AJlk3bmr2eC@O;s;U!rXo7@wBq-djurzAZ>a5=nG|%I z^%q6nP*zm|_#Bj96!2mawD2Z|-yT;QsHDP2W+*)QYrC4cX3^>UQ}6xC#ogCCcXeN0 zzWMDlIk;~|sy6^Xp4Hv}NFB&&GvFyWpxmKEcw*)3>O-5p?#K7 zJEhzIF$%0br7ah`y_q*MiP`x^@+LZs*J$xY90zd3R3071Pi+)6(t1v2dbc;VKBlpY z6^?b`;cQ`jFNhi`Lu;YjrfBg88UlSr6VYF~mviFfK#@R+k1VHnCh7t&MZ?#U6DoceRzmcfaVXHSZ;Z!f@5-?Q~@l`!BAOiWgsMU!1a7 zIp&x8)f;KWpDVt5s-A%r2c!Th;{sxQ`9|g4Gx_3#Jpo9kp@zU!Irz`=E{n++;RN3# zx!pG2pH#>xfexe9rd~Yzl+Q;dOJ0R?ot=Eu$~Qdx@^tnA#2CV_FWqlDY!r#%6v4JD z5Y2z#uc6mE?Lx9_)*pfZgCwjU{78xV)9^hDfVpL$D}|ZNu?mSi7{ z(xR)o{CO3ZpD!pDFt2RNvis__8v7q7OEa--OL4V@<0KC=rLTgVb{l`H8`oubD;U^k zDSBgK3}u#%UN|~dZ9S0IO+IcK2gERBG4MPWXyiw4k~~%ql9?UF7j` zS5}guqAWc!SSSYU7HF@q{qqj{dONLiE$=HfL5g}c6_zcpB4_C8pti|nJa|{&0sBQ7 z&H-PwZy?fAfScme>*u5H*$w6P`b%_`+YZAWr%G|-Ux~38iegPl-pvSv6r7z}D`TmG z>&|&uiyB?W^A~sBnH$Dk6>Cw9)T>4wRW?L72UqJ7n>H`VQ7pO}0?Qai#({5P0180d zPYXKplWvXUh3WF=$BuW;ci!Ebo;7FPqx&Y2Swi5m_HvKTls0nc43G$Qx{tdo_G_bv zeb*fr?$~~wOT)`YyqkjAr#H>xpzFlLm5J;-CfnK@Z4oFj$5aFMMa~N-c(Ven0g04t zA<2ND!dRGE7ArsW;}dn-tLZ-0VIt6&Qm^(utQRYNy;R5R+HR*+hJyUc(S|!ia}Cr$ z1v>IW#xz_q+52;_{$a=$fEoC_-A$+gf*U)|2QxrFQAJw4>&S~zIPGrw;>=3@>+nTK zjsaq0*M*g*&evnxlDF30>)|Kw0YE;7TIU_0@|V;4SF% zn)$}Ly7?F6I#@7*HHvLmxzYg70wqTa5pYhZPZb!GU5bTGHiI)DQOi-Ih!D@emvaZH znD)mgh<-%8XXKYSJSKeUs^=beqelnb&D}>E4^`e)Qb7%DVY?b!6V;{G+Czo*xK^~> zuF8f0B=3AXH2Txia}NX0C{W_7RG`sy^x65L>2L$=Bhf#970D|hYqDvyCx90g$dofJ zhr9Gp87Ay$fJM!ff6%|5c@&%Y?jNI5V%#T2*PT zmXB~U&YEkon$^zI7fvwIVCVG~uvRm>OQpPfSo{c)0hv8jRd%hql)`q*YMH-{UoSJd z^Y^!C9c}UH0pp-ez?llMvM+#pegKo5(m{H@g zJUXSUR2{p32JBU!ux`VH%}kMM|JZ0|L$+7-CJHO4B8?XbJibpUs$)M)-@>!>`DOH+ zJ;-v2X@X1xx=1R1w6DbItGlN4Xr0ZU0|mbXX(2FpeKN6(g(V}cGF$O$MnLia#K0BD z>+m(|_z#nrf!a%apuR9%#DjKt`gp-eoaQ=vIcO2FJ)P>R{v~FOE;`GVkYD4ixmW=r zdS1Ye!2b-ufdbX|Xit|a|L1JBH;6C$?|MxKak)JwS0 zEH;M^zdcI*EQ0A*ab9$elD!4vR1_+KFiQj*#4cmHScSDmC%NurI#8PJ!WA)RI%&TI z3f?2PX1&EXPL--@8S>7Z`%p%PHw-mMuioOe7>2JU_t3zTyaEA`M*W3nass(qIP9t~ zUy9A>=+v!|y)0;c9q|{re?3E|+Nj4=OnLxc9m-~#Iy~Q-Tb$+-Aum4+{>Jxu@zVV^ z+;z54xsNV=95COglsAhOrCNU$8Y*D-=&l1PKZO;yMK_ru+9EUa5aYrHzgBBoW>Lr< zH8k2PKNQT!3qMrC^6-Xmuv+#JE)o|s`$7AWBv`k621_W%_4uLnpjImsO-&UxCaRwA z{3%yn1s{fw94#R!2}IPE+37!Q9{i!HZP5d!y=F3M+(PNDoMfcTh>X`Md0~g(P(&uD z5#U*CvPFYJ=!etkA+w%Ef_zGs%9=$NIP zC1$!5j6ngE|0joml?g%0KD&gr{f)r3;YW&8vQ~kH$gh{q#fp_odvyGM#t}yi=1Z@O z8#CHE_ngw<=Q$(hT!q%p+P7@DhvrE`X#xf96o$~-Dni@TPa79$8gob02&B=Y%ME+? zIrNgy9y201R&0`Z^U%TBtME>rJ^DRo2ky3!Hl4DBUL&JpPbv`C){vmDQT3&-VY`+6 zK{&fO7|!Ptsmxc0wy8Ly+Cfr31|B%6XtE;GPd4UCva&N2VesBF!#xA~i+-p{DY(@R z#e)rqkrfgobGi=}H32Tgqn)8g)V;iYoODAVDlry zkm!$G1_PXG5`S~MvFj8qT$E^6c5=`c0c%l^ z7~m?o+^gCW8H*dQ6_$rk4)868wVh6F-gI@g1&1p~=-O@@@y~3MwF3%9jh9}Cji62< z`!o)^(lizm`wt(3u#TxK{vOt~u^wIH5Au^#1|Ftlw%E5kJla#V$3aWGL=_T=+<#u* zG2!PRisfZ%jIDK#egA`Cb#R@cR3Y6P$HdCrw3EL!pEpsg@x_V0%96v?qNi$6KM6Ob z_7)?oqE%*3`DAizIBluUl%Lg4OTTT;wBjV$VArQBZj+ z_e}7NI>oQIs8Fxuz5R>RO8=U;_fV(`>uE#=kV)yLPq`rAi4n!4HY0z7=cD*kj`hQ@ zV{`^b^aYE3^PiOUwe|*Bjfu82tRZ&5j68Yl4IiXURa@~)HKRJ;&Skq5$yOe&rsO?& z)3EZyxXymzNv^U9Ke#famj8i=S!BEjyBv@n(EIwB$*ceOD^k7QtXfVDU(L&+7zHY2 z^832#Xnx|lTHy-WnUQr5F5syzdD15_h;X);*44cw=HzL zzd3PrSn@b}0FDZn^BW-iKx2|6f77;v%hLBPb|EidgQLyd4E`}9=^TfC_zLMB_0|fL zlC9!(owtnd+n5frF;3+%{uzh17gtoo?`fF6IAQyd_H*dP1-;6b6F;rCsHi|&TmUvS z9K=(xm@1zmJbTrGf&}2Tz4hCLb}nBhMY8J82T6S!gFwgkry=U8!t_BM&*-X*Kq|X9 zvDYHe>rx?HLhbr{Vfkcb@U!O>Zw9OtkgYJ~hFDM0SKdwXmhqNb3uLTwh<9l>lK0}I zQcN!MQILtu&m=mLbjsJNY&29;(RyfvL7dKMAQAGED)P8$$ zG#9*9*Mk!`+>e^B`W_rfPaSVKc=W+8?s~kzz5~_QHmScG-h{7xT%OLfaM8_|zuDET z1eG0oQdk8qykYp z@`r8n-px1HS%ti>n(-(UIw}b3EIpix*RqH(2u0Zi0%-9x{of{{%YSbM`16f&<_BE| zl!tybMH+*RUP;7qxhPbs?e9z2-|7ZQZL(D7m-EFpXx_V?eqRPaDm*NCdamCso!Rj_ z%g_}qX*KB(Hm+Z+y)IE>jf_tI_`mu<7@@JghTICDtiMXFe(6%sC|)sLPsas$0eh;Pr<`2sahYaIUu8@#K=ptKk7ka zoq+e~hIHi2?yvU72S6I$vsapTD}R-6o%)7en8N~E2$zbZsT$ssiZP;m#(0NI!1Sr* zsESE>9vi}MfiqzwqA?7A_X=^ZKK8SDQ8pum7Mki2ufgIAI1y(-LHFSET3N;IZE^26 zu2I|DrY@#>SMvic*7?{)I{WqIHeK6<8n}4dCsV(H7C|+2){~@RW!L38^;KBk^y|S) zc@<*AO3qurnCHX-n(g=W`?_y%V82H_WIlfQu%#XB>)hgojv-J`p-i@V7u91F$-8&1 zW?HljD`MTA^XW8`oQvzR3wcT#E;cD-<@cd<_ydQ;h5m2>f#N*-mKptre2?Lq`Vnj_ ztBeZysc_6})7UtqdZb_YE~kA$Q?&e0gIBB?!x>MhY$P$jv)@Z=u#pMQG6G?dwS4MMr zUV{E3gQ$(~1n2ZUyp%dLSDOG`x^1+rQgO1v27=KbbzJ^OMS%dwtDTvEb?H~u6^DY` zhO89D3e-jyerq<1px)`3D5vtV9(HtRZujJ+_Pj3cmS#Ww{sO@4&L#NLO~vP^>F8L0 z4>pBN4)2HU(Iuwa)9wA>6E>o3 zOA2B#TQQrSspgm!R+4s@q=}2H1-m0b*lO8}wy`m0x0y;#S{?^rk+()ambre2s$bJV zEm?Op8$X20%8K%v(VTGJBYO5 z#J|eXw3G=E@*)ze+2)0_{V^vStw}oQtLLLxb`KFf2{u)B+Oub&_OU)B%m*6wkc5M= zJRMM=b@WSO>`acD>yfSSbRXRyn)NGOQzL(MJ^8(BN(D5##_Ca1>XHdIcXEYVuGrS{ z3we{7rL2L4>)GWR;x5IimW{*%(oF#`TgBwKb3Y;XVqr|I4jWb8vXenBx-c6giOlb$hWHDe!;X0sKzy{V_JKRS6FpJ%I53e{!X>k^<*U%uC zR14G}#~-I?@4K@uwurNoiyM89+GER~)3nwdmh<_yBM#>Xos*-6ql2gWK{3frD;ipJ z70hnBA0Y9m)gIGJ+Q5*(zm*a^QA>OqLrYmj$_`m9~P^&ff)2;T$J-QC)Y*LKxB4#1O@DBJUnkB?1$x2%R%P&RlYi@|c z`ef9wwS@up5HgL?bFeA$*0xNYg@J;G7!0vxs#}r@d-LV}@~*b!J9Rssb)UyB1NV!0 zmg-#j_^$N8*MT9ySq-*(y|>LW#f_-wq0w^O%HlQ;sOjR~LHTIUZKSJop@k&&cnD?8 z*IMk(H!uAZN|9~^E(;*h$FXE$I=5@xkKi)HA8W-C&)UPGL2<3&%jZ?Bd!G+L#Z zsU6x_UhV+qobg1WqaRC0PW*hmQw)cVNOVRG?OT2_<_dD8P}{1`D+CMHS7B!61m>EC zgpJ!yoj15)eg7d{bEZfY1K2tT9L`6-srLW8C&yV8fss(V<*d$14B!aYEWaG+K6Wik zxNh6kp0=*L9WBybYAc5N!F94}Eb88{9zoYAx#gwyUi$Xp$W;%CwUQHdwsN3U5%71f|s z0Tiq^qhLS#^*p9rjrk+z2W5^!H%RRc+G3J&iyh&7#=oV|okOan~31KMW28c&Jc>&_oSn zKC-i4jo`BRTgsW9M>xD@s%MFX%PMyTzN$M|h^fI=C~$#x)eLgUaPE&?^+f`Ox8s6+Z{ksgT|8pDsFNT?N3@`83m0n6%MQz@ZmwIVEyVi zOJ%SJQd$ltRy?j3=&hgK6K34b55YV~iqgAlG&%v{o!g{Lan{wZi(N-@tE=S3C=&h$t|doCmB==b1WC&J`$)Nq zPW>+6+9AM%cYF``{Z({1O(;NJf{L2lut&{DlEd`+V)3sTo2Yl-BaGx z*_EM%J-%vY@oe;tS8zgH1b`Dm2&qy{h8e%TV)uSe%Tu}BagZka!mW`wGLY!`j8({$ zhVFna#kEdO#d!#{UwIGSp?^eJIkZtiH2*NURmLk_aEk4{ zY#()*JqJ0aElG)#ZHr6MK|b43l%*=AI4|TFker<&?tAQ0((H9EyxMoYB3hk|+k>O0&Gv^Ia@OJ^H}dOtT;j2msbR%%vShtXRHF?!1G*VbGa z9&5>L8;DQUAi9FWb@|M_{V6=pf!s0M3;vFC3bQYHbO!C~S{;Br0owe#B^)zs6{w2V zu<$v(d%ug?MJDcd2pjE;0Ugh$%py&z1C1hG@Ao?OPU9LbhpnVKmkHG-pF+=pL_n(P z)P?5~F|GUX1Fx&cdec_K8H$oMZ>_{KCfMH%zL)G)YF3mRxub4B4|`Z0Zx4Z9 zxTC)X0nUQMmQ|ktBU+=taM2I44&>Td6{+^5KcrEYy@Gd{+{b}{Y?M;R_ihPlvD-4! zCA-F>(LG1f$Y@J$j&iS~hKi-H4|Vd*)a)9dS$)QTdTtLqunA2Ozm$T$499vi0N>wJ ze4_30xON)ed8H1@wWhR|{wFgk#dbh>*oE{0neRRO8Lh*PEZmmaa?>S8es}D*S1@81 zIE@>9e`aA!zP?VizcU(48n)4cV9;ESk;!C6-c%|ir{Z52Q2UIFy*A~r=!BOPBYgOE zR(MD9#JBl0G8Cns)l>VFGrzzmlb35U+9+IGb0HC@QT2$QOOYftILimL$UvH8xOYS% z*}~L6!e90y`@ri6A}H;(Q^tG+#NAIf4rm2_y#=_4Q)#m-JfDvS(V5`YoO7m9JSQ?-0AJ5TPqN3(EAD);x|W;#3)h1RDaEGh}6Y%$KNcuN{VV?3zoZ-uOeQ zf$lCrqJ5sdD*F_*VCLjFz_#VKCO+;%qGGeKVW|0PR%<>z7Z!TD(mn!kj_SSp5A)KF z{Glwfvu03L?-}Q{t|I73b(D2H-Aq0Uw;fQQd(-Vs?-3~i)tmc=3t=~Yf;5i9Bqr{dtSy3pkOcZEmwuo1Go^c zkHXhie8?8vTO0gv4OOKU+OvMnZK2`HPxteD-E?jjM`NZd6U6)N_9s*0t-~HVE+=GT zUkgG66qvBApfCW6x*xi`d|GqYZ!mMa&72aCw085YlEM)bg%0=$v8lW^7drsM0-~s6 zp{uSR8k5z(?)$i=VU+vYO^yg}UTW7}oaa7K_#vMk8Xh?was2Tk^0ZSe{ri4}JGq#~ zJtb%`W#&f^>3x0hc1Pt?bN83DQfE+pk_hpTK=Y6Yc|E@oYM!VXh^-2Zo$z@Ikrv;q z>>UoVPPTgD)beqqxAnn1Mm)@E!Hw6vy0%=42 zN*I63z|WuOuL+R~qxmRmOoG1Ne?r72V*WtEB-&a2#ZS5;pj>P4-Lg}A%iB>qB@~)_ z-8YPdfVw*1@s3!lv&YJd$CT7mq0w%LwbSxc0Mg^`U{^^WX(Z%xK%|IdGsPl_@{jVP z#b*OJ1W5c3@5b4t!pf&x+ztYp2YYVYVOZ;FGaaWcCoI7xI_kw=Gf>UI>A8^qvdC?-$-RLn&7EmpL;0(s{Ib#)*&hkPbbOVX^TH?fudll{5GRhlpkU>baI%m;?>E^A!uUm-bZHalS!6XAt{i zN5gj?FlMFCpc>$pj4gk*dNUp>_sbWy`p77t?a{%AN$Ki3J@HOT-Sbv157=q{)DfgX zu}S?5nj=8EP?DqA@Rco0`)6Ei|ieL4mG^d0I}N(SaeF; za~x>2q@|Gnnymn$Xsy8wC1|RddwF?_HBspbBM`P4ABOKwgw|b4$G%jr8HaY>+Fmz+ z!{$;N*#_O-T`TjbKj{xdBaO}w;nxy4kL{oeyjaiT{jm?^{Y&>DixHM-Jq_ReCJHqK z_Ss9s&Mi)+Z%@FYjG5ZnD)jZ$T;9up$0p5nqHUM_D?V(Y{N2(J1g>CjxBTf{8f+(b z#Lq#TdAHCW*=3$`>xG|Cw5`_}HEc`0h$w6WXm9QxzC7t0{&YDQNsV*Jtwc84QRp&p zerR&rv7HL@T<-Cvt@-XOSnV_21@vpcYs+J8ujux$0zR;R`0#jXwM18Isq&PqN#3Vt zF)LHD>#D1M3Bo+LmQ>95+S*lT$NrozJRP?ht=`_GT)=kHVB=Iz2CcSExS#f2Gd?inXb_()ol4 zLm=DQ+1Y)TpXxLCLX->CZm*|$x2`^wsaL<)(}CNY?p*1e-0NhaGuRksdlYxd0;&}7}oZug_qrO=!aJ( z?BIE;Eh!6zQ4&nR&n`9@;_T72*bD_&zuStRsH~>>>rhqw%OL*mUyne5!L++LT5C@q zoUejC`=+mw33N~Z&gH20GA}VF-mZZFvXHO7;oj$}3pIRpt&w_|zC^bXVNVqDm#eC} z8tZ9+Yr>)*#KKRZoeg4MxDIPTZOQAP(YxF68R$%V?x?Nt-ZnR1=jkks7U-}vh9Oh}e+Ngc?eRUDyUSppNC=VVU9;7R~U5sX+ z)6nA+!Sh}BM;MW=f&Jw@t|0p^HS(J?<4t_x+>^M4oO{=$iZKQ(tE$pT=?|jt(R&2 z!Ks6pa+UdVWw$>UD|VeqVJn%ZyxP=T=Dpc4?#J%GwZypef`<3R8qK3$#xmuP>hC*^ zzu19FSqReyRdfJFIm~9hx87b}KfN*mF!&oaz*0zLgt7WKn z30+%IynwU^1f@|izJw#?o%;hQEo3D4genP1;07Jyw(|7NKiF&lC7 zk4w4RRg>p`w7;BVXxPtA_-yhdt%SS@vFj^u3lslg0&_YE(- zS9m{@c@cCudQUCQw@2^PN-fFPv$b=6mF34yOz@X8Es~iMD-b}_6=LA_tfd!gaM(yj93_zh$8U}qb(3d!pgC}~E}Duu`oXm<_* zJs|>Q!8y>V{a6_P&a3>YjeQJaJ+2cO`lqS%q81Qqp{@Q?Y$p5nU+fK&oO>RJ#bt5) zO;i(9IQSjJ$NI%i(#E)~an~$MSG>+l{t9~0(Ld18*{xt={RBhcm>F{Bci ziJv`e{QgU~lI8@J`yNw-tpr;KgwjZfHn`ag{C~b`^n!i&tk&hgz|o2dz|%cKDk`s2B%U}Apwx?R zHEK@BJMB6*d-pJqM8?RkUms)D($am(JQ-yk^cz_Lw(tC7rl4Qz=ALJ^vvNrNKaLRbwHUVN6J;}rW{$(OP9R>k| z)bwi)Yu^MAKzLv(&ta(WL^Q^WH9?BCD)cd-$Ysb%nqpN_b%tVwfw2PU~Vi1>gT zwS=jejxS$Pk@~f3+qOt2RM_$Pu|(%L$)VKbrEf-+oRrmhc>?Hb{C9`{o>8^yW=CIw zI}KwT19Kt+8`>Q4Ge>cd@C1>QGyx~@!*tyUj@_=Tgiyy|C6tFHNLvN z|84rwX3X6oqHO>D-IEV_=N@4-L)}(pjl5g;EBdbfnm*^f9JpUcM<+?doSu&EBAy7g zD_l9NxU6ip_rdGV6p7!ngICvA=A@lRb8~Zx=V_w8-afX;s_0u47}yxkqshg^Wr(LT zwPyV{tx-c2j^C%J+g(R)?US>8lipslZ~bHou&JH3wMubmLc+NWML(Ha zSFc~6?|nei*wOIJ=l8t({K(NCuO2>pcsf<`a&KpVsy5Cfv;!;!X9UM0PsCzHZ9PwG1b^2ISa%Bfe z6fL|Uy}iB0Wp;~yT-MiryeKj*kGg#BZ0ufXr(g3Q<+dJutZ_Ffv(tHW=H4l@KLaJ! z)-Pk)x2C829);8GHTYR7?urZ1OcMUP^!8Z6-aqG+l$7R%%3mx$k(HIbxmWtB`HT9w ztta|V67S#Vc%eYnAbpcVVXY;SDRgTCKev-j(r$C(&-LrqCD((v3`=d=YuvwY{{Ebp zn0WQun|;zw>7k*N(e)&mcCjJRnOZ60berbSy_fU#pV(f$++LGc6n*QK)BA$Kt*063 zn4UoL?W{*AKZ)72#V&n&>%Z8@cEn!Lw8~|oCDPcq)i=>{xWW**Ia{zfoR9uI%`(ALN(C1BeU{{5K0V%?-N=dSI>O^1}Y&YNUCc@oX~($>}% zTLc4N*xTFNP;H1DyRlLH?AetcYzo*m3kxnhJUnsVgRTXnr<+<^Tl@UrS}A_;fG1z9 z(&^*Oj~{P;7dm2cKGpAgt~EXJ^5$L%+s-k?3%G*Kzw0OURdagm#Kpx2gg>g^=2%`? z;ZFGH*I9P;D2_+h_cQpR4|9;!mwYx+#!oulO zJ)N8V=?ulcjltUktc6V~v=;2l%*-hK1xvo+z}dNXJ2*Hf-oWFn1n|(e*k=!fn#1T0 zp07^+In{HJg(XSBXQid3#m3e)UT8blsH&Qp6ZW`XPWImX z^z@&ZcZW+uRj9sg{8>^7r~iV-+r7Mb(1hW-uz7u_jra@SKgtyZtH4Cj9mR74*IZoe z92|DtY^th~QPP)^k~(?vWVQ|^X=7Ix-A3+A-y>}o7njqgZw8RwyLaz+SgM@IQ>tSR zl6#~K?`bM2kwi9aqh%$zQL#{Z=`9E2#b}X~l$63-xivLw-B~K<3ys}3Ru?hTe>eUl zINT(ZjK6zwo^{j9d8F!ig^Nb?*r!l>qZc<627ew{?d$KqhJ8Uv!@Tg3or8%QJ!}C6fx_twqs8{IafvL_bbXDJ({x1GJ#L~gil4cT^WyNVr;g7ILXnb_wV0h zMNP}?-<4Ruva+^rPo&K{G(A0Sx8+rYXP8ykxA{*GSBiwa79qQLhR;NiLRyCX&E3Q2 zO)p%yd-pD-?=&XO=CImvWo6}Im-+H5AL0w6l`?r^$U4&mB*eud-iHa56h=iw$p{;q z|CYpnaF(BtKTNwHaEO_ra~4%8!aqW-y_^2*1nWGgOFEWulTjkq9K4(kU6@p zS?NH9<8atacV1pzXjoX3mf6{}gsKVz#7M1Vism$BcJ|Y!Pse?U42WQ0W@Zj})SV)6 zsg0TDkBCX7Q>zO@W{=%=dUhN$iaVvHrMLGVGkb9Zj}0nJ;62^+#8}{6MFbxo-}?G` zj`8?C1s|!weCjMFVe{fU0q=)bRu`?+)zzCOH+J5-bqfazcY7smkTT@Y(l=>oX%WNX z0FukG^i(u7)+L5^J~#Kttu%$w6waC*K75!t`iNCa#K3?d?J0bq!o`j|;XC)A@_HVQ zx!WgqJ#$7^N2kQ1F^EOPEM3vh*uWrB^zxRPrY23Dm|$%X4d#;7+i|c|jp7@*>TGv+ zH;Kcbl%GP#uF_}EJm!W%v#bpbS@UC(2@KsMBk36#=4a0~{7t)09GjnC-3|&Ok7y=ICKo3W=<&EW28Vkt)|8qtEA_ug?xX+kM!8UBP=- zaB@fpMGNbE|4DBV`ec?(6%`f#ziZ30gA#SS%eUR-akz9zJ(u}1sUyF$O{gm}^T;E2 zcXzBv??wjYI9dIWosErZF*6mR_WGQ?lhcXe??1o1es<|C?+J#)pF|U$!)XFe>aea2+fYyYogQT>y|LVdxHY;x3XYCjN5I1F^VU^2x z=JDX-LYi~Ts|hh=dL9{=Cq7*oe@RKn`^XT^qw)^aWz0gx{r&xAo{LswxyOlHd3#w{ zLSlpHzhYl#@g_7{h_ceUmL(iz zr%&q=7Wy}Yg@pwL1rHr+d;R)#SJ!E6?XBhS=WT2z0n*wMPYN~oHU0eglj2I?wa(W` zBHH(4Ln}T!Je`)gN09Bt=0>&a=M$y+*Oc)6bHkM-cW;n9Dk%};=bz5--~6*MPX4np zFOT}wVv>-_3g9LCcJ6(P>peUS3%gh(*-j_RuPeKaH&J3~9E2MYV=xe|ep>ef##{GSixCd4+{@h*r^2QQi49KEIzvM{9a|$_(9_ICJ@O-tF7BV}mVT zKMvfnaD^ebkr2pnnPe_8&~JTZ(eHUhg_I(3-2YLVpPw%vAkfj#VP(0cr>D1zh2_JC z4+%UL?+c8O6>*@ye*Jpl#0kt)yA2yF>*L3dV|l0v3w@Cjm&nq(P9z@txkMa&y}kAR zTcZd{4zCyQTm)9MtJO-Eas562v864r?B&o_7>*x@J{wF88rx$0Q_{5Qz|C zH8eFW;YGwbRu3ZR;^7`nRod3n>t4{B;^o<4o5p}NQ?BV%~{_;^Dg zIqMp5KtXl&;Op1XBp3Rd-n}a?E&VzFvDSwAaY2FVMe5<9A+pp)s^INAZ!C;8-r+wd zYQu6nA_DIz>%S>Kbj#`{CP_49U>c1!mR-l9JT(RhDeE7&i1=$Q?Q|j``CM z^uBua=-i_-*Tx%o<25f_xS;=U;J}Lu)$$2G~x z$>|C{iK1$;?<1mlHm3XdocfdTapmPlf*TQv&g$qK7d|^PGlR8j+x@VPj-5W-!}+SC zqY!_F6|fsQ;_Xp4ZvJPVcaj=aaql4~Cy&jsx3oN7(aTKxz`&CsdgfA-sBI_fWCA_A zOl+5JLiRXr>~Z}aKw`_}FW)~DVbKT91S7N%7OMGo7;njo;Es{vxa8unTS!8er+QQ* zioAjni;CRmUsu7 z9?-iQt;`KRKoPQfI;oL|ylgX!xuM_Vn z`ms5y>5acVwl!J9VD{Ym2v9w?P|s_ni{7{p^0a3?@83)D@;(HHV_n;}eY>VEZ43!e zYH_O?c*PVB>G>kFdq=N6#J(r(T@5JM6U=#Q{Y)OUKt^$4p-`MLC*jn=I+j#f3&Vz= zD=YWKFYXX?b9tDZJ+c1#2go0?6=1N|<;(noQN`Bw_NOW+IO~1?{9IeUBKFgIiRb81 zuRlM(*gR?2?wJrm4}8s@7D<8~PDx4W5J|x-Vpj0@v5d#ODcLpjeH*0&2dkVqwc|(h zr+UA=ioTwno~y-{&8iP4dvesbHrGWweMY&<>`ID@BO)VHjX|L?<-FY&ap%!Gdh)g}U%u$;>CMj078DlVym^!Ezd=izti_w(Pd;$y@Zsi* z?A(WSkv}mOoiQ=7&rgz)lE$Am%G&){(9*Sd?tg2ksmcJ*-goRxD{+>?7&EZs|jd*(^$&PqgvjV>+D!J>ku>o=_ z$npn;gl;-e8xmf!-Uk#wFafwf9UvTsWSg|L@$FcU^V9`X(;YN4YJ{JQ*zuViopqP{ zc*ptXMxXm2rL@Hz6ypws&bYQRx3_|DCF<(e&XgBhn|mFLz|YiAoY-;H-P-zKbgNZ~ zq4KSR0lmGt47=nrXKy?L{=i#&-d+B*pn%qnIyk&Yg_B^v zq-1*Zc`de4dAU0XVgI{#d-v|``!_%$SoZC^U}knpvf|k@l@ZNkrmC~yNNUJ$IwR7S zk(J&(eNWC8t>l|81Z8c=_E1^V-rghe6_ka18!e|&QiLa#X{<5$xbaiNlAtbI=i!Q* zXBD>ZXX%rnvGESG-0fw~c?o=EWPuiAtBarc&ZKji zghnhZEErb1@eB&Mka#rfo;%lvHHW_jufSBZ(i#pD#$mL3#BJILqvsDfQKuy*v+mjB zKHaNkZl2LRp8N1&z1Px1{k{JF{_*|e)MX*mfkBFvA#{7C`rp6LcJn-wE+Zx@o2sGH z;x^sO*z40?Qa-!#K{(ElmxYGp%ImlwFx~)~m zOre3{D2Q60g{N^kA4}GGFYoZ6qvE_W8Wi-__Vw zt2oa?ZIo*vJ*2EFT1FY7$`%$DfM|MpcgAdy>vq!9Gv7U=+;Em9JPE50Qhq{BElVJi zi&c-_^8ESp6BE5jf=2O)iPy&(0$145W!-l)a~PL2w6{+WKD%V+b5vILluMjr=h>s$ zr4|{*ByLx)lFpg2_h1iw_<$s2Y;5dH{wB>!sW-CHZ^sR*N&z^o?52pOd`@ zS;oxJ5WCb^U;l1eT6CXSfKppaU!Mf@1LA>0AZILANT#XMu6po5?GBH`unK+xjG}E= zZ=o8CRP*;`M@K#~w#?{Y)RAt+6WcQ*qq`W-XlQ^#SOQQMB7o7-#zy5(RbAq`851*$ z-%RlP$EV>d#m=7;eJdYveWya^{qyG!Ua`3}>?Yo39J}-;efrzC@E@jB;NBJG<@aRW z1->%G)(s-c^}c(z+id61z9vB8CT)R$hK2?Nb?Cpvb0%?daq%BZffEZ z6r^pw#<;)Xiu#xU#^fH@2hGDscR>H#k~=_$$@ug#TT zdN#y3)$M8dGVh_4yZD_a^~#U>uKXKmsi^}FQf53I{1HV(BKfKGR$b7RiyyRS z3KI1{O{JR%ob!pr;Pi|i9l4*8Smby zow_Y>_%OH+257c4;?EJ{$nxVHFu(>n<~G>|rUl&bz2=;0b?n#^{iLXJ+U8g>=NvcK!; zIqvkKil|-flf5&Qcd~JCa8GmnZzSH(&`_NU141<`8(X|k)&>&m^|%f@y} zQ}Z>zOGig+`R$2`iTHm5I(U3MqnMCXHCzBVb#aBts-E87h#q$xcFU08M+Ya}&PY%p zi|=g~qO|_O1&`gNkE&>k7(=$AFU`(@7=rIZMRi3o^?j}H>S^X7ns z6(W_Wa{W9%QhiXXVG<&>&e0IHgr`QDnun2@K1^=@{GxfALxC!F9!J_H)LP%b;7#fg z8sEbVB9@kxkV+{c;wc`p@55mzc5U<`^K+$t8{ZgLntB+ z?FxeYQJ)pT3buraHheCX@$5ks*SdxvlP17@urPV6HQlT3+P>rGwsvrlDCguH2w~i_ zhhK~Kx|P)gQcS2(jM9gGa9#8v|=iWJE-?JteAusnL&sScw{lJaz_v5T8~u zLXy&>KAa8xvu6Xs?XN*&MINC|+f(Q9!;FQ6g-rD%Rh0ufC%s-4`?t3lTPPYt-XRZE zdfZxQQV9`2wPFWD599~jd_rLmfqR&dL&{+QI2pH_xhF=hn=gjGy)Bui9IUCYsmW@z z&TF6sg2!&^go?5<)nRFFZf-?Ij=I0keST{Uns-Romw<`=l;;dw1a}|N{Rs^FA5hj1 z^+lj+3#H*wR2+Tx?udxUPL)sCJdEECgKKp|wP95~r2AukQxZpy;)-vNBp4n$I`e{Qj;TVb~*V zVr=YrZUV;%$H%hI!NS7j&(Djvnh`Z!QWs4_vh17#GGs!Q*OW3u86tk>PtMOjee_5g zFt4NI6z#`zy1E4GftN40)t$!?JI>#zssa%o!`=PJpmH&Rfnj`RCTflyLC(y=A~Ti@ zNChkM;n^i^oJ|D<1-vpbF@X}|BQ3?b14ZuC`Tk*SUPTdWX?dCQGQSKZ!*qZ%$6iT0 zLZdOK-Z&~ffXc`(sN-ny_Mt*zZf<`5e9{D;M30M1I+QM(T4y9YBtlFEgvYk`YP&!* zP&a@u-qI~CFJI^Usea@UWvOU}6z@VZf zl`AnUb@K_5#_W|AmXf*-ggc;o536u5J)J||ljMH%+qZ9va&sw~O$i4EOE13u?qK(LaR}9=Ob2N^kftlGGtCAsM z0!;>Ad?~6Kh-@jXR+dx59d9NvFgz^B%^h+g2LLoBH5CDw`TT>ex7g?<7sX=l-hB?7 zWpk;Ppsr~Mq1b_%0A@hZ-O`#a$Hv75CP%Pd1+ZnU*;-vB7YjSBr6udYzJLGijT6eM zC%wJbaLXLWZp0M&peln3;{r2J3PiM6q2q)xMN4W;G75$ONKT(VK+_6uK6(1|?&|F8 zM$c^NBA;z7zS__I<_tU3!!&s>@rvBt!yJB@&EuKk)0zI~Ph zSW43S*VNFEF|D8T#~KK#R3ki*{QZ%)zB-8o$5L(3ca!=xF$SYV2JsECq1b&3ECgQw zGsrpczjy7i>Hz%TTFTh6kP1eQKgh*pF~G>#U;Si^=7B)B(xITccXtmmwvb24pA|8$ zXP0(jmy+;=_5>N@vm%n7L#?f>g#huC?hStBX6X19T^t% z(g@|DwR%dT9Lp0Uq_~}n8}S?&;hvm_aK${4Wy^cI5bI`A(3|`>H;x(v5&&lU3!o)H zNDO+xLi#u_Pfb-dz;pe86={_=tMFURoqEDSr}?>^UpF8Mp$r!8*w?Y&f1~lEcpsty z<_Lc(924W>0*@Z0G{Q0}5c`YuzwLw!jhs(RKOG%7KV-9pN=%iI-4HYC>pA#4O>}e`{mvR2rvfhkN^Ph) zSnwvKWMtg_9v&9p23`WK!Qsm?5{<8)AGEYLzdz!>+`qr479x;!v_`X_f<$R1Um=9= z0FXg-wQ43PmbywC5?Y{z5FtMk`c-~+SwEMh<~A`gF;sV+78SkfKCh|y9W@V)Gv(Ou zK0ZFAY0n{ODS}0#TuSvQ=|oP?Q`72+uV2Foe?W^DD_YhR?O!Zv`f^@?b9bVsGJ=d1 zab#0{6%Z5zdth!^>bS}18&sc^S5)}smmJSpa}bLUQV zMiKw@m995$o;LrjL7505E(ELOOSwIEQ2x!z?RQtyUwV7L)l`Bph=!g18xB~G`faBo z(qGi)Ncl@ccxWNa34Hqk7R*r8)r+4o(A6EPbl&foM8hInRa;y6dme=lAef*8*$B== z;S1gn6~xG>f$Uw{M-H+6=5gQy6M)CO0vs+6D>X*|TR?-P};Hz`!X$ zgg3FNRt=zGTMNk}DGSMq0O|yy!Uzl`U6S_p_C|=;P_3-1lQ&maf0DXmCx?8);j}Gi zt>!Oo6h3@tpr=PMbrjj)_>AP8J9jvjsRtdd_c-s^u_M+vw5qK+6v}C;1(3@92~5k`+nuaQ#B6UeAm#>8a^uM zT=h%TYFkY*kE4>ZfZ=;`iOiYjfx8PP0WXcFoO`tx}qXikr!NSU3D4LtJ$^zVYXvNG;$;ilJSw922f#c=O_}~m-&|WD= z>9iIncgTziWZJOIj-jHyS4mSd8jK9n1UX7KwqTCDejo6*{go@EaaeHBVY^4)Nyp++ZSoP%B zuPC@p$YjROeylyeko4fmleF}7L5q;mQgJ8{b#EEk4;(v|k@-XT&>^bfxMKispGa|E z!!J?VUjMVi_h|({(3AZ@dw3>OfCzLCO1<3-3xIVui7d^0+l{PG~Gp!H{TgvI5% zr>~7ZYTFYLP;fi(&YfaIKt4g8he1n=i@(fhb0hq+dM0U&1>PXBX_V# zS$6F*t-XF4hu`Hj*hg22#CDTZJK1|T;JY{=xphKNTreO-C3JSq&KdXb3o9*QQAC~D!RLH7 zR>dsC&(UT+)RvLOLw0Pjf@Ap$bH5_hs_VDx zyu8Tu{%oQ_NBQ9ltEdGEY22SG>z+SH@dSbL_VrG1{hfQIW^wSLE84Bk|BqSbq+nM= z1E+vMK1ka@jY>pwOG}6Yb?hz9;ax10j6ax8+&vKM(HBiZa={+U{;IAdWzA)0!HXCE z;Wx1#1t)h%iZFg=;!VoT%v@3YfkF@EHQ%jGg;hNB{Y^S{cw!=RC-s?JI)?k0Kdnft z$Vjcn?G|#%`~FoAA8z|SNc!0a3Ruv3ziL8aq7+oN2J2xCtQHP1LB0n#NbL3tu%FQ^ zL%~GR$~@HbSATw?X?&$mT%6?QajI@wza~ABuRoZ5vU76Y-jmxtswgU2jBOkABpH$l z1iGLY<~@6k+I44^l}WmgTD3s1d2~9B8Bs7x!N|}MbhksgkugNo*Vh-21Z;m~W;r&N zP`~GERX`CZ=blr5bHvhdSlG6LQwInU8Vu&)kJ<>eD%cG$QvI1|rLo*bEImX;%@nWQ zd-CLp&zaBk*z)_1-SE}XVPHIE>*)A*tdZP71v1gg)>i6)wrvL({v%VR`bx4S#!Xgl zQ4H=+&x007C_CnO@`49boy%arbG zsGEZ4?K(IJ4hnG2Sz+3Mm2iH6)<4g@Bi2aB*v{+gmnR*qQ#sqJr;?jeTr4{I^dlr) z2n&rCb`WkZUv}NH|CyPat6E{kcJsuOqM~muGVJt^5bLV`jmM9vyucLhLMFO666J=wp%lO?63-UBX2Si~?ugNa3LaXG=+uwti= zvSo?IF)^LUBCGYpIxP$xRwdBlFo;;Hv=sKT2zy_)n=fsjHA*xtwD$Eq({DAl`5cv# z^BFS{?bwML4rb@TRjDWj|E<5j5klGxNJ*8|)E=lv$Q(FuTq$0Nib4e7zAaISyv(78 zOW1_j)%0$n9+j*LRTJ{>;NVe|garj>QBiGeZGBwxsmA?EO9aEf!_CjPj}fe8#Kahe zsUBA$>q1FZ6=hJ>uo$Z`1m-S|6|M#6&z|G5@VJRfuKqVf<72ZGA%U{W4h;>_<1B#K z!Aaxz33CBfXY{OM{?uwrRtc%FetREJYrtxzw%$u&G2VFAkv zbb6a70b}jp4kXS5FEMFpBKXCOLMj$Et!gL_!Lbq&6lA5{#ux~_PuzV*Z{NQGK?oKR zj84~F_*(*^yHZnA3w!ZFeT(}t35+j2(_>;HW)h*NFi2GF)_M9{XUt%8(2oHKgrXm z+JFa<7vGIJDEWB;%8QiKNJK@_ix|@i!DeiG^WAYVmK6J1q4oMYwuL? zhS3+n%Iz58b3w_-h{*3y%>6M?@)`P( z?=ajd)UPzGW$$q?FqGJ|GmbP*pc1A0xY8(}kP?`mnaPRO%2-64SOt0c?(3?=LgG|U4$qILANu;3QX3gj;Rqf3+2or75fX{k zYikf|iDXQ)*3Be0_6MGM1PazVI(h^6#O6vC#e|huwwjsQeV9@#&g|q@$oy!pc^Z-I z7EH-tpRS)@mV1B4?uPEx==YMyi!anNzamAL+Sz5nj=FUSiygHGNyUr|g;5hA5K4F` z3_bRC94ey@-kI_m$4czdjJB2*`w{#84@Kr;Z_gb0`sSX`*Umk(`>_9*7#XR~QZxnv zr)7Vn=z)d^Uq{ofzb80#|cMlIfyBgFe za5O$2F*Y;fs5mW5sE25#j_?mF6$gR9Suf1Rx=$zqJJx|zPt~vq+ZmT=R}=;o`ti9> zW$ZcUf#SeMi%!rYzoA})3dU$P<8bieZ@R3o8|XG&b)LSRb;rwHHQQQ z!otEPbQ>AD$~EkbbzB#zo2RDe;fy)V&mWgta135?cG+u>pFLZ8b5E}1V{ZU~K!9eu zjeWcCuc^2P557#aM*rQ|sJS*B^tr7!vhAskOyV6xJm}@webZdEi*~rkjnk+f7#Z!+ z)v5=|35Q|NCii(#Qj%TBIDJ_NOvCWI1;h2R_XC^90N>y^wHFLIHh1Xdu6hVVms>K;RiOiurmR7`A#sX8jJp8+6N+{ z_vRKC$2w43Dc!hH&u!Gy=BI;$eLNWIgcpu2g+A3W_Zc|P9=*qkf%r#uHjpupg!#o0 zlq{~5aiaPgUU+a`(Gs#dN>Vv9(BC~kd)bdb3g#6Segg$a!(2trLz=r}F>Y6ne%JQuKGzy{u?NQ84rB*SGK0PP1>C+<19U?mNTwUA*V z)Jc}UK5>OwJ55SRR+i1N2rAdg{KviOdnDNkar9sWK-L|Im)5!`dqhxBtInXp_U-+f zLjE#MI2D!7pKfIlb9Rpt)i;{e2@hA-MHJ$Jl;q@nAaJwYKm{K1NfXF;RFwFQ=DpW7 zz9!%V!eGN+L8){|Ek> zvuC?;l+>iGFI@N-cToKn`_Ty~wA0i4Has>|svUueF11wW80d>+HbJ#v^u|?W&#@8+ zqOjYTN|pt++W@3i*VKfChg(l6`aS(h|G!=UfO>e&VV5Rw(p^Hu2OCMG)2vj?`lW?t zky=iV>JN9{)rA8UTGK&Uv$jLzaRGm9kKQd-FBoD@=^R z!NFp`mKPQt7ZsV8+EA0$EVMK?tFTH`finy+({OT)fp@X`E(1?NQ&1T`{7-KxpZk7I z_5##c45X+zY5W)QA!sXmcXy?;dD#UR%yf=Vt;X;0d|7}1C48Y~SFzaY5R<^cZ_v;| z>c-~y&p0^=6NOvWpw~$Vx)hX@I+J{xsAC;AC@_3+nfULqDmy#fLNlpuZm-M*jBAaX9EakKZh-LN@MG(tmE^ij*}6LSeUytZ zH^`?64hIV1*rcN&4iuD*Ygq|6pXOo-+Hp_0WtScGlDcR30#Xkw@7)HMj#r^SCy0^; z7YS5*#Qx49a(8Go+=mbQj5Uzb`l0(LFT?#~W8*k}1e1fULFw`4bz2*>jINH3C<}Lj z;Ks(EzShkmkS%G*BN0<2e6HrH6__6Z9e&)wlEwVwUCRT^0S5AY9g?*4j|w{zOwo=H}r!Amsu-->v4E z>FIq(-C6*Ca10URvF^>TE+ODy;G76jh9^$g_B&%SLf2KTwwHw*sB54iRUCQYzrV;g z5)%)zB}D=G28^3vfYuVh0G1$28hPuLh@t?&G$!WG&OgAx>vl5)y?C*Ogd{t{%LeD# z?5v=G4&fU1KjI9jin?7C70Fsi@0%(8|JuJ<2&}YD}Bhum~A~YXJN7w}DYo8G*c2VG%(2s((<4$fwHWCyX3HV2bLp|hze`>0!l6>PKE^c>Porap9- z9zL@p*||}W_>Orl1UT$wjQG`W)EN7emL-#iHZURvH(Yq3wgM{&2Ht$OT16CG15(%7f(HU^(zwHXuY52uev*A#!Un>ix8zN zs3V2u?H@+s;{X%!?Ry8o|J|!s+q<1dnDzDF;oL!1-BGA>==%H!1VFyB@2R7sqtKLi zw03QZvc@0@qX(tNF~1nOKU&TG(6mi!)NZsG!7gRPrQmJ^Iaecv#ki?y0V)#q7$}(g z+|WDp4L}ox*MTS?UeJ4lDh;u}uUJ%0F0IuHjzs&*mxavh(;Y6T#-*fq!A=kF6@o5< zSUr?T97)PMl~^8>B{I-kf^et$OhJy1?;A36o2?Rbds6akzvhNNpmzqAb_l18=f3SE zW1uYyG3m=%VwQkmu&uMx;Yj`M+i#KfLiR`r2+Y6^xXbt+CxA05FYhfs=AcyGJN*l< zWc0Pxujm(E-kAW+K~}318^;0JCUTXGK@7{G;PsPxP=GrT#hg-}2&yS2=5sJ3piCq* zdVcC!4OR&)C-MBoDBBFV}M-7QmfdWz}`-u6~R1Wc?4oDzX;NZ-$;iK7jJ{~UG_ z3xN=up01UiYXHRo-lmujJl0N5Z_1+_;bKONF)LzKNi-5`g4we-_oSUq>#JAT>V(_} z<>kvn7hZZ1VmJ!<04qQ$7|;l!b?p`oFqL^L5$o#TrD zE&289+NeN06q%Uq$W|y1K;ze)ELST%JWTAIMDfDTXYSLB{oRZerKQSpH!uLeKnJiO zh&bK$IrQC(oBMIM4d?Ak820X!L(IYApgP5W`0%G%4-wSaaX?wmW`?YRPFCH< zjz%D!27P{tQ}moO1JMpLRb#~~{4<)F&IB07m7>#+?Pg+XDjR{IRKZN;Pn2VU$V>G# zWMFXs;y88clnu=Rb~|d7+&$Q0r94!xao+@T);d8hTPQ-n7m}Fa&V!yPmwS2bL$wxk zN06E3yt|=AiJ>4f(Gl}o4J-@01Mi3IRQ`!|?_Tub00upU4$fk9u@o7pA#nnwO;Ew^ zMX`+z)_+4rMuw21aZO2c)t|HfKqm^O7a^!%z{H2vr2m4vCn^e7MuD8ju0Uh(HdTJ$ zl$RfAZKY%>2X-zr`}1o$p_Vw-A@F_t_;DRemfr|u3U~`d_=kRVk1TQYUGkf=@#l;;6+5_tkOrF>b;k0|; zB#gKt;^yK~TvF0Vu&A@`N+mW1$+(PxCs(oTXTA@t1Gj$tAQx=nuwJM>q!{6&HG6$e z76x%9*?P&MN@dN}EqJtEDrj5y6c5fK?wn9n?MxOU_qvFniQ+Lr2^tZfjnzY7LdqZC zyXzjB+*ny71|&Rp`ho`CHt zFImGMHXdI@HaJU46}AT;xbwHWRN6hv_?d#wug;Bf4Y%A zLno68o8b7`nz!!PyI*~FQMoH!caNk5ZoBgL#-E>RHcOBIyaBXVR#rx~P_iF_N6qY& z5?Yy{5joxfi0K&B=^txZN^Po zx9!-Q>N?YpCV14&X21`OKwec{4Z}g%2es3u=MWE3(z~+k4T51|!Rg%a7PCd^Fh$AD z&!5+voDK*H2P@Izj%x2UWK6iPH}i0n{Wo()<(WJgh-R6U8XGt92+JKazHZNI_gdiB7w6x=hg!v$gIFNJ@K;9RZV z9nCECb9woQwDgbSFKC$eym=L;750G^0SYunn_uD$7HpJN_A z{0d@-rT|s>BAfLP$ZxRjl;FU=z$V1`jJ{zeB2t8lbK3ER=m885hkm|v^V~&IR_qi+ zq{(_*F*_@(C*TTh$lxZTmOvek?~hT1hD0cxjsFJpE;BT308~MM#aD1$kM{Qd{FdPl z7lE;cSU)NMn#bQY_otfsl^L*(-_Z9II^aMNd2|k!1nux~0=wETJ$-%BiWa}ul0bsF z%|w%bcPgUuPWgaZip7h-kDxzx76@A-upZ|J4pI6xR8YIdkqqZ8yKjA0igIn z5FrfVcV3$BPfF9KjmY7lz5jzv-*^8uY?>2cP;JJ+wQMe0e5!CvL#ge@VQe+$wCDj` zGvbb^#41QMTJU@N`atH*O(a-LLP9_Aa!S@HU}q40nX|K|hK4}rsj;zRlat(HDVUKdeB{ulZ0-70+7RYxx^%rWJEiI}jwntTcNBBpc zIw=;cqo&T}s2@aLWy(eN{X(k$#Z2wwu#pKWqyF<6x6l!aj?&$`Tm3E~DdKRCn5u96 zdFy{)-V4P*BXe_qtSg$hn0N1{<{;KtV6{-*re{$c!T^H+@H&=zW^Bx5eZ|pI@@y2& zj(N2krPl^}BVj3`x^*!qg4W#p6Tn?uAsJUmzI&l?oh#K<8FsfLBXwR^;Y6>~mjxu01^{8Z`E_avE4Lkt8}iBFLx&uO z%JNhsSo0!;CN~@4>1gnCCAK%F7*4t?sXZCenH0`cB7kk4xXnS=WkHn{JH-^Hq@iwN zl2(wXdFm8+N?Yo^Botggia5o@bc*-H*mN{VwL<+C{e=n(bb)rF8L-Y1-L7Q1`4ksw zu3bhGes)F@O%+c;W~Nec;#RL^yxM|~gB>*!7|vzDmfTr_lcusV(VgWSmJh>|{u2X* z8yl=%ve%~0>b36;Eek2a?)If?;9D=?s8-F|jy#HG;8?O>Rocp5Oiy zWCy4g9{Xe`_$qT|hpTF`<|zzH={#=3ej32+z;vRj+5#o+c#Xxga;Rz|Hb3LA#u;)R zx0=V%@(7c#YZGl~6e>IrwA5|-7AZFvg~T5L9xSDFk+c-FWR7-6N&lbv3L{~nppo*#4`v2vR*TE<{=LZ9D?-O0 zM#C&9gXTb#KXgey1*P}~Dq5B(0ZIjLM<_!a`0^=pxdF%?tObM0vtb=ob+(0=neM-b zXc29J>-K4Ax(Qj@6eXsmrA7EVq5?7lwPJq$R>i?>JMV{wZ}(71kA0kXl*RQkIhmP*QL>%7o>E4!AxI+B}|NGBq|9;7Cz+J-Vc15rL|ti-!d7fq<}o zKe2%rDGEic@1SEWRy_2vc?gVAh@ z(?f@+0H7M|S}@O|ixUibs=wHq8*Iortiu*VLp z8U!f$s@L@h^yxy%k(Zap&xGvR?zkaT$xC_M(8OdNO$vYhK%s#mjo4S~xvv?OEK4(y z1{N31Rcs52kAEt&n*-(mT^h0=_${98hb=_#0TzzH)wNR2<9aN_vX;2Vnky-wPD0eT%bsvtqafs5IUu#C#;X?4S}&j?a&ToJk|qG41wOZ zgNh0zK)I`5X+2-TXu)(RD3sVsR)CCA=1uHi1uq5$`}bHG6a4j`Y!-Uvk@S{`1{IHp z@+sp|o5)UQY=?_O-Q(I80PFZJ45%$>s2&eOWXrv@XO~zvW0;1-u96V682(4XmJt@& z!{4E{>+XIJtQkSybq`_g=^H@xF>&W`LnEU?xLCVVkAND8MgI6$`@u5m^Gb1LC5x{I zw>YFXBgzMq=}B-h5F^kTl{Le4qQDrxbk8FBbea_Y(CI5mP??FQbizM^ zw(sR4=sR#Z$T@6HwliTB;xyL%QM$x{h^Hzq0_NyGb0>oDt_B2>INK2O_+}aeer#2U zIt1V5AQE2f`?E-;kafb#w$Mk%puY}EHhwr$g_ZUtgmx$u#BBl$k8JU(wKePJJv=yo z$C9ik@O68DT#yjl-u6mq%Ba#!Jvel^B811~7#}ARqU`lq)_h^?>Soa%RP}&o4_9>| zB%YlsgiJ)?OgpdiHK>CTCBjD9*vUmi=ahiU^@qqHo0^50lL#EqaJQf5ntzlmq>(o&BcU*nQo<`_Q zF!iC52UBFzN7`-t+^?g-6?-_2k)oU=d-Ui&a1?-I%MwFGNeH(n$^0m`#%VbDmZghQ zUefpu)zje9`@)Owm^Af%XF9Q$#nHkdpxVk4Ee7yHL;YB-2hunKx~@l#;wk6RzsK3Y z8_MO2&Q)kY<*HFNVYfwE&3}t*G2vvBo$9*3Ql^;Qk=Cn*K0DZba0y znF@+f2>4)fvOFZuE8`ma@#iO!;NA)*J}dweLc5LF%mYM>#uBg0wHF#QW*iJ>VT(ti zKw;MQ84@$v;!wjv4-CdX)f64G8rs_6EXVWp7`f5oxQNLEAXa(zy1SbY8N@3qNOe~m zfj8#=wD%p}T=wzbk&2{jCCaXhjEInxt%Q&eePv}tWtMgGHOeL@vSaR1zgZ zMn*+u$t=Iub>Gi(et*RCT<1QHb02Yiuj@13@7MZhi|v2c-Hp8z!uLb*S9cNWP!y(W zYSe@0ilOCC%#K(7P)7dbQW4}0ts~V-s`d-PyPT|sH9~HhQ}E=_l&W=Uw1}2t(Co$I zMFVDD`yeOhiRYy0wvAXIe855nMWefCxJbA6$YmPKu#k|AOp}GIBs6*$J=AUpa3J9` zQtb}=?je84dov>$cG^#UeSM!kl}m-4ye+4ivB@RE2jJHy($Q@P)y8|Xg+A1ug!&WL zGTNa}YVKit8r3jA_ghhIdSV97j}ZQF$-N`#iHVf}(M2uqi9&=gUh`PFLy~;5ggDwY z(36C0`%5iCX#hZXVWxp9g|q7Vb-G?xZ?f76J-I`NN->^l<4(@%7DI`(--wgD)_>K@ zcnT}{fzhowVPVa{DEovAdn#OaS2W#0j6%@9eQ2Th&gIkq-l&Bg-Le^*P{Zy>4$KUN ztH1X5S_R7n>;5d)yoreq%wd2p4qby@vM8?!t9lfbH|=}JGT;=9g`r$S<@9Q*NRzVnE+i@-)`qgP^C(tI!%`eMlUf(oZU1$U{D?#Y-U?}ic-Z}Dqe5|QEaW>^r! z1rSccH0!dLm6e9MD)JStB5V{5Z+dLBP_1LKcl<`$l`-5OL7Nl&Wr6M!@^M^1P;cJ+ zh4KRWOmsI1W1H*Uhl3LmI$IO>LFyYZ+(%Za3bLS}xn(|+YyRZnS;o0B4APRr<&eB$ z!Slp`?Du!5ad1QX78M;GFa1}(UX}X0OkhX_r@KYK+7i{k65^N`FF&n7jRXx8?}c^W zb+972VFsF-o9BaMIeMYvP}z(`kC$g3#etznbfH^oee(utK9Rvnv;}u*3}Hew zJ`2BAOn3bsRPvZ?-amT&QPfCUDiXsgF_er(BfPuBVqec0Or-P1g5v@`yzqvK`Gja$ zVDUtcxv7$>{^PmGTW)$zPQvG2|A!2I@^b8|0$TWA`(9IX+rl&-tN}-x91%?g_K&t+ z)+{Yh;6(;7(P0btqc9#fyU_ikLWVPQ9Jmi0?s)0Mygqo4y5@y6=M)xnyiztrp$z>L zSXe(4vdmZ33VZwb)41%}B%d)*A+U!z!UDyVg+-*7%D{f%$NUElT*n7zit`FLQX*Zb zYmJxJ+Zi%f0j&eqCYQmwqgZ7X6)g=7FA?IYZFX?hOiu>@RcrVSS!M9iWFDP_>TSCZ zyP)P9nre!G`E6St1>j|Fe*V5HhNbdxlxU8dXm!BS$$0nD(jAW%91Hqd!}h3rhoW^* zt~sUCFOw!)4juc%zp(><<~Wf8b`Lv{tDpcA(mXmpe4wf8?B6`lb&=#mJWy8hTup8?S56Gul zZ%kt#WsO>i#^JMgV3K|^7>Ii@P{Z4Dm(4f+6G!di&rOvqu@uI6yVIfsXO)H_ zY=`-;L6zV%R9MjdMYOG(lQvotJUT%UeE49z&M^2i58ZK%!uus9c+xkuL;>+a z)jA5q>umGq^PA_h&<&!Nf`$$kr0G-aS1(g7(qSIX`E#3uX)yIaiKDLZQsrSj4p!tv zWy@jVOn1l_SlQSl+wURkpz}T`4iS+_ycz5{<9$2;UZC@`-4Epr3b~t7y!+$W^?bk` z6kJy2MY*}T&m*TMYYna<6qPJ-f9u%5#GdYRj3$^bzh>+i<>mJTH@Mg(!XSlNN7u7+ z;Dqi*?4i)UW5>2I}~8;4n2XkMQe4QfkqD6S7bkyfpwiF83UdPF?tjhA4hld9$03slg*xwO3@Gppqwt&sIVKt>+EFPc(guPLO z6Ts5alE5Y65}*M<-xZh4%gc+pz!j@F#^e3_Ky$&VV6{RkfrPSu`8ZUlV_(1iJZl(i zz%{o(clXs)Xa32(eC1i7R0~7M78EFe8!NQ*TcQkdPBOtUTaH@DedZ#}t#FcIAK_qP zqEdPdGs82_$uJXsjUy-~u!eU0MkSq^oz2^8CE+bi!>1eYckO`f2>6TO*x8qR1A}D4 zXk#%nY!<;lqIwQ>IYQ0?=yyKgA5+M2Hdd&cGJF`;c9F9Vs)!cnd!*^ zOmOWNPXU)#zL*>pMfknL`Qye``oR4IR&D63JS{^O2lWI{C)LA{6%lNJyvk~ka0n7V z!c}wjSu2E3r1Ts%5WyRyH^as6!x2PGZ(G~m)jn)G|K3dyxs`WAGEI<*x+W}1MZZx| z)+7@vyrD1Hm2L6H0t+p-Ra|*yf4==A~ zq=IyxwzjUX^#&``QVrwt8>t{s3LE@bSWDUU6BwU;JX!SQtnn4q8bA@%;NhkdM3 zdsI^Kl_0Zakn7p{DlhLuN{e1WV)OVOnNKx_r2@kq>EM&AV&K@a>1- zC53MgJ5ruM$Df+xHl6>H0~aA13rk4FY0#1YNhMDTe>yD)IOOQjTGPU&pY!uAr)c86 zfJhmZyCq4}oA4c_^P-JhC>IeFB*)n1Hb)+r?;eE-NJY2 z-zkmG0BYj~XYI?9Qc@EV8&X%2C76f&0U9kW`P6K_DG@#kovFkR-bx%bEp(7mb*mVu zHLF(qAV2?ebcLFCFE}}&@brR?6;hqXb370B;SFHV1TTfnIbKibA6Uo!WpJRoJ0ogJ zu)qNUD3)Q~h9>XH1+hbii2B7g0qFL4ouf!GB6vA}T7K9v72_-ny{|F4WivDF3Pv9E z$t}qnsHjbDe8gB_KJ8HgKZ41%(bLDdRFmYcA~;0diE5J~XUD*R6-x8e>Y=0iXcO$| zR##Ro(+>y<2*B@8s1AgLHjh4i=kzI6JV@wZb@|o{(&k{etg^47oJ|;=Y|X(E5%iBC)%S-DwtM{pp2>W}LxtL4sl3wYa(<-bQux=1y1 zo@z($wZ~ne*a&!zu)c^`cb*v-xm>fZ95mP_nVsin3v%OZOBx3*l6 z&Ud}66rIo_prcuyP6Gv{I$d*jb?DAOuTO~VbQP}?V>BDxo&Z&!5K z;MjMwv1#306=2a^udd z+>s+trPSQfc+?u-&-0E48JjEL^D`ZtLA%{wk(a+cY@u`K@6t#tmczJp`_iB_@+*}8 zfopSG^YdKt-dqgSO%&Av&Iu8ZDUo$C#Mcus&3t(%#~wzFSY5Y z9Xx6b-swD>i(aJsO1LEy$8i6@m{LmE23HwWBJ?C8;q zijM$)eX8?=+ypnAP_1LT@e1_Vg&nF@nF1y{MdG_aiuXR7K9KYZ9^a#20)eJtRsyHZ zJsTKrII#T--(Z4P7|f6-x_Ivw7c0Q6?KNj$M@hRDjmA=Po`Ml6dY;!4>ce@;#3J2B zx;d7^|6p~5=VC6!I)FePC$}J3PkX5(SM}B-wd^GnqFsfkAk?a;Vl|iWR7bviadeh} zT^%VL4|m4V=`jp26-AMlwfg=v@P`mnv3f#|{m{a6X5cX>?3bN_quPT|o5vw{JZ_EM#_si*PX~X!nrw zi;Lk~{escdRRK_`gUlvlcju8VP$1OoM@|nuB5iy)9B~1F&%#XX0@xWzr|GYz+;vI0 zntZY)Dn^boyme^fxMDD%&QlUbj~tpxfh)KPZF75;gj+f_TBPxwmY8U_-0oJgO+jTiwUP*{sFv$WqggS=M*8ci_%uJ(n zh0L~91EG%CL#n%KfyWFENAUCV5_xW1!r%b|@NRuqGKQbKh45@@Ay3|ni+hCNWF*{7Pv2kMQxDy`F9}^)^5aKpNpyO) zq`>*@q;1jc%nzv9)|Xx?x6nIV3UTe)rBg0w?}?}7bTLz86YzYb(+nGQ^z@`IW`}eh zkyP2VD3rCisKkJEs|Iislz-tH;~gl**K*$7=|nZ57|=fX?HeS!kqRke2Ujp<0gi&UdX$qJ`$aH9ji;PCl!JfUrm`Yw~|ft z=sHb(6G_j*=DJw5w?1o~NP}%C%In@QFbP-EkE=qOY+*bmBgv9yE~3YGYZhV-D5hxX z5y++qpz=@1m(EZcDUv+rhka;dO6)oBC)~U_IytFV7g*>44QcdG{TA44{1!iZFUQah z>L&1q#tLh1nfgMDxjUqoJ_+=NELeEeNA6o!aSD0@@e_p=V&vw0INE+N=GvjEhP*Gk zq-oWb+}rYAtSc=Gepk`wq+s*|kS()%-ROlc+;e2Zt8}GV2X4(m84St( z>(kD7FR=6Ul{&;C`O@0;i5E8U?ZwqaNY{Xhyl{#PYwU9IUwFCas=wwx8eNl zZV2=N3ZE|HtZ9_mcYvFj87np=+r|?+Az%u_Ql@{(FMc++uy_Vi^Wl2&O6E%7izMB9 zMuqnw^P5xlR5JT&jYv~;!1~PD07e5(R&K}gEOelG#Vg8wc{K4PbRK#Zub~4P9LzkS z9(Z?x1@cR~UGyv<^RToEh>M4AA4ZD2-_JH#awxbLlj7xIi8#9V9TCTUfcZjfKVI1e zgCdATBw23nv)_t7yT7e1dRG2!1!CDWXL#lh7Bcm6M=^M^@CU$f6olII~&woLfP@x8v8w}UZ|vPLJ46NWlz&g-Rc9Z-cb|) zkT19F^;0ndLNJeIx~6?+r#c9-;yQ+X)d-9@M4OLrGR1jHS`TjxV%m!4jata9-UWvH zSy4-n7mw~HJL@yIkiTmO-kF7)1D(^$oH6Jbe0_Xe@KP3~)4idIMt{~n0rimko>QNB z{6usZCcSg9kmHuH-62rwqPtr|QGLNmjMIAHLLl?S=B$SgXTXf-)k9c`t9Id|h535% zZ~g^k&^~8OO@E{52et-LUIDtRzxSBWAaMi@W?@mmm9wkwPbwQG)q75|lVFF`?LK%I z0ef$qX;8YD29YxRA+fb}aERm%RmaW{RO1e-q|`8^dzV&+!cml$2G5}!#d2mTbl+IG zP>O`crh|j@wyo`+`MKLKx05hBRfLXfeetKDMIDu}34a9*BBqlgM-o#f*bV%Abr-j^ z<2$qj%Q4^FFf2K(l!~zFmSOz@aRBZZixSxR1-!8jh4|Bc>iu_0<-e(FP!dTPqwvkZ)0#ZgR zATxc{+=F>pEbA2+1OTAWGhzImIQRjC99{SgpeLFBzjuX1OsFK$p*AUv3JpZhnA2;W z@iVPZF=v7ij(#+pUnbu%FW-rmv!!^@DqIUyUWS@PnnWQJ;gr-|&KSJAcJ0E-Y1C6# zQ-KeH5;Cn%Ms{+W_4xTq@bN&gOMs!6xbU#gUpVxmU^xb1aJqhy)p;j0mJ(!vJV;3R ztWH(R*hqz{Wv4=6i0M#JRB+_o92PI*F~Bz`%o46-5+XoMVpF{Tz$SbQRPxx436T#5 z7}-#jf%((9FOcsDiNEp9H5rkB9jLb?#M%PpgP54@gG^n#aNz*pt*L4L{5*%7Q|zjc z-&C(mQL5xIGaq6EA4)uewhiHhUC}Bsh%!UQ2M|;b82N*_Z^oskTU@&II5zeqN@tTM zppC$Mq$2fHJtGAmwZ^7yupWVz#4qX#-6X^%g`1z%nJ{z>V-5L_10-Sxe~1HY7e9Cq z@oGvY>md@Hf4EN>N|D-_nV3}jT$SwKq@w&PzVZ&B0GO4o<$U>>lG(b;ZW#`BiHYyQ zVmK@IWFOlCZV?v(QK7_qV%4A{Brx9yoJT-hv{BrhG&(kR;*KJmqmdR1C{v>MD&LvO zn(zWzTr>}}<{O(~YY1Px2mQ6I29V|RE4u?rQ?78kn-Drgkv$g9_XXz8-d7b!r7Nz# zAmISe3LI~2Ow9EA{Mh|eHP>Te-XbicFz-0%YOt@7mn>O*yHS?xdT9iBlKrdo)wZs0 zi!8}4n<{O=Wp`Q?;sg#`i~y%S|3{)}QW`H`c;x{Y!^->j-(YRR1>G~iCEQ;s=c{1# z6*6(AfmLeJ#_En9s5;@7f<~*ggh&Jly?w%TRp0avyz7mk?0+vHnNUfm4W59SS%T8f;H_S)fi|hlX(Y0R^&1X}5vfR}@6= zpMCc6)hj1VNQPwOC)t*{8Zo4$8_1X@tf-d;9k1F0Ro$pWH$-6z=soRN;@v0K4)(%k z0)P7pS}iYu1iHcMD_Yp@a14+isKl~Qr#(^Qe|~&^%R^O}AxH^ELlMz{%84SyrNqX{ zihjt*gy|?tUTvGa|B6SzMih<8ORkR$DH_b&-z{YW7rIU()T4|(&UCRuYp*L{^lSrS z6M^%|*x~62T23&>kKo<}H1;<75hG#zKei<>a2?A1+B-H7`mo!7rnRLQz5h1IWJ__I z?{lk%n9IP`F4tf`fy2ORE4qJ=`<;E;)DpCX2(iQCV{rwj=>Yu|9O3bUxJhlhaviWj z@UX3~>n{X{Np~DP@)15t2L}iHVQCity~ZOypm{^Vw-ilvOM#bR6w2=G* zXG)h&2#1(?u5Jk1)+W$2U=v`=>$+Y921ZWK2PtkwyGfH%QxK0@wQm*x&juGeqtH%E zf8A(SrH62;kExuMwsA?5|&=;t#?w z=8TeNkYZPA<^xAaIojzWVTAF#phpFhRmEQE9@B+VB#h2Z^>h+1#GSVyFG#vfqfFduYuF4_={CaGpY2!D?=%5{WxsdZ5LGKP?7*E%r zz8vJb{w=A{*sA?M2J6?cyBV8E-#aNMBI1m-+1Lcyk;z?jE#_)!6f9$S$T%Kir-T$K zi|3W+Pi)Tqetzh67Y5h=o^H#7T+p!na4JCm!GP5%42v$s8?P{rHKTUom>x!}0P_zH zHa3?r8z&~`mseKw)1-A;c1~nI*hX>wbxY#I?7+1dsJA;lPN0TC_Ck7uN{stHH9qK7 zp{zj>+jVAF@l)udpoN?5WUY>h+Sc19emf(B@KT8KDP1rPcMk~o3rhWh^PJMLRCXy6 zkKnB5PjsL-lIi>O^qqu+9Q#vP;PhbmWMC+%H$%H#zdW7}Uz*I0MRWA(c!dc5Q&%T{ zdu}d6@lgW z-$-J4!{^jMe1?a=fYDrCoNnXWIlqRe>R^oxX{vP6wD~5eb_#s603}`Z@tIvX2(1&q zV1rGF25ju^C`Mtc2ixDzCd2pmbF5pN(4mo)iBpZtYKk~@m%8f@L%)(>05@|}%&)+; z-+gXm@ylQ>UTimI1_N&E=GH!?eiqwI&+sXp+tb-NJIE{diRJ+JB2cf#A+CzBWk8pS z6zeK~whzawt(64@HCnzRyaO7~E=W}`Qh|ffXmLC;GE4LP`SYe5K(y|04Mai_AbBHv zO?Tk=^AN*ro#9Q!-xy;qjw!wwLN5bG3VdDz24q#aLHfgoa41<~-z{_k-q8=g+v3@H zYzQzlkdQw*Q%^e>Lb`a-3R<97Raxt+_3JEuvkZ1nWW;iRH^+;!XN^Chj(!fAVMuUr zRoe;L?b{zXXkMqmfl@beQ;iA{?T*}eu}@kbdi>^^IMDgSV_^WC5Z078DAExK6&;OZ zBxo&YckB=s6SLbbX#DfD_aGJ`ToCXiX{=%efcLOy*nSjtEl6(6p?`to;IL~i9P?;# zkS)@UMGj-@Utahbb`{-wPr%;c0&xnr(Z8o8tOGwqHF$fY*QHCO4|J`F{fd~WU%yk4 zY}V(CKZYZTZdZIPFuV)BXR{FB4Ly#+v!k?hwq4cTK{3AcLwOv zwJ+0;QZfy^9%P}A{%!5@W8Bx#vmiv5kf7lKL-kr4##<@`+G`m^Q{pi8KD!3ZIC1&2f2MRo%mYGBbO4wpFv-t6!1 zhravWwAaQmlvQY1u#Kk>BwM_yQbbq?Ty+=we7TeZYOrE$<8v|#VHwMXiNhJFb6GhS zmPX&DSa}rcRdz#opCig?^>OQ5Emx=S^kj5$hDliPbH%6Lxl@Ng0?pTpkVp8#MmnK9 z20bt&f9y;j(UmBBe0v9-A*N~J-U=GS&wE>OCY6GfC&tPIdSH|YiYVz(*+8l#2eIme z@|w+-0%PGMY()HiS_pq_+Wy;Z^yqG&XiN%b9XNwZUQRA9@SBr_q-3@KYDLe<;I~j+ zw`QJ0x!E)4R0KcV+plb5Q87XwG5~@m#>GjXSO7UdN**W9IKv<7qO-W^nE)|v!ytWo z9HwBngZJ!^s$tGpgw7zS_gG@WJ*dmDwLADcljHx89oP+iYhSsZ|S4*ayGenK-EfC0Tl!o zFkFZj%^S>Om3E!@O^cA*<^4mPzO?zgy?wJZ^MPYR$MaEn;qyIsaB;yL501>%m7kxV zlA7A`r5ca!z5d}mJ3!B`)J!S(RG7T13IoltPTO6PgxE9pH`+<;gwBFJJrATh&{nT4 z6tX-MJf8XrZuGzy%zPFFD~9Bhl>G4{nmL#!Hp+%NI599~!;G3Mis{kG1DHP+EiIq> z`8g$jgh$doL-$!uRgYdx6RF(L4PhISJZ5;+Fdj{IGDw- zg&*2zaOwd7i2Y>b&^)2Ot21|I{Hno1wgsuc!ZI>EtorP;rnWY1Y1Rb$?%l6RNur@@ z!#J4g*(T{NqspN`T8oQ{Y`9|7d0FxdaLiRejv|VV6=nDUp)q|abTHKEi=6yMD&j5Z6tPB;W47w4E!ZMP+n9;0i7!(+aV=RIKRCD+*# zprbO_4*CMTRXO9>$VeiEHKdA0 z%~SUjTUalu7%h+kzZg+Mp6?A9kpRwxs``m@J@zBGuiDz%cWm2c+dzber1zbvm(pGu!`oLsCH>&K`a7PoST`t)RX#kC-}6q zTgV3t+I}LN!*FT~5CkyO?dP@e8E{sUnukZ(xu0<+D4?)#<3st$^x;6bW>jq37#MtS z=yf^`autSax~S%UF9;BrSW}M-o#4gGRc;oawso*|W>U8K* zpEYBHzED|^BnE|-=1~T;6A*k3AnD6^^_(Q(fAfmQq6+g)Eu=H&ylJUCru&zl?@+Em z?*>UJl-kgL8vJ>P4VL2Vjq)9oF9;`twmev2ES`CZy{`JJ=_#nQ@UtV8hb1p7KLO<{MsnT{Oy^|-sJv>HD{omd`f>LfwVgL7>S7f_uSM;^h z(&qb5xyD3A89PMbgpLKEA9pe{ z2^H3ri)3+N;P@LiA|kR%h@A*r=3!MG)~-K?Z7I}Qx=eze&sgbpRO3hhglkp~6wSOu zKD!RfONNh{p2@G)UJTLSow^BHqVu7(GP{_p4dl22EV`3m&5QBe**Q2c#WFs-#`OBk zu!9l@K!6X0QoeH8^$cI$@moYB7dS*16agPx3V|Sg9aA6{=Pj(i^g_=T{5fgq&*EUA zCYeCt?BqH_F|ow`DZ-G{S5{QyF*I~`N*zA@8zY)p^nV@7lA-a&+ATcsg4 zwGVU*Ad9bWZavovF&ZBpj>qABUaWzfI0h7m3QSLvHH*E30(r@&F*uvMMW`^6iUHe`1+@iEPuOWMY=Il40Ai!}vT#f_#{~Fp+V09qvinK)K z59`Kk=go?W-$0Gc&CGfnFSbPlKX*U0V@Irb{o0d)0>`;8O(;c3ET^A;xSfR}c@J_R zf!3oPf$RP+0m@qB%jH#8dPlz`!gp)5c?@VUR3GXko_AXZig`tS$x7Swq0ax>;}O?26tpdaOi(O59{B_BDwFRh zfK*lmE4sPG#nZK-KwlAWdcS9QHp)q79Z=tOZa4mH`*l~KmqcX$-o5K+d>?1(@~c*L zpNDW5)7gEbnE}6zrOPx@7@Zk3xL~KC#H;BUJ_e-O_39sexCJza?qc)DGzNgfCZ~4& zosn=-*FYPUh!3mn(8Pjlcr)Wi*~QVq|8%jJQDT3_t6{ z3%;enfQ3`PtgPho%QRQJ>;EpSY^T2txS^-F7u6tf$;(Tb3+s%-mT)H}<@DD~^KvH{ zpxDaF^=Pf>_nh8{jSF+|^vQqFg#B8I1k3^&C8&d{9Nh@m9oTz8vziYb6Su6AfKO#X zd2+4d;T(^v?Bx;cV8IU_9PFxK5qLO-N&)%&27ltAzeWj#3m{$6sZ{1C7;uQgG2R=p z0>1%@%t+3gj*eT%cz~-*OA_>33SLs{j5e!7*4`CffCp+U;?k;$T@^ceQ%rDRxvc%e zb%$?={Me~hSch=|i+1<&KI|(n0ivgbqD4fc7V*Hm>C(fG@}wu0qa5D*wqE6P_@V4%3_sPw0}gXAFI+%o!~Dt7Dly( z=#n=|2j59RW1rs}ZBS^uwc;9YkRqsK&;M=-TndOQd@Tw*e4!IZL5}LdkO6gZk!KXD zSs3B-QN4N@FaY~zc1hvhpdutnR3cVY?!4$0k)Za&%nTLYTVQSIqJ_P9aESK1CG>R2 z>#SLwerB_UdIOfioy6hVIFo$Sv;8>y92XASX4(AjcQ}Dv2uFnz*L?8*{@^Vfnoe9T z^isrG-?-v9Y>C6Yaq)<^;P7JQY;1^*AC4Jx<* literal 0 HcmV?d00001 diff --git a/src/assets/user.png b/src/assets/user.png new file mode 100644 index 0000000000000000000000000000000000000000..fbe2d32807865cadee991005a3d4f13c5d06d978 GIT binary patch literal 5205 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 zia + + + + + + diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..6a314c7 --- /dev/null +++ b/src/components/Breadcrumb/index.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/components/CURD/PageContent.vue b/src/components/CURD/PageContent.vue new file mode 100644 index 0000000..c9e470d --- /dev/null +++ b/src/components/CURD/PageContent.vue @@ -0,0 +1,933 @@ + + + + + diff --git a/src/components/CURD/PageModal.vue b/src/components/CURD/PageModal.vue new file mode 100644 index 0000000..152189c --- /dev/null +++ b/src/components/CURD/PageModal.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/src/components/CURD/PageSearch.vue b/src/components/CURD/PageSearch.vue new file mode 100644 index 0000000..32510ec --- /dev/null +++ b/src/components/CURD/PageSearch.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/components/CURD/types.ts b/src/components/CURD/types.ts new file mode 100644 index 0000000..b4a2b16 --- /dev/null +++ b/src/components/CURD/types.ts @@ -0,0 +1,223 @@ +import type { DialogProps, DrawerProps, FormItemRule, PaginationProps } from 'element-plus' +import type { FormProps, TableProps, ColProps, ButtonProps, CardProps } from 'element-plus' +import type PageContent from './PageContent.vue' +import type PageModal from './PageModal.vue' +import type PageSearch from './PageSearch.vue' +import type { CSSProperties } from 'vue' + +export type PageSearchInstance = InstanceType +export type PageContentInstance = InstanceType +export type PageModalInstance = InstanceType + +export type IObject = Record + +type DateComponent = 'date-picker' | 'time-picker' | 'time-select' | 'custom-tag' | 'input-tag' +type InputComponent = 'input' | 'select' | 'input-number' | 'cascader' | 'tree-select' +type OtherComponent = 'text' | 'radio' | 'checkbox' | 'switch' | 'icon-select' | 'custom' +export type ISearchComponent = DateComponent | InputComponent | 'custom' +export type IComponentType = DateComponent | InputComponent | OtherComponent + +type ToolbarLeft = 'add' | 'delete' | 'import' | 'export' +type ToolbarRight = 'refresh' | 'filter' | 'imports' | 'exports' | 'search' +type ToolbarTable = 'edit' | 'view' | 'delete' +export type IToolsButton = { + name: string // 按钮名称 + text?: string // 按钮文本 + perm?: Array | string // 权限标识(可以是完整权限字符串如'sys:user:add'或操作权限如'add') + attrs?: Partial & { style?: CSSProperties } // 按钮属性 + render?: (row: IObject) => boolean // 条件渲染 +} +export type IToolsDefault = ToolbarLeft | ToolbarRight | ToolbarTable | IToolsButton + +export interface IOperateData { + name: string + row: IObject + column: IObject + $index: number +} + +export interface ISearchConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string + // 标签冒号(默认:false) + colon?: boolean + // 表单项(默认:[]) + formItems?: IFormItems + // 是否开启展开和收缩(默认:true) + isExpandable?: boolean + // 默认展示的表单项数量(默认:3) + showNumber?: number + // 卡片属性 + cardAttrs?: Partial & { style?: CSSProperties } + // form组件属性 + form?: IForm + // 自适应网格布局(使用时表单不要添加 style: { width: "200px" }) + grid?: boolean | 'left' | 'right' +} + +export interface IContentConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string + // table组件属性 + table?: Omit, 'data'> + // 分页组件位置(默认:left) + pagePosition?: 'left' | 'right' + // pagination组件属性 + pagination?: + | boolean + | Partial< + Omit< + PaginationProps, + 'v-model:page-size' | 'v-model:current-page' | 'total' | 'currentPage' + > + > + // 列表的网络请求函数(需返回promise) + indexAction: (queryParams: T) => Promise + // 默认的分页相关的请求参数 + request?: { + pageName: string + limitName: string + } + // 数据格式解析的回调函数 + parseData?: (res: any) => { + total: number + list: IObject[] + [key: string]: any + } + // 修改属性的网络请求函数(需返回promise) + modifyAction?: (data: { + [key: string]: any + field: string + value: boolean | string | number + }) => Promise + // 删除的网络请求函数(需返回promise) + deleteAction?: (ids: string) => Promise + // 后端导出的网络请求函数(需返回promise) + exportAction?: (queryParams: T) => Promise + // 前端全量导出的网络请求函数(需返回promise) + exportsAction?: (queryParams: T) => Promise + // 导入模板 + importTemplate?: string | (() => Promise) + // 后端导入的网络请求函数(需返回promise) + importAction?: (file: File) => Promise + // 前端导入的网络请求函数(需返回promise) + importsAction?: (data: IObject[]) => Promise + // 主键名(默认为id) + pk?: string + // 表格工具栏(默认:add,delete,export,也可自定义) + toolbar?: Array + // 表格工具栏右侧图标(默认:refresh,filter,imports,exports,search) + defaultToolbar?: Array + // table组件列属性(额外的属性templet,operat,slotName) + cols: Array<{ + type?: 'default' | 'selection' | 'index' | 'expand' + label?: string + prop?: string + width?: string | number + align?: 'left' | 'center' | 'right' + columnKey?: string + reserveSelection?: boolean + // 列是否显示 + show?: boolean + // 模板 + templet?: + | 'image' + | 'list' + | 'url' + | 'switch' + | 'input' + | 'price' + | 'percent' + | 'icon' + | 'date' + | 'tool' + | 'custom' + // image模板相关参数 + imageWidth?: number + imageHeight?: number + // list模板相关参数 + selectList?: IObject + // switch模板相关参数 + activeValue?: boolean | string | number + inactiveValue?: boolean | string | number + activeText?: string + inactiveText?: string + // input模板相关参数 + inputType?: string + // price模板相关参数 + priceFormat?: string + // date模板相关参数 + dateFormat?: string + // tool模板相关参数 + operat?: Array + // filter值拼接符 + filterJoin?: string + [key: string]: any + // 初始化数据函数 + initFn?: (item: IObject) => void + }> +} + +export interface IModalConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string + // 标签冒号(默认:false) + colon?: boolean + // 主键名(主要用于编辑数据,默认为id) + pk?: string + // 组件类型(默认:dialog) + component?: 'dialog' | 'drawer' + // dialog组件属性 + dialog?: Partial> + // drawer组件属性 + drawer?: Partial> + // form组件属性 + form?: IForm + // 表单项 + formItems: IFormItems + // 提交之前处理 + beforeSubmit?: (data: T) => void + // 提交的网络请求函数(需返回promise) + formAction?: (data: T) => Promise +} + +export type IForm = Partial> + +// 表单项 +export type IFormItems = Array<{ + // 组件类型(如input,select,radio,custom等) + type: T + // 标签提示 + tips?: string | IObject + // 标签文本 + label: string + // 键名 + prop: string + // 组件属性 + attrs?: IObject + // 组件可选项(只适用于select,radio,checkbox组件) + options?: Array<{ label: string; value: any; [key: string]: any }> | Ref + // 验证规则 + rules?: FormItemRule[] + // 初始值 + initialValue?: any + // 插槽名(适用于自定义组件,设置类型为custom) + slotName?: string + // 是否隐藏 + hidden?: boolean + // layout组件Col属性 + col?: Partial + // 组件事件 + events?: Record void> + // 初始化数据函数扩展 + initFn?: (item: IObject) => void +}> + +export interface IPageForm { + // 主键名(主要用于编辑数据,默认为id) + pk?: string + // form组件属性 + form?: IForm + // 表单项 + formItems: IFormItems +} diff --git a/src/components/CURD/usePage.ts b/src/components/CURD/usePage.ts new file mode 100644 index 0000000..2108b6c --- /dev/null +++ b/src/components/CURD/usePage.ts @@ -0,0 +1,105 @@ +import { ref } from 'vue' +import type { IObject, PageContentInstance, PageModalInstance, PageSearchInstance } from './types' + +function usePage() { + const searchRef = ref() + const contentRef = ref() + const addModalRef = ref() + const editModalRef = ref() + + // 搜索 + function handleQueryClick(queryParams: IObject) { + const filterParams = contentRef.value?.getFilterParams() + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true) + } + // 重置 + function handleResetClick(queryParams: IObject) { + const filterParams = contentRef.value?.getFilterParams() + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true) + } + // 新增 + function handleAddClick(RefImpl?: Ref) { + if (RefImpl) { + RefImpl?.value.setModalVisible() + RefImpl?.value.handleDisabled(false) + } else { + addModalRef.value?.setModalVisible() + addModalRef.value?.handleDisabled(false) + } + } + // 编辑 + async function handleEditClick( + row: IObject, + callback?: (result?: IObject) => IObject, + RefImpl?: Ref + ) { + if (RefImpl) { + RefImpl.value?.setModalVisible() + RefImpl.value?.handleDisabled(false) + const from = await (callback?.(row) ?? Promise.resolve(row)) + RefImpl.value?.setFormData(from ? from : row) + } else { + editModalRef.value?.setModalVisible() + editModalRef.value?.handleDisabled(false) + const from = await (callback?.(row) ?? Promise.resolve(row)) + editModalRef.value?.setFormData(from ? from : row) + } + } + // 查看 + async function handleViewClick( + row: IObject, + callback?: (result?: IObject) => IObject, + RefImpl?: Ref + ) { + if (RefImpl) { + RefImpl.value?.setModalVisible() + RefImpl.value?.handleDisabled(true) + const from = await (callback?.(row) ?? Promise.resolve(row)) + RefImpl.value?.setFormData(from ? from : row) + } else { + editModalRef.value?.setModalVisible() + editModalRef.value?.handleDisabled(true) + const from = await (callback?.(row) ?? Promise.resolve(row)) + editModalRef.value?.setFormData(from ? from : row) + } + } + // 表单提交 + function handleSubmitClick() { + //根据检索条件刷新列表数据 + const queryParams = searchRef.value?.getQueryParams() + contentRef.value?.fetchPageData(queryParams, true) + } + // 导出 + function handleExportClick() { + // 根据检索条件导出数据 + const queryParams = searchRef.value?.getQueryParams() + contentRef.value?.exportPageData(queryParams) + } + // 搜索显隐 + function handleSearchClick() { + searchRef.value?.toggleVisible() + } + // 涮选数据 + function handleFilterChange(filterParams: IObject) { + const queryParams = searchRef.value?.getQueryParams() + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true) + } + + return { + searchRef, + contentRef, + addModalRef, + editModalRef, + handleQueryClick, + handleResetClick, + handleAddClick, + handleEditClick, + handleViewClick, + handleSubmitClick, + handleExportClick, + handleSearchClick, + handleFilterChange + } +} + +export default usePage diff --git a/src/components/CommonWrapper/index.vue b/src/components/CommonWrapper/index.vue new file mode 100644 index 0000000..025f837 --- /dev/null +++ b/src/components/CommonWrapper/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/components/DarkModeSwitch/index.vue b/src/components/DarkModeSwitch/index.vue new file mode 100644 index 0000000..01bd8e4 --- /dev/null +++ b/src/components/DarkModeSwitch/index.vue @@ -0,0 +1,39 @@ + + diff --git a/src/components/Fullscreen/index.vue b/src/components/Fullscreen/index.vue new file mode 100644 index 0000000..23a4071 --- /dev/null +++ b/src/components/Fullscreen/index.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue new file mode 100644 index 0000000..4d94bba --- /dev/null +++ b/src/components/Hamburger/index.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/components/LangSelect/index.vue b/src/components/LangSelect/index.vue new file mode 100644 index 0000000..4a03c52 --- /dev/null +++ b/src/components/LangSelect/index.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/Notification/ApprovalForm.vue b/src/components/Notification/ApprovalForm.vue new file mode 100644 index 0000000..f0c710b --- /dev/null +++ b/src/components/Notification/ApprovalForm.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/Notification/ConfirmationOfProxyServiceForm.vue b/src/components/Notification/ConfirmationOfProxyServiceForm.vue new file mode 100644 index 0000000..b5da81b --- /dev/null +++ b/src/components/Notification/ConfirmationOfProxyServiceForm.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/Notification/ViewTheConflictList.vue b/src/components/Notification/ViewTheConflictList.vue new file mode 100644 index 0000000..5cb355a --- /dev/null +++ b/src/components/Notification/ViewTheConflictList.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/src/components/Notification/index.vue b/src/components/Notification/index.vue new file mode 100644 index 0000000..7da4724 --- /dev/null +++ b/src/components/Notification/index.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue new file mode 100644 index 0000000..e854b60 --- /dev/null +++ b/src/components/Pagination/index.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue new file mode 100644 index 0000000..dd2a9fb --- /dev/null +++ b/src/components/SizeSelect/index.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/composables/auth/useTokenRefresh.ts b/src/composables/auth/useTokenRefresh.ts new file mode 100644 index 0000000..2d37880 --- /dev/null +++ b/src/composables/auth/useTokenRefresh.ts @@ -0,0 +1,91 @@ +import type { InternalAxiosRequestConfig } from 'axios' +import { useUserStoreHook } from '@/store/modules/user-store' +import { AuthStorage, redirectToLogin } from '@/utils/auth' + +/** + * 重试请求的回调函数类型 + */ +type RetryCallback = () => void + +/** + * Token刷新组合式函数 + */ +export function useTokenRefresh() { + // Token 刷新相关状态 + let isRefreshingToken = false + const pendingRequests: RetryCallback[] = [] + + /** + * 刷新 Token 并重试请求 + */ + async function refreshTokenAndRetry( + config: InternalAxiosRequestConfig, + httpRequest: any + ): Promise { + return new Promise((resolve, reject) => { + // 封装需要重试的请求 + const retryRequest = () => { + const newToken = AuthStorage.getAccessToken() + if (newToken && config.headers) { + config.headers.Authorization = `Bearer ${newToken}` + } + httpRequest(config).then(resolve).catch(reject) + } + + // 将请求加入等待队列 + pendingRequests.push(retryRequest) + + // 如果没有正在刷新,则开始刷新流程 + if (!isRefreshingToken) { + isRefreshingToken = true + + useUserStoreHook() + .refreshToken() + .then(() => { + // 刷新成功,重试所有等待的请求 + pendingRequests.forEach((callback) => { + try { + callback() + } catch (error) { + console.error('Retry request error:', error) + } + }) + // 清空队列 + pendingRequests.length = 0 + }) + .catch(async (error) => { + console.error('Token refresh failed:', error) + // 刷新失败,先 reject 所有等待的请求,再清空队列 + const failedRequests = [...pendingRequests] + pendingRequests.length = 0 + + // 拒绝所有等待的请求 + failedRequests.forEach(() => { + reject(new Error('Token refresh failed')) + }) + + // 跳转登录页 + await redirectToLogin('登录状态已失效,请重新登录') + }) + .finally(() => { + isRefreshingToken = false + }) + } + }) + } + + /** + * 获取刷新状态(用于外部判断) + */ + function getRefreshStatus() { + return { + isRefreshing: isRefreshingToken, + pendingCount: pendingRequests.length + } + } + + return { + refreshTokenAndRetry, + getRefreshStatus + } +} diff --git a/src/composables/index.ts b/src/composables/index.ts new file mode 100644 index 0000000..185e184 --- /dev/null +++ b/src/composables/index.ts @@ -0,0 +1,14 @@ +export { useStomp } from './websocket/useStomp' +export { useDictSync } from './websocket/useDictSync' +export type { DictMessage } from './websocket/useDictSync' +export { useOnlineCount } from './websocket/useOnlineCount' +export { useTokenRefresh } from './auth/useTokenRefresh' + +export { useLayout } from './layout/useLayout' +export { useLayoutMenu } from './layout/useLayoutMenu' +export { useDeviceDetection } from './layout/useDeviceDetection' + +export { useAiAction } from './useAiAction' +export type { UseAiActionOptions, AiActionHandler } from './useAiAction' + +export { useTableSelection } from './useTableSelection' diff --git a/src/composables/layout/useDeviceDetection.ts b/src/composables/layout/useDeviceDetection.ts new file mode 100644 index 0000000..b34b5cf --- /dev/null +++ b/src/composables/layout/useDeviceDetection.ts @@ -0,0 +1,40 @@ +import { watchEffect, computed } from 'vue' +import { useWindowSize } from '@vueuse/core' +import { useAppStore } from '@/store' +import { DeviceEnum } from '@/enums/settings/device-enum' + +/** + * 设备检测和响应式处理 + * 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态 + */ +export function useDeviceDetection() { + const appStore = useAppStore() + const { width } = useWindowSize() + + // 桌面设备断点 + const DESKTOP_BREAKPOINT = 992 + + // 计算设备类型 + const isDesktop = computed(() => width.value >= DESKTOP_BREAKPOINT) + const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE) + + // 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态 + watchEffect(() => { + const deviceType = isDesktop.value ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE + + // 更新设备类型 + appStore.toggleDevice(deviceType) + + // 根据设备类型调整侧边栏状态 + if (isDesktop.value) { + appStore.openSideBar() + } else { + appStore.closeSideBar() + } + }) + + return { + isDesktop, + isMobile + } +} diff --git a/src/composables/layout/useLayout.ts b/src/composables/layout/useLayout.ts new file mode 100644 index 0000000..f730938 --- /dev/null +++ b/src/composables/layout/useLayout.ts @@ -0,0 +1,62 @@ +import { useAppStore, useSettingsStore } from '@/store' +import { defaultSettings } from '@/settings' + +/** + * 布局相关的通用逻辑 + */ +export function useLayout() { + const appStore = useAppStore() + const settingsStore = useSettingsStore() + + // 计算当前布局模式 + const currentLayout = computed(() => settingsStore.layout) + + // 侧边栏展开状态 + const isSidebarOpen = computed(() => appStore.sidebar.opened) + + // 是否显示标签视图 + const isShowTagsView = computed(() => settingsStore.showTagsView) + + // 是否显示设置面板 + const isShowSettings = computed(() => defaultSettings.showSettings) + + // 是否显示Logo + const isShowLogo = computed(() => settingsStore.showAppLogo) + + // 是否移动设备 + const isMobile = computed(() => appStore.device === 'mobile') + + // 布局CSS类 + const layoutClass = computed(() => ({ + hideSidebar: !appStore.sidebar.opened, + openSidebar: appStore.sidebar.opened, + mobile: appStore.device === 'mobile', + [`layout-${settingsStore.layout}`]: true + })) + + /** + * 处理切换侧边栏的展开/收起状态 + */ + function toggleSidebar() { + appStore.toggleSidebar() + } + + /** + * 关闭侧边栏(移动端) + */ + function closeSidebar() { + appStore.closeSideBar() + } + + return { + currentLayout, + isSidebarOpen, + isShowTagsView, + isShowSettings, + isShowLogo, + isMobile, + layoutClass, + toggleSidebar, + closeSidebar + } +} diff --git a/src/composables/layout/useLayoutMenu.ts b/src/composables/layout/useLayoutMenu.ts new file mode 100644 index 0000000..bb15424 --- /dev/null +++ b/src/composables/layout/useLayoutMenu.ts @@ -0,0 +1,39 @@ +import { useRoute } from 'vue-router' +import { useAppStore, usePermissionStore } from '@/store' + +/** + * 布局菜单处理逻辑 + */ +export function useLayoutMenu() { + const route = useRoute() + const appStore = useAppStore() + const permissionStore = usePermissionStore() + + // 顶部菜单激活路径 + const activeTopMenuPath = computed(() => appStore.activeTopMenuPath) + + // 常规路由(左侧菜单或顶部菜单) + const routes = computed(() => permissionStore.routes) + + // 混合布局左侧菜单路由 + const sideMenuRoutes = computed(() => permissionStore.mixLayoutSideMenus) + + // 当前激活的菜单 + const activeMenu = computed(() => { + const { meta, path } = route + + // 如果设置了activeMenu,则使用 + if (meta?.activeMenu) { + return meta.activeMenu + } + + return path + }) + + return { + routes, + sideMenuRoutes, + activeMenu, + activeTopMenuPath + } +} diff --git a/src/composables/useAiAction.ts b/src/composables/useAiAction.ts new file mode 100644 index 0000000..ca9b1f3 --- /dev/null +++ b/src/composables/useAiAction.ts @@ -0,0 +1,270 @@ +import { useRoute } from 'vue-router' +import { ElMessage, ElMessageBox } from 'element-plus' +import { onMounted, onBeforeUnmount, nextTick } from 'vue' +import AiCommandApi from '@/api/ai' + +/** + * AI 操作处理器(简化版) + * + * 可以是简单函数,也可以是配置对象 + */ +export type AiActionHandler = + | ((args: T) => Promise | void) + | { + /** 执行函数 */ + execute: (args: T) => Promise | void + /** 是否需要确认(默认 true) */ + needConfirm?: boolean + /** 确认消息(支持函数或字符串) */ + confirmMessage?: string | ((args: T) => string) + /** 成功消息(支持函数或字符串) */ + successMessage?: string | ((args: T) => string) + /** 是否调用后端 API(默认 false,如果为 true 则自动调用 executeCommand) */ + callBackendApi?: boolean + } + +/** + * AI 操作配置 + */ +export interface UseAiActionOptions { + /** 操作映射表:函数名 -> 处理器 */ + actionHandlers?: Record + /** 数据刷新函数(操作完成后调用) */ + onRefresh?: () => Promise | void + /** 自动搜索处理函数 */ + onAutoSearch?: (keywords: string) => void + /** 当前路由路径(用于执行命令时传递) */ + currentRoute?: string +} + +/** + * AI 操作 Composable + * + * 统一处理 AI 助手传递的操作,支持: + * - 自动搜索(通过 keywords + autoSearch 参数) + * - 执行 AI 操作(通过 aiAction 参数) + * - 配置化的操作处理器 + */ +export function useAiAction(options: UseAiActionOptions = {}) { + const route = useRoute() + const { actionHandlers = {}, onRefresh, onAutoSearch, currentRoute = route.path } = options + + // 用于跟踪是否已卸载,防止在卸载后执行回调 + let isUnmounted = false + + /** + * 执行 AI 操作(统一处理确认、执行、反馈流程) + */ + async function executeAiAction(action: any) { + if (isUnmounted) return + + // 兼容两种入参:{ functionName, arguments } 或 { functionCall: { name, arguments } } + const fnCall = action.functionCall ?? { + name: action.functionName, + arguments: action.arguments + } + + if (!fnCall?.name) { + ElMessage.warning('未识别的 AI 操作') + return + } + + // 查找对应的处理器 + const handler = actionHandlers[fnCall.name] + if (!handler) { + ElMessage.warning(`暂不支持操作: ${fnCall.name}`) + return + } + + try { + // 判断处理器类型(函数 or 配置对象) + const isSimpleFunction = typeof handler === 'function' + + if (isSimpleFunction) { + // 简单函数形式:直接执行 + await handler(fnCall.arguments) + } else { + // 配置对象形式:统一处理确认、执行、反馈 + const config = handler + + // 1. 确认阶段(默认需要确认) + if (config.needConfirm !== false) { + const confirmMsg = + typeof config.confirmMessage === 'function' + ? config.confirmMessage(fnCall.arguments) + : config.confirmMessage || '确认执行此操作吗?' + + await ElMessageBox.confirm(confirmMsg, 'AI 助手操作确认', { + confirmButtonText: '确认执行', + cancelButtonText: '取消', + type: 'warning', + dangerouslyUseHTMLString: true + }) + } + + // 2. 执行阶段 + if (config.callBackendApi) { + // 自动调用后端 API + await AiCommandApi.executeCommand({ + originalCommand: action.originalCommand || '', + confirmMode: 'manual', + userConfirmed: true, + currentRoute, + functionCall: { + name: fnCall.name, + arguments: fnCall.arguments + } + }) + } else { + // 执行自定义函数 + await config.execute(fnCall.arguments) + } + + // 3. 成功反馈 + const successMsg = + typeof config.successMessage === 'function' + ? config.successMessage(fnCall.arguments) + : config.successMessage || '操作执行成功' + ElMessage.success(successMsg) + } + + // 4. 刷新数据 + if (onRefresh) { + await onRefresh() + } + } catch (error: any) { + // 处理取消操作 + if (error === 'cancel') { + ElMessage.info('已取消操作') + return + } + + console.error('AI 操作执行失败:', error) + ElMessage.error(error.message || '操作执行失败') + } + } + + /** + * 执行后端命令(通用方法) + */ + async function executeCommand( + functionName: string, + args: any, + options: { + originalCommand?: string + confirmMode?: 'auto' | 'manual' + needConfirm?: boolean + confirmMessage?: string + } = {} + ) { + const { + originalCommand = '', + confirmMode = 'manual', + needConfirm = false, + confirmMessage + } = options + + // 如果需要确认,先显示确认对话框 + if (needConfirm && confirmMessage) { + try { + await ElMessageBox.confirm(confirmMessage, 'AI 助手操作确认', { + confirmButtonText: '确认执行', + cancelButtonText: '取消', + type: 'warning', + dangerouslyUseHTMLString: true + }) + } catch { + ElMessage.info('已取消操作') + return + } + } + + try { + await AiCommandApi.executeCommand({ + originalCommand, + confirmMode, + userConfirmed: true, + currentRoute, + functionCall: { + name: functionName, + arguments: args + } + }) + + ElMessage.success('操作执行成功') + } catch (error: any) { + if (error !== 'cancel') { + throw error + } + } + } + + /** + * 处理自动搜索 + */ + function handleAutoSearch(keywords: string) { + if (onAutoSearch) { + onAutoSearch(keywords) + } else { + ElMessage.info(`AI 助手已为您自动搜索:${keywords}`) + } + } + + /** + * 初始化:处理 URL 参数中的 AI 操作 + * + * 注意:此方法只处理 AI 相关参数,不负责页面数据的初始加载 + * 页面数据加载应由组件的 onMounted 钩子自行处理 + */ + async function init() { + if (isUnmounted) return + + // 检查是否有 AI 助手传递的参数 + const keywords = route.query.keywords as string + const autoSearch = route.query.autoSearch as string + const aiActionParam = route.query.aiAction as string + + // 如果没有任何 AI 参数,直接返回 + if (!keywords && !autoSearch && !aiActionParam) { + return + } + + // 在 nextTick 中执行,确保页面数据已加载 + nextTick(async () => { + if (isUnmounted) return + + // 1. 处理自动搜索 + if (autoSearch === 'true' && keywords) { + handleAutoSearch(keywords) + } + + // 2. 处理 AI 操作 + if (aiActionParam) { + try { + const aiAction = JSON.parse(decodeURIComponent(aiActionParam)) + await executeAiAction(aiAction) + } catch (error) { + console.error('解析 AI 操作失败:', error) + ElMessage.error('AI 操作参数解析失败') + } + } + }) + } + + // 组件挂载时自动初始化 + onMounted(() => { + init() + }) + + // 组件卸载时清理 + onBeforeUnmount(() => { + isUnmounted = true + }) + + return { + executeAiAction, + executeCommand, + handleAutoSearch, + init + } +} diff --git a/src/composables/useTableSelection.ts b/src/composables/useTableSelection.ts new file mode 100644 index 0000000..52e9719 --- /dev/null +++ b/src/composables/useTableSelection.ts @@ -0,0 +1,63 @@ +import { computed, ref } from 'vue' + +/** + * 表格行选择 Composable + * + * @description 提供统一的表格行选择逻辑,包括选中ID管理和清空选择 + * @template T 数据项类型,必须包含 id 属性 + * @returns 返回选中的ID列表、选择变化处理函数、清空选择函数 + * + * @example + * ```typescript + * const { selectedIds, handleSelectionChange, clearSelection } = useTableSelection(); + * ``` + */ +export function useTableSelection() { + /** + * 选中的数据项ID列表 + */ + const selectedIds = ref<(string | number)[]>([]) + + /** + * 表格选中项变化处理 + * @param selection 选中的行数据列表 + */ + function handleSelectionChange(selection: T[]): void { + selectedIds.value = selection.map((item) => item.id) + } + + /** + * 清空选择 + */ + function clearSelection(): void { + selectedIds.value = [] + } + + /** + * 检查指定ID是否被选中 + * @param id 要检查的ID + * @returns 是否被选中 + */ + function isSelected(id: string | number): boolean { + return selectedIds.value.includes(id) + } + + /** + * 获取选中的数量 + */ + const selectedCount = computed(() => selectedIds.value.length) + + /** + * 是否有选中项 + */ + const hasSelection = computed(() => selectedIds.value.length > 0) + + return { + selectedIds, + selectedCount, + hasSelection, + handleSelectionChange, + clearSelection, + isSelected + } +} diff --git a/src/composables/websocket/useDictSync.ts b/src/composables/websocket/useDictSync.ts new file mode 100644 index 0000000..2748972 --- /dev/null +++ b/src/composables/websocket/useDictSync.ts @@ -0,0 +1,205 @@ +import { useDictStoreHook } from '@/store/modules/dict-store' +import { useStomp } from './useStomp' +import type { IMessage } from '@stomp/stompjs' + +/** + * 字典变更消息结构 + */ +export interface DictChangeMessage { + /** 字典编码 */ + dictCode: string + /** 时间戳 */ + timestamp: number +} + +/** + * 字典消息别名(向后兼容) + */ +export type DictMessage = DictChangeMessage + +/** + * 字典变更事件回调函数类型 + */ +export type DictChangeCallback = (message: DictChangeMessage) => void + +/** + * 全局单例实例 + */ +let singletonInstance: ReturnType | null = null + +/** + * 创建字典同步组合式函数(内部工厂函数) + */ +function createDictSyncComposable() { + const dictStore = useDictStoreHook() + + // 使用优化后的 useStomp + const stomp = useStomp({ + reconnectDelay: 20000, + connectionTimeout: 15000, + useExponentialBackoff: false, + maxReconnectAttempts: 3, + autoRestoreSubscriptions: true, // 自动恢复订阅 + debug: false + }) + + // 字典主题地址 + const DICT_TOPIC = '/topic/dict' + + // 消息回调函数列表 + const messageCallbacks = ref([]) + + // 订阅 ID(用于取消订阅) + let subscriptionId: string | null = null + + /** + * 处理字典变更事件 + */ + const handleDictChangeMessage = (message: IMessage) => { + if (!message.body) { + return + } + + try { + const data = JSON.parse(message.body) as DictChangeMessage + const { dictCode } = data + + if (!dictCode) { + console.warn('[DictSync] 收到无效的字典变更消息:缺少 dictCode') + return + } + + console.log(`[DictSync] 字典 "${dictCode}" 已更新,清除本地缓存`) + + // 清除缓存,等待按需加载 + dictStore.removeDictItem(dictCode) + + // 执行所有注册的回调函数 + messageCallbacks.value.forEach((callback) => { + try { + callback(data) + } catch (error) { + console.error('[DictSync] 回调函数执行失败:', error) + } + }) + } catch (error) { + console.error('[DictSync] 解析字典变更消息失败:', error) + } + } + + /** + * 初始化 WebSocket 连接并订阅字典主题 + */ + const initialize = () => { + // 检查是否配置了 WebSocket 端点 + const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT + if (!wsEndpoint) { + console.log('[DictSync] 未配置 WebSocket 端点,跳过字典同步功能') + return + } + + console.log('[DictSync] 初始化字典同步服务...') + + // 建立 WebSocket 连接 + stomp.connect() + + // 订阅字典主题(useStomp 会自动处理重连后的订阅恢复) + subscriptionId = stomp.subscribe(DICT_TOPIC, handleDictChangeMessage) + + if (subscriptionId) { + console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`) + } else { + console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`) + } + } + + /** + * 关闭 WebSocket 连接并清理资源 + */ + const cleanup = () => { + console.log('[DictSync] 清理字典同步服务...') + + // 取消订阅(如果有的话) + if (subscriptionId) { + stomp.unsubscribe(subscriptionId) + subscriptionId = null + } + + // 也可以通过主题地址取消订阅 + stomp.unsubscribeDestination(DICT_TOPIC) + + // 断开连接 + stomp.disconnect() + + // 清空回调列表 + messageCallbacks.value = [] + } + + /** + * 注册字典变更回调函数 + * + * @param callback 回调函数 + * @returns 返回一个取消注册的函数 + */ + const onDictChange = (callback: DictChangeCallback) => { + messageCallbacks.value.push(callback) + + // 返回取消注册的函数 + return () => { + const index = messageCallbacks.value.indexOf(callback) + if (index !== -1) { + messageCallbacks.value.splice(index, 1) + } + } + } + + return { + // 状态 + isConnected: stomp.isConnected, + connectionState: stomp.connectionState, + + // 方法 + initialize, + cleanup, + onDictChange, + + // 别名方法(向后兼容) + initWebSocket: initialize, + closeWebSocket: cleanup, + onDictMessage: onDictChange, + + // 用于测试和调试 + handleDictChangeMessage + } +} + +/** + * 字典同步组合式函数(单例模式) + * + * 用于监听后端字典变更并自动同步到前端缓存 + * + * @example + * ```ts + * const dictSync = useDictSync(); + * + * // 初始化(在应用启动时调用) + * dictSync.initialize(); + * + * // 注册回调 + * const unsubscribe = dictSync.onDictChange((message) => { + * console.log('字典已更新:', message.dictCode); + * }); + * + * // 取消注册 + * unsubscribe(); + * + * // 清理(在应用退出时调用) + * dictSync.cleanup(); + * ``` + */ +export function useDictSync() { + if (!singletonInstance) { + singletonInstance = createDictSyncComposable() + } + return singletonInstance +} diff --git a/src/composables/websocket/useOnlineCount.ts b/src/composables/websocket/useOnlineCount.ts new file mode 100644 index 0000000..50127ff --- /dev/null +++ b/src/composables/websocket/useOnlineCount.ts @@ -0,0 +1,217 @@ +import { ref, watch, onMounted, onUnmounted, getCurrentInstance } from 'vue' +import { useStomp } from './useStomp' +import { registerWebSocketInstance } from '@/plugins/websocket' +import { AuthStorage } from '@/utils/auth' + +/** + * 在线用户数量消息结构 + */ +interface OnlineCountMessage { + count?: number + timestamp?: number +} + +/** + * 全局单例实例 + */ +let globalInstance: ReturnType | null = null + +/** + * 创建在线用户计数组合式函数(内部工厂函数) + */ +function createOnlineCountComposable() { + // ==================== 状态管理 ==================== + const onlineUserCount = ref(0) + const lastUpdateTime = ref(0) + + // ==================== WebSocket 客户端 ==================== + const stomp = useStomp({ + reconnectDelay: 15000, + maxReconnectAttempts: 3, + connectionTimeout: 10000, + useExponentialBackoff: true, + autoRestoreSubscriptions: true, // 自动恢复订阅 + debug: false + }) + + // 在线用户计数主题 + const ONLINE_COUNT_TOPIC = '/topic/online-count' + + // 订阅 ID + let subscriptionId: string | null = null + + // 注册到全局实例管理器 + registerWebSocketInstance('onlineCount', stomp) + + /** + * 处理在线用户数量消息 + */ + const handleOnlineCountMessage = (message: any) => { + try { + const data = message.body + const jsonData = JSON.parse(data) as OnlineCountMessage + + // 支持两种消息格式 + // 1. 直接是数字: 42 + // 2. 对象格式: { count: 42, timestamp: 1234567890 } + const count = typeof jsonData === 'number' ? jsonData : jsonData.count + + if (count !== undefined && !isNaN(count)) { + onlineUserCount.value = count + lastUpdateTime.value = Date.now() + console.log(`[useOnlineCount] 在线用户数更新: ${count}`) + } else { + console.warn('[useOnlineCount] 收到无效的在线用户数:', data) + } + } catch (error) { + console.error('[useOnlineCount] 解析在线用户数失败:', error) + } + } + + /** + * 订阅在线用户计数主题 + */ + const subscribeToOnlineCount = () => { + if (subscriptionId) { + console.log('[useOnlineCount] 已存在订阅,跳过') + return + } + + // 订阅在线用户计数主题(useStomp 会处理重连后的订阅恢复) + subscriptionId = stomp.subscribe(ONLINE_COUNT_TOPIC, handleOnlineCountMessage) + + if (subscriptionId) { + console.log(`[useOnlineCount] 已订阅主题: ${ONLINE_COUNT_TOPIC}`) + } else { + console.log(`[useOnlineCount] 暂存订阅配置,等待连接建立后自动订阅`) + } + } + + /** + * 初始化 WebSocket 连接并订阅在线用户主题 + */ + const initialize = () => { + // 检查 WebSocket 端点是否配置 + const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT + if (!wsEndpoint) { + console.log('[useOnlineCount] 未配置 WebSocket 端点,跳过初始化') + return + } + + // 检查令牌有效性 + const accessToken = AuthStorage.getAccessToken() + if (!accessToken) { + console.log('[useOnlineCount] 未检测到有效令牌,跳过初始化') + return + } + + console.log('[useOnlineCount] 初始化在线用户计数服务...') + + // 建立 WebSocket 连接 + stomp.connect() + + // 订阅主题 + subscribeToOnlineCount() + } + + /** + * 关闭 WebSocket 连接并清理资源 + */ + const cleanup = () => { + console.log('[useOnlineCount] 清理在线用户计数服务...') + + // 取消订阅 + if (subscriptionId) { + stomp.unsubscribe(subscriptionId) + subscriptionId = null + } + + // 也可以通过主题地址取消订阅 + stomp.unsubscribeDestination(ONLINE_COUNT_TOPIC) + + // 断开连接 + stomp.disconnect() + + // 重置状态 + onlineUserCount.value = 0 + lastUpdateTime.value = 0 + } + + // 监听连接状态变化 + watch( + stomp.isConnected, + (connected) => { + if (connected) { + console.log('[useOnlineCount] WebSocket 已连接') + } else { + console.log('[useOnlineCount] WebSocket 已断开') + } + }, + { immediate: false } + ) + + return { + // 状态 + onlineUserCount: readonly(onlineUserCount), + lastUpdateTime: readonly(lastUpdateTime), + isConnected: stomp.isConnected, + connectionState: stomp.connectionState, + + // 方法 + initialize, + cleanup, + + // 别名方法(向后兼容) + initWebSocket: initialize, + closeWebSocket: cleanup + } +} + +/** + * 在线用户计数组合式函数(单例模式) + * + * 用于实时显示系统在线用户数量 + * + * @param options 配置选项 + * @param options.autoInit 是否在组件挂载时自动初始化(默认 true) + * + * @example + * ```ts + * // 在组件中使用 + * const { onlineUserCount, isConnected } = useOnlineCount(); + * + * // 手动控制初始化 + * const { onlineUserCount, initialize, cleanup } = useOnlineCount({ autoInit: false }); + * onMounted(() => initialize()); + * onUnmounted(() => cleanup()); + * ``` + */ +export function useOnlineCount(options: { autoInit?: boolean } = {}) { + const { autoInit = true } = options + + // 获取或创建单例实例 + if (!globalInstance) { + globalInstance = createOnlineCountComposable() + } + + // 只在组件上下文中且 autoInit 为 true 时使用生命周期钩子 + const instance = getCurrentInstance() + if (autoInit && instance) { + onMounted(() => { + // 只有在未连接时才尝试初始化 + if (!globalInstance!.isConnected.value) { + console.log('[useOnlineCount] 组件挂载,初始化 WebSocket 连接') + globalInstance!.initialize() + } else { + console.log('[useOnlineCount] WebSocket 已连接,跳过初始化') + } + }) + + // 注意:不在卸载时关闭连接,保持全局连接 + onUnmounted(() => { + console.log('[useOnlineCount] 组件卸载(保持 WebSocket 连接)') + }) + } + + return globalInstance +} diff --git a/src/composables/websocket/useStomp.ts b/src/composables/websocket/useStomp.ts new file mode 100644 index 0000000..1a6a7fe --- /dev/null +++ b/src/composables/websocket/useStomp.ts @@ -0,0 +1,530 @@ +import { Client, type IMessage, type StompSubscription } from '@stomp/stompjs' +import { AuthStorage } from '@/utils/auth' + +export interface UseStompOptions { + /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */ + brokerURL?: string + /** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */ + token?: string + /** 重连延迟,单位毫秒,默认为 15000 */ + reconnectDelay?: number + /** 连接超时时间,单位毫秒,默认为 10000 */ + connectionTimeout?: number + /** 是否开启指数退避重连策略 */ + useExponentialBackoff?: boolean + /** 最大重连次数,默认为 3 */ + maxReconnectAttempts?: number + /** 最大重连延迟,单位毫秒,默认为 60000 */ + maxReconnectDelay?: number + /** 是否开启调试日志 */ + debug?: boolean + /** 是否在重连时自动恢复订阅,默认为 true */ + autoRestoreSubscriptions?: boolean +} + +/** + * 订阅配置信息 + */ +interface SubscriptionConfig { + destination: string + callback: (message: IMessage) => void +} + +/** + * 连接状态枚举 + */ +enum ConnectionState { + DISCONNECTED = 'DISCONNECTED', + CONNECTING = 'CONNECTING', + CONNECTED = 'CONNECTED', + RECONNECTING = 'RECONNECTING' +} + +/** + * STOMP WebSocket 连接管理组合式函数 + * + * 核心功能: + * - 自动连接管理(连接、断开、重连) + * - 订阅管理(订阅、取消订阅、自动恢复) + * - 心跳检测 + * - Token 自动刷新 + * + * @param options 配置选项 + * @returns STOMP 客户端操作接口 + */ +export function useStomp(options: UseStompOptions = {}) { + // ==================== 配置初始化 ==================== + const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || '' + + const config = { + brokerURL: ref(options.brokerURL ?? defaultBrokerURL), + reconnectDelay: options.reconnectDelay ?? 15000, + connectionTimeout: options.connectionTimeout ?? 10000, + useExponentialBackoff: options.useExponentialBackoff ?? false, + maxReconnectAttempts: options.maxReconnectAttempts ?? 3, + maxReconnectDelay: options.maxReconnectDelay ?? 60000, + autoRestoreSubscriptions: options.autoRestoreSubscriptions ?? true, + debug: options.debug ?? false + } + + // ==================== 状态管理 ==================== + const connectionState = ref(ConnectionState.DISCONNECTED) + const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED) + const reconnectAttempts = ref(0) + + // ==================== 定时器管理 ==================== + let reconnectTimer: ReturnType | null = null + let connectionTimeoutTimer: ReturnType | null = null + + // ==================== 订阅管理 ==================== + // 活动订阅:存储当前 STOMP 订阅对象 + const activeSubscriptions = new Map() + // 订阅配置注册表:用于自动恢复订阅 + const subscriptionRegistry = new Map() + + // ==================== 客户端实例 ==================== + const stompClient = ref(null) + let isManualDisconnect = false + + // ==================== 工具函数 ==================== + + /** + * 清理所有定时器 + */ + const clearAllTimers = () => { + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = null + } + if (connectionTimeoutTimer) { + clearTimeout(connectionTimeoutTimer) + connectionTimeoutTimer = null + } + } + + /** + * 日志输出(支持调试模式控制) + */ + const log = (...args: any[]) => { + if (config.debug) { + console.log('[useStomp]', ...args) + } + } + + const logWarn = (...args: any[]) => { + console.warn('[useStomp]', ...args) + } + + const logError = (...args: any[]) => { + console.error('[useStomp]', ...args) + } + + /** + * 恢复所有订阅 + */ + const restoreSubscriptions = () => { + if (!config.autoRestoreSubscriptions || subscriptionRegistry.size === 0) { + return + } + + log(`开始恢复 ${subscriptionRegistry.size} 个订阅...`) + + for (const [destination, subscriptionConfig] of subscriptionRegistry.entries()) { + try { + performSubscribe(destination, subscriptionConfig.callback) + } catch (error) { + logError(`恢复订阅 ${destination} 失败:`, error) + } + } + } + + /** + * 初始化 STOMP 客户端 + */ + const initializeClient = () => { + // 如果客户端已存在且处于活动状态,直接返回 + if (stompClient.value && (stompClient.value.active || stompClient.value.connected)) { + log('STOMP 客户端已存在且处于活动状态,跳过初始化') + return + } + + // 检查 WebSocket 端点是否配置 + if (!config.brokerURL.value) { + logWarn('WebSocket 连接失败: 未配置 WebSocket 端点 URL') + return + } + + // 每次连接前重新获取最新令牌 + const accessToken = AuthStorage.getAccessToken() + if (!accessToken) { + logWarn('WebSocket 连接失败:授权令牌为空,请先登录') + return + } + + // 清理旧客户端 + if (stompClient.value) { + try { + stompClient.value.deactivate() + } catch (error) { + logWarn('清理旧客户端时出错:', error) + } + stompClient.value = null + } + + // 创建 STOMP 客户端 + stompClient.value = new Client({ + brokerURL: config.brokerURL.value, + connectHeaders: { + Authorization: `Bearer ${accessToken}` + }, + debug: config.debug ? (msg) => console.log('[STOMP]', msg) : () => {}, + reconnectDelay: 0, // 禁用内置重连,使用自定义重连逻辑 + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000 + }) + + // ==================== 事件监听器 ==================== + + // 连接成功 + stompClient.value.onConnect = () => { + connectionState.value = ConnectionState.CONNECTED + reconnectAttempts.value = 0 + clearAllTimers() + + log('✅ WebSocket 连接已建立') + + // 自动恢复订阅 + restoreSubscriptions() + } + + // 连接断开 + stompClient.value.onDisconnect = () => { + connectionState.value = ConnectionState.DISCONNECTED + log('❌ WebSocket 连接已断开') + + // 清空活动订阅(但保留订阅配置用于恢复) + activeSubscriptions.clear() + + // 如果不是手动断开且未达到最大重连次数,则尝试重连 + if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) { + scheduleReconnect() + } + } + + // WebSocket 关闭 + stompClient.value.onWebSocketClose = (event) => { + connectionState.value = ConnectionState.DISCONNECTED + log(`WebSocket 已关闭: code=${event?.code}, reason=${event?.reason}`) + + // 如果是手动断开,不重连 + if (isManualDisconnect) { + log('手动断开连接,不进行重连') + return + } + + // 对于异常关闭,尝试重连 + if ( + event?.code && + [1000, 1006, 1008, 1011].includes(event.code) && + reconnectAttempts.value < config.maxReconnectAttempts + ) { + log('检测到连接异常关闭,将尝试重连') + scheduleReconnect() + } + } + + // STOMP 错误 + stompClient.value.onStompError = (frame) => { + logError('STOMP 错误:', frame.headers, frame.body) + connectionState.value = ConnectionState.DISCONNECTED + + // 检查是否是授权错误 + const isAuthError = + frame.headers?.message?.includes('Unauthorized') || + frame.body?.includes('Unauthorized') || + frame.body?.includes('Token') || + frame.body?.includes('401') + + if (isAuthError) { + logWarn('WebSocket 授权错误,停止重连') + isManualDisconnect = true // 授权错误不进行重连 + } + } + } + + /** + * 调度重连任务 + */ + const scheduleReconnect = () => { + // 如果正在连接或手动断开,不重连 + if (connectionState.value === ConnectionState.CONNECTING || isManualDisconnect) { + return + } + + // 检查是否达到最大重连次数 + if (reconnectAttempts.value >= config.maxReconnectAttempts) { + logError(`已达到最大重连次数 (${config.maxReconnectAttempts}),停止重连`) + return + } + + reconnectAttempts.value++ + connectionState.value = ConnectionState.RECONNECTING + + // 计算重连延迟(支持指数退避) + const delay = config.useExponentialBackoff + ? Math.min( + config.reconnectDelay * Math.pow(2, reconnectAttempts.value - 1), + config.maxReconnectDelay + ) + : config.reconnectDelay + + log(`准备重连 (${reconnectAttempts.value}/${config.maxReconnectAttempts}),延迟 ${delay}ms`) + + // 清除之前的重连计时器 + if (reconnectTimer) { + clearTimeout(reconnectTimer) + } + + // 设置重连计时器 + reconnectTimer = setTimeout(() => { + if (connectionState.value !== ConnectionState.CONNECTED && !isManualDisconnect) { + log(`开始第 ${reconnectAttempts.value} 次重连...`) + connect() + } + }, delay) + } + + // 监听 brokerURL 的变化,自动重新初始化 + watch(config.brokerURL, (newURL, oldURL) => { + if (newURL !== oldURL) { + log(`WebSocket 端点已更改: ${oldURL} -> ${newURL}`) + + // 断开当前连接 + if (stompClient.value && stompClient.value.connected) { + stompClient.value.deactivate() + } + + // 重新初始化客户端 + initializeClient() + } + }) + + // 初始化客户端 + initializeClient() + + // ==================== 公共接口 ==================== + + /** + * 建立 WebSocket 连接 + */ + const connect = () => { + // 重置手动断开标志 + isManualDisconnect = false + + // 检查是否配置了 WebSocket 端点 + if (!config.brokerURL.value) { + logError('WebSocket 连接失败: 未配置 WebSocket 端点 URL') + return + } + + // 防止重复连接 + if (connectionState.value === ConnectionState.CONNECTING) { + log('WebSocket 正在连接中,跳过重复连接请求') + return + } + + // 如果客户端不存在,先初始化 + if (!stompClient.value) { + initializeClient() + } + + if (!stompClient.value) { + logError('STOMP 客户端初始化失败') + return + } + + // 避免重复连接:检查是否已连接 + if (stompClient.value.connected) { + log('WebSocket 已连接,跳过重复连接') + connectionState.value = ConnectionState.CONNECTED + return + } + + // 设置连接状态 + connectionState.value = ConnectionState.CONNECTING + + // 设置连接超时 + if (connectionTimeoutTimer) { + clearTimeout(connectionTimeoutTimer) + } + + connectionTimeoutTimer = setTimeout(() => { + if (connectionState.value === ConnectionState.CONNECTING) { + logWarn('WebSocket 连接超时') + connectionState.value = ConnectionState.DISCONNECTED + + // 超时后尝试重连 + if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) { + scheduleReconnect() + } + } + }, config.connectionTimeout) + + try { + stompClient.value.activate() + log('正在建立 WebSocket 连接...') + } catch (error) { + logError('激活 WebSocket 连接失败:', error) + connectionState.value = ConnectionState.DISCONNECTED + } + } + + /** + * 执行订阅操作(内部方法) + */ + const performSubscribe = (destination: string, callback: (message: IMessage) => void): string => { + if (!stompClient.value || !stompClient.value.connected) { + logWarn(`尝试订阅 ${destination} 失败: 客户端未连接`) + return '' + } + + try { + const subscription = stompClient.value.subscribe(destination, callback) + const subscriptionId = subscription.id + activeSubscriptions.set(subscriptionId, subscription) + log(`✓ 订阅成功: ${destination} (ID: ${subscriptionId})`) + return subscriptionId + } catch (error) { + logError(`订阅 ${destination} 失败:`, error) + return '' + } + } + + /** + * 订阅指定主题 + * + * @param destination 目标主题地址(如:/topic/message) + * @param callback 接收到消息时的回调函数 + * @returns 订阅 ID,用于后续取消订阅 + */ + const subscribe = (destination: string, callback: (message: IMessage) => void): string => { + // 保存订阅配置到注册表,用于断线重连后自动恢复 + subscriptionRegistry.set(destination, { destination, callback }) + + // 如果已连接,立即订阅 + if (stompClient.value?.connected) { + return performSubscribe(destination, callback) + } + + log(`暂存订阅配置: ${destination},将在连接建立后自动订阅`) + return '' + } + + /** + * 取消订阅 + * + * @param subscriptionId 订阅 ID(由 subscribe 方法返回) + */ + const unsubscribe = (subscriptionId: string) => { + const subscription = activeSubscriptions.get(subscriptionId) + if (subscription) { + try { + subscription.unsubscribe() + activeSubscriptions.delete(subscriptionId) + log(`✓ 已取消订阅: ${subscriptionId}`) + } catch (error) { + logWarn(`取消订阅 ${subscriptionId} 时出错:`, error) + } + } + } + + /** + * 取消指定主题的订阅(从注册表中移除) + * + * @param destination 主题地址 + */ + const unsubscribeDestination = (destination: string) => { + // 从注册表中移除 + subscriptionRegistry.delete(destination) + + // 取消所有匹配该主题的活动订阅 + for (const [id, subscription] of activeSubscriptions.entries()) { + // 注意:STOMP 的 subscription 对象没有直接暴露 destination, + // 这里简化处理,实际使用时可能需要额外维护 id -> destination 的映射 + try { + subscription.unsubscribe() + activeSubscriptions.delete(id) + } catch (error) { + logWarn(`取消订阅 ${id} 时出错:`, error) + } + } + + log(`✓ 已移除主题订阅配置: ${destination}`) + } + + /** + * 断开 WebSocket 连接 + * + * @param clearSubscriptions 是否清除订阅注册表(默认为 true) + */ + const disconnect = (clearSubscriptions = true) => { + // 设置手动断开标志 + isManualDisconnect = true + + // 清除所有定时器 + clearAllTimers() + + // 取消所有活动订阅 + for (const [id, subscription] of activeSubscriptions.entries()) { + try { + subscription.unsubscribe() + } catch (error) { + logWarn(`取消订阅 ${id} 时出错:`, error) + } + } + activeSubscriptions.clear() + + // 可选:清除订阅注册表 + if (clearSubscriptions) { + subscriptionRegistry.clear() + log('已清除所有订阅配置') + } + + // 断开连接 + if (stompClient.value) { + try { + if (stompClient.value.connected || stompClient.value.active) { + stompClient.value.deactivate() + log('✓ WebSocket 连接已主动断开') + } + } catch (error) { + logError('断开 WebSocket 连接时出错:', error) + } + stompClient.value = null + } + + connectionState.value = ConnectionState.DISCONNECTED + reconnectAttempts.value = 0 + } + + // ==================== 返回公共接口 ==================== + return { + // 状态 + connectionState: readonly(connectionState), + isConnected, + reconnectAttempts: readonly(reconnectAttempts), + + // 连接管理 + connect, + disconnect, + + // 订阅管理 + subscribe, + unsubscribe, + unsubscribeDestination, + + // 统计信息 + getActiveSubscriptionCount: () => activeSubscriptions.size, + getRegisteredSubscriptionCount: () => subscriptionRegistry.size + } +} diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..090bb04 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,74 @@ +/** + * 项目常量统一管理 + * 存储键命名规范:{prefix}:{namespace}:{key} + */ + +export const APP_PREFIX = 'vea' + +export const STORAGE_KEYS = { + // 用户认证相关 + ACCESS_TOKEN: `${APP_PREFIX}:auth:access_token`, // JWT访问令牌 + REFRESH_TOKEN: `${APP_PREFIX}:auth:refresh_token`, // JWT刷新令牌 + REMEMBER_ME: `${APP_PREFIX}:auth:remember_me`, // 记住登录状态 + + // 系统核心相关 + DICT_CACHE: `${APP_PREFIX}:system:dict_cache`, // 字典数据缓存 + + // UI设置相关 + SHOW_TAGS_VIEW: `${APP_PREFIX}:ui:show_tags_view`, // 显示标签页视图 + SHOW_APP_LOGO: `${APP_PREFIX}:ui:show_app_logo`, // 显示应用Logo + SHOW_WATERMARK: `${APP_PREFIX}:ui:show_watermark`, // 显示水印 + ENABLE_AI_ASSISTANT: `${APP_PREFIX}:ui:enable_ai_assistant`, // 启用 AI 助手 + LAYOUT: `${APP_PREFIX}:ui:layout`, // 布局模式 + SIDEBAR_COLOR_SCHEME: `${APP_PREFIX}:ui:sidebar_color_scheme`, // 侧边栏配色方案 + THEME: `${APP_PREFIX}:ui:theme`, // 主题模式 + THEME_COLOR: `${APP_PREFIX}:ui:theme_color`, // 主题色 + + // 应用状态相关 + DEVICE: `${APP_PREFIX}:app:device`, // 设备类型 + SIZE: `${APP_PREFIX}:app:size`, // 屏幕尺寸 + LANGUAGE: `${APP_PREFIX}:app:language`, // 应用语言 + SIDEBAR_STATUS: `${APP_PREFIX}:app:sidebar_status`, // 侧边栏状态 + ACTIVE_TOP_MENU_PATH: `${APP_PREFIX}:app:active_top_menu_path` // 当前激活的顶部菜单路径 +} as const + +export const ROLE_ROOT = 'ROOT' // 超级管理员角色 + +// 分组键集合(便于批量操作) +export const AUTH_KEYS = { + ACCESS_TOKEN: STORAGE_KEYS.ACCESS_TOKEN, + REFRESH_TOKEN: STORAGE_KEYS.REFRESH_TOKEN, + REMEMBER_ME: STORAGE_KEYS.REMEMBER_ME +} as const + +export const SYSTEM_KEYS = { + DICT_CACHE: STORAGE_KEYS.DICT_CACHE +} as const + +export const SETTINGS_KEYS = { + SHOW_TAGS_VIEW: STORAGE_KEYS.SHOW_TAGS_VIEW, + SHOW_APP_LOGO: STORAGE_KEYS.SHOW_APP_LOGO, + SHOW_WATERMARK: STORAGE_KEYS.SHOW_WATERMARK, + ENABLE_AI_ASSISTANT: STORAGE_KEYS.ENABLE_AI_ASSISTANT, + SIDEBAR_COLOR_SCHEME: STORAGE_KEYS.SIDEBAR_COLOR_SCHEME, + LAYOUT: STORAGE_KEYS.LAYOUT, + THEME_COLOR: STORAGE_KEYS.THEME_COLOR, + THEME: STORAGE_KEYS.THEME +} as const + +export const APP_KEYS = { + DEVICE: STORAGE_KEYS.DEVICE, + SIZE: STORAGE_KEYS.SIZE, + LANGUAGE: STORAGE_KEYS.LANGUAGE, + SIDEBAR_STATUS: STORAGE_KEYS.SIDEBAR_STATUS, + ACTIVE_TOP_MENU_PATH: STORAGE_KEYS.ACTIVE_TOP_MENU_PATH +} as const + +export const ALL_STORAGE_KEYS = { + ...AUTH_KEYS, + ...SYSTEM_KEYS, + ...SETTINGS_KEYS, + ...APP_KEYS +} as const + +export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS] diff --git a/src/directives/index.ts b/src/directives/index.ts new file mode 100644 index 0000000..db867f5 --- /dev/null +++ b/src/directives/index.ts @@ -0,0 +1,8 @@ +import type { App } from 'vue' +import { permission } from './permission' + +// 注册指令 +export function setupDirective(app: App) { + // 权限指令 + app.directive('permission', permission) +} diff --git a/src/directives/permission/index.ts b/src/directives/permission/index.ts new file mode 100644 index 0000000..3ce35f9 --- /dev/null +++ b/src/directives/permission/index.ts @@ -0,0 +1,58 @@ +import type { Directive, DirectiveBinding } from 'vue' +import { useUserStore } from '@/store' + +export interface PermissionBinding { + value: string | string[] +} +function filterByDoubleColon(strArray: string[]): string[] { + return strArray.filter((str) => { + const matches = str.match(/:/g) + const hasTwoColons = matches && matches.length === 2 + if (!hasTwoColons) return false + const firstColonIndex = str.indexOf(':') + const secondColonIndex = str.indexOf(':', firstColonIndex + 1) + const afterSecondColon = str.substring(secondColonIndex + 1) + const startsWithView = afterSecondColon.startsWith('view') + return hasTwoColons && !startsWithView + }) +} +export const permission: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const { value } = binding + const userStore = useUserStore() + + // 指令逻辑 + if (!checkPermission(value, filterByDoubleColon(userStore.userInfo?.permission_data || []))) { + el.style.display = 'none' // 隐藏元素而不是删除 + } + }, + updated(el: HTMLElement, binding: DirectiveBinding) { + const { value } = binding + const userStore = useUserStore() + // 更新时重新检查权限 + if (!checkPermission(value, filterByDoubleColon(userStore.userInfo?.permission_data || []))) { + el.style.display = 'none' // 隐藏元素 + } else { + el.style.display = '' // 恢复默认显示状态 + } + } +} + +// 权限检查逻辑 +function checkPermission(permission: string | string[], userPermissions: string[]): boolean { + if (!permission || !userPermissions) return false + + // 检查是否为超级管理员权限 + if (userPermissions.includes('*:*:*')) { + return true + } + + // 将传入的权限转换为数组 + const permissions = Array.isArray(permission) ? permission : [permission] + + // 过滤用户权限数据 + const filteredPermissions = filterByDoubleColon(userPermissions) + + // 检查是否有任意一个权限匹配 + return permissions.some((perm) => filteredPermissions.includes(perm)) +} diff --git a/src/enums/api/code-enum.ts b/src/enums/api/code-enum.ts new file mode 100644 index 0000000..f5328fb --- /dev/null +++ b/src/enums/api/code-enum.ts @@ -0,0 +1,23 @@ +/** + * API响应码枚举 + */ +export const enum ApiCodeEnum { + /** + * 成功 + */ + SUCCESS = '00000', + /** + * 错误 + */ + ERROR = 'B0001', + + /** + * 访问令牌无效或过期 + */ + ACCESS_TOKEN_INVALID = 'A0230', + + /** + * 刷新令牌无效或过期 + */ + REFRESH_TOKEN_INVALID = 'A0231' +} diff --git a/src/enums/codegen/form-enum.ts b/src/enums/codegen/form-enum.ts new file mode 100644 index 0000000..b9eaef2 --- /dev/null +++ b/src/enums/codegen/form-enum.ts @@ -0,0 +1,15 @@ +/** + * 表单类型枚举 + */ +export const FormTypeEnum: Record = { + INPUT: { value: 1, label: '输入框' }, + SELECT: { value: 2, label: '下拉框' }, + RADIO: { value: 3, label: '单选框' }, + CHECK_BOX: { value: 4, label: '复选框' }, + INPUT_NUMBER: { value: 5, label: '数字输入框' }, + SWITCH: { value: 6, label: '开关' }, + TEXT_AREA: { value: 7, label: '文本域' }, + DATE: { value: 8, label: '日期框' }, + DATE_TIME: { value: 9, label: '日期时间框' }, + HIDDEN: { value: 10, label: '隐藏域' } +} diff --git a/src/enums/codegen/query-enum.ts b/src/enums/codegen/query-enum.ts new file mode 100644 index 0000000..2f321fe --- /dev/null +++ b/src/enums/codegen/query-enum.ts @@ -0,0 +1,37 @@ +/** + * 查询类型枚举 + */ +export const QueryTypeEnum: Record = { + /** 等于 */ + EQ: { value: 1, label: '=' }, + + /** 模糊匹配 */ + LIKE: { value: 2, label: "LIKE '%s%'" }, + + /** 包含 */ + IN: { value: 3, label: 'IN' }, + + /** 范围 */ + BETWEEN: { value: 4, label: 'BETWEEN' }, + + /** 大于 */ + GT: { value: 5, label: '>' }, + + /** 大于等于 */ + GE: { value: 6, label: '>=' }, + + /** 小于 */ + LT: { value: 7, label: '<' }, + + /** 小于等于 */ + LE: { value: 8, label: '<=' }, + + /** 不等于 */ + NE: { value: 9, label: '!=' }, + + /** 左模糊匹配 */ + LIKE_LEFT: { value: 10, label: "LIKE '%s'" }, + + /** 右模糊匹配 */ + LIKE_RIGHT: { value: 11, label: "LIKE 's%'" } +} diff --git a/src/enums/index.ts b/src/enums/index.ts new file mode 100644 index 0000000..520e2bd --- /dev/null +++ b/src/enums/index.ts @@ -0,0 +1,11 @@ +export * from './api/code-enum' + +export * from './codegen/form-enum' +export * from './codegen/query-enum' + +export * from './settings/layout-enum' +export * from './settings/theme-enum' +export * from './settings/locale-enum' +export * from './settings/device-enum' + +export * from './system/menu-enum' diff --git a/src/enums/settings/device-enum.ts b/src/enums/settings/device-enum.ts new file mode 100644 index 0000000..152aa77 --- /dev/null +++ b/src/enums/settings/device-enum.ts @@ -0,0 +1,14 @@ +/** + * 设备枚举 + */ +export const enum DeviceEnum { + /** + * 宽屏设备 + */ + DESKTOP = 'desktop', + + /** + * 窄屏设备 + */ + MOBILE = 'mobile' +} diff --git a/src/enums/settings/layout-enum.ts b/src/enums/settings/layout-enum.ts new file mode 100644 index 0000000..9b3ab4d --- /dev/null +++ b/src/enums/settings/layout-enum.ts @@ -0,0 +1,53 @@ +/** + * 菜单布局枚举 + */ +export const enum LayoutMode { + /** + * 左侧菜单布局 + */ + LEFT = 'left', + /** + * 顶部菜单布局 + */ + TOP = 'top', + + /** + * 混合菜单布局 + */ + MIX = 'mix' +} + +/** + * 侧边栏状态枚举 + */ +export const enum SidebarStatus { + /** + * 展开 + */ + OPENED = 'opened', + + /** + * 关闭 + */ + CLOSED = 'closed' +} + +/** + * 组件尺寸枚举 + */ +export const enum ComponentSize { + /** + * 默认 + */ + DEFAULT = 'default', + + /** + * 大型 + */ + LARGE = 'large', + + /** + * 小型 + */ + SMALL = 'small' +} diff --git a/src/enums/settings/locale-enum.ts b/src/enums/settings/locale-enum.ts new file mode 100644 index 0000000..dc443ea --- /dev/null +++ b/src/enums/settings/locale-enum.ts @@ -0,0 +1,14 @@ +/** + * 语言枚举 + */ +export const enum LanguageEnum { + /** + * 中文 + */ + ZH_CN = 'zh-cn', + + /** + * 英文 + */ + EN = 'en' +} diff --git a/src/enums/settings/theme-enum.ts b/src/enums/settings/theme-enum.ts new file mode 100644 index 0000000..4115560 --- /dev/null +++ b/src/enums/settings/theme-enum.ts @@ -0,0 +1,32 @@ +/** + * 主题枚举 + */ +export const enum ThemeMode { + /** + * 明亮主题 + */ + LIGHT = 'light', + /** + * 暗黑主题 + */ + DARK = 'dark', + + /** + * 系统自动 + */ + AUTO = 'auto' +} + +/** + * 侧边栏配色方案枚举 + */ +export const enum SidebarColor { + /** + * 经典蓝 + */ + CLASSIC_BLUE = 'classic-blue', + /** + * 极简白 + */ + MINIMAL_WHITE = 'minimal-white' +} diff --git a/src/enums/system/menu-enum.ts b/src/enums/system/menu-enum.ts new file mode 100644 index 0000000..e32efb5 --- /dev/null +++ b/src/enums/system/menu-enum.ts @@ -0,0 +1,7 @@ +// 核心枚举定义 +export enum MenuTypeEnum { + CATALOG = 2, // 目录 + MENU = 1, // 菜单 + BUTTON = 4, // 按钮 + EXTLINK = 3 // 外链 +} diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..b1ce460 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,8 @@ +/// + +declare module '*.vue' { + import { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/src/lang/index.ts b/src/lang/index.ts new file mode 100644 index 0000000..759cb43 --- /dev/null +++ b/src/lang/index.ts @@ -0,0 +1,27 @@ +import type { App } from 'vue' +import { createI18n } from 'vue-i18n' +import { useAppStoreHook } from '@/store/modules/app-store' +// 本地语言包 +import enLocale from './package/en.json' +import zhCnLocale from './package/zh-cn.json' + +const appStore = useAppStoreHook() + +const messages = { + 'zh-cn': zhCnLocale, + en: enLocale +} + +const i18n = createI18n({ + legacy: false, + locale: appStore.language, + messages, + globalInjection: true +}) + +// 全局注册 i18n +export function setupI18n(app: App) { + app.use(i18n) +} + +export default i18n diff --git a/src/lang/package/en.json b/src/lang/package/en.json new file mode 100644 index 0000000..021e409 --- /dev/null +++ b/src/lang/package/en.json @@ -0,0 +1,91 @@ +{ + "route": { + "dashboard": "Dashboard", + "document": "Document" + }, + "login": { + "themeToggle": "Theme Switch", + "languageToggle": "Language Switch", + "dark": "Dark", + "light": "Light", + "username": "Username", + "password": "Password", + "login": "Login", + "captchaCode": "Verify Code", + "capsLock": "Caps Lock is On", + "rememberMe": "Remember Me", + "forgetPassword": "Forget Password?", + "message": { + "username": { + "required": "Please enter Username" + }, + "password": { + "required": "Please enter Password", + "min": "The password can not be less than 6 digits", + "confirm": "Please confirm the password again", + "inconformity": "The two password entries are inconsistent" + }, + "captchaCode": { + "required": "Please enter Verify Code" + } + }, + "otherLoginMethods": "Other", + "resetPassword": "Reset password", + "thinkOfPasswd": "Remember your password?", + "register": "Register account", + "agree": "I have read and agree to the", + "userAgreement": "User Agreement", + "haveAccount": "Already have an account?", + "noAccount": "Don't have an account?", + "quickFill": "Quick fill", + "reg": "Register" + }, + "navbar": { + "dashboard": "Dashboard", + "logout": "Logout", + "document": "Document", + "gitee": "Gitee", + "profile": "User Profile" + }, + "sizeSelect": { + "tooltip": "Layout Size", + "default": "Default", + "large": "Large", + "small": "Small", + "message": { + "success": "Switch Layout Size Successful!" + } + }, + "langSelect": { + "message": { + "success": "Switch Language Successful!" + } + }, + "settings": { + "project": "Project Settings", + "theme": "Theme", + "interface": "Interface", + "navigation": "Navigation", + "themeColor": "Theme Color", + "showTagsView": "Show Tags View", + "showAppLogo": "Show App Logo", + "sidebarColorScheme": "Sidebar Color Scheme", + "showWatermark": "Show Watermark", + "classicBlue": "Classic Blue", + "minimalWhite": "Minimal White", + "copyConfig": "Copy Config", + "resetConfig": "Reset Default", + "copySuccess": "Configuration copied to clipboard", + "resetSuccess": "Reset to default configuration", + "copyDescription": "Copy config will generate current settings code, reset will restore all settings to default", + "confirmReset": "Are you sure to reset all settings to default? This operation cannot be undone.", + "applyToFile": "Apply to File", + "onlyCopy": "Only Copy", + "leftLayout": "Left Mode", + "topLayout": "Top Mode", + "mixLayout": "Mix Mode", + "configManagement": "Config Management", + "copyConfigDescription": "Generate current settings code and copy to clipboard, then overwrite src/settings.ts file", + "resetConfigDescription": "Restore all settings to system default values" + } +} diff --git a/src/lang/package/zh-cn.json b/src/lang/package/zh-cn.json new file mode 100644 index 0000000..3346ce0 --- /dev/null +++ b/src/lang/package/zh-cn.json @@ -0,0 +1,94 @@ +{ + "route": { + "dashboard": "首页", + "document": "项目文档" + }, + "login": { + "themeToggle": "主题切换", + "languageToggle": "语言切换", + "dark": "暗黑", + "light": "明亮", + "username": "用户名", + "password": "密码", + "login": "登 录", + "captchaCode": "验证码", + "capsLock": "大写锁定已打开", + "rememberMe": "记住我", + "forgetPassword": "忘记密码?", + "message": { + "username": { + "required": "请输入用户名" + }, + "password": { + "required": "请输入密码", + "min": "密码不能少于6位", + "confirm": "请再次确认密码", + "inconformity": "两次密码输入不一致" + }, + "captchaCode": { + "required": "请输入验证码" + } + }, + "otherLoginMethods": "其他", + "resetPassword": "重置密码", + "thinkOfPasswd": "想起密码?", + "register": "注册账号", + "agree": "我已同意并阅读", + "userAgreement": "用户协议", + "haveAccount": "已有账号?", + "noAccount": "您没有账号?", + "quickFill": "快速填写", + "reg": "注 册" + }, + "navbar": { + "dashboard": "首页", + "logout": "退出登录", + "document": "项目文档", + "gitee": "项目地址", + "profile": "个人中心" + }, + "sizeSelect": { + "tooltip": "布局大小", + "default": "默认", + "large": "大型", + "small": "小型", + "message": { + "success": "切换布局大小成功!" + } + }, + "langSelect": { + "message": { + "success": "切换语言成功!" + } + }, + "settings": { + "project": "项目配置", + "theme": "主题设置", + "interface": "界面设置", + "navigation": "导航设置", + "themeColor": "主题颜色", + "themeColorTip": "主题颜色", + "darkMode": "暗黑模式", + "layoutSetting": "布局设置", + "sidebarColorScheme": "侧边栏配色", + "showTagsView": "显示页签", + "showAppLogo": "显示Logo", + "showWatermark": "显示水印", + "classicBlue": "经典蓝", + "minimalWhite": "极简白", + "copyConfig": "复制配置", + "resetConfig": "重置默认", + "copySuccess": "配置已复制到剪贴板", + "resetSuccess": "已重置为默认配置", + "copyDescription": "复制配置将生成当前设置的代码,重置将恢复所有设置为默认值", + "confirmReset": "确定要重置所有设置为默认值吗?此操作不可恢复。", + "applyToFile": "应用到文件", + "onlyCopy": "仅复制", + "leftLayout": "左侧模式", + "topLayout": "顶部模式", + "mixLayout": "混合模式", + "configManagement": "配置管理", + "copyConfigDescription": "生成当前设置的代码并复制到剪贴板,然后覆盖 src/settings.ts 文件", + "resetConfigDescription": "恢复所有设置为系统默认值" + } +} diff --git a/src/layouts/components/AppLogo/index.vue b/src/layouts/components/AppLogo/index.vue new file mode 100644 index 0000000..89f2cc8 --- /dev/null +++ b/src/layouts/components/AppLogo/index.vue @@ -0,0 +1,72 @@ + + + + + + + diff --git a/src/layouts/components/AppMain/index.vue b/src/layouts/components/AppMain/index.vue new file mode 100644 index 0000000..afcde20 --- /dev/null +++ b/src/layouts/components/AppMain/index.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/layouts/components/Menu/BasicMenu.vue b/src/layouts/components/Menu/BasicMenu.vue new file mode 100644 index 0000000..8d4fecd --- /dev/null +++ b/src/layouts/components/Menu/BasicMenu.vue @@ -0,0 +1,245 @@ + + + + diff --git a/src/layouts/components/Menu/MixTopMenu.vue b/src/layouts/components/Menu/MixTopMenu.vue new file mode 100644 index 0000000..9b9cbbc --- /dev/null +++ b/src/layouts/components/Menu/MixTopMenu.vue @@ -0,0 +1,187 @@ + + + + + + diff --git a/src/layouts/components/Menu/components/MenuItem.vue b/src/layouts/components/Menu/components/MenuItem.vue new file mode 100644 index 0000000..03a67f4 --- /dev/null +++ b/src/layouts/components/Menu/components/MenuItem.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/src/layouts/components/Menu/components/MenuItemContent.vue b/src/layouts/components/Menu/components/MenuItemContent.vue new file mode 100644 index 0000000..560c07a --- /dev/null +++ b/src/layouts/components/Menu/components/MenuItemContent.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/layouts/components/NavBar/components/NavbarActions.vue b/src/layouts/components/NavBar/components/NavbarActions.vue new file mode 100644 index 0000000..2476c26 --- /dev/null +++ b/src/layouts/components/NavBar/components/NavbarActions.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/src/layouts/components/NavBar/index.vue b/src/layouts/components/NavBar/index.vue new file mode 100644 index 0000000..a775e92 --- /dev/null +++ b/src/layouts/components/NavBar/index.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/layouts/components/Settings/index.vue b/src/layouts/components/Settings/index.vue new file mode 100644 index 0000000..259b9fc --- /dev/null +++ b/src/layouts/components/Settings/index.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/src/layouts/components/TagsView/index.vue b/src/layouts/components/TagsView/index.vue new file mode 100644 index 0000000..7aba209 --- /dev/null +++ b/src/layouts/components/TagsView/index.vue @@ -0,0 +1,410 @@ + + + + + diff --git a/src/layouts/index.vue b/src/layouts/index.vue new file mode 100644 index 0000000..0252dd1 --- /dev/null +++ b/src/layouts/index.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/layouts/modes/base/index.vue b/src/layouts/modes/base/index.vue new file mode 100644 index 0000000..c5d1087 --- /dev/null +++ b/src/layouts/modes/base/index.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/layouts/modes/left/index.vue b/src/layouts/modes/left/index.vue new file mode 100644 index 0000000..e20b092 --- /dev/null +++ b/src/layouts/modes/left/index.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/layouts/modes/mix/index.vue b/src/layouts/modes/mix/index.vue new file mode 100644 index 0000000..d2c5c27 --- /dev/null +++ b/src/layouts/modes/mix/index.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/src/layouts/modes/top/index.vue b/src/layouts/modes/top/index.vue new file mode 100644 index 0000000..8bccb02 --- /dev/null +++ b/src/layouts/modes/top/index.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..efe498d --- /dev/null +++ b/src/main.ts @@ -0,0 +1,24 @@ +import { createApp } from 'vue' +import App from './App.vue' +import setupPlugins from '@/plugins' + +// 暗黑主题样式 +import 'element-plus/theme-chalk/dark/css-vars.css' +import 'vxe-table/lib/style.css' +// 暗黑模式自定义变量 +import '@/styles/dark/css-vars.css' +import '@/styles/index.scss' +import 'uno.css' + +// 过渡动画 +import 'animate.css' + +// 自动为某些默认事件(如 touchstart、wheel 等)添加 { passive: true },提升滚动性能并消除控制台的非被动事件监听警告 +import 'default-passive-events' +// import { setupDirective } from '@/directives' + +const app = createApp(App) +// 注册插件 +app.use(setupPlugins) +// setupDirective(app) +app.mount('#app') diff --git a/src/plugins/icons.ts b/src/plugins/icons.ts new file mode 100644 index 0000000..86c7fa5 --- /dev/null +++ b/src/plugins/icons.ts @@ -0,0 +1,9 @@ +import type { App } from 'vue' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' + +// 注册所有图标 +export function setupElIcons(app: App) { + for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) + } +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000..511f470 --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,34 @@ +import type { App } from 'vue' + +import { setupDirective } from '@/directives' +import { setupI18n } from '@/lang' +import { setupRouter } from '@/router' +import { setupStore } from '@/store' +import { setupElIcons } from './icons' +import { setupPermission } from './permission' +import { setupWebSocket } from './websocket' +import { InstallCodeMirror } from 'codemirror-editor-vue3' +import { setupVxeTable } from './vxeTable' + +export default { + install(app: App) { + // 自定义指令(directive) + setupDirective(app) + // 路由(router) + setupRouter(app) + // 状态管理(store) + setupStore(app) + // 国际化 + setupI18n(app) + // Element-plus图标 + setupElIcons(app) + // 路由守卫 + setupPermission() + // WebSocket服务 + setupWebSocket() + // vxe-table + setupVxeTable(app) + // 注册 CodeMirror + app.use(InstallCodeMirror) + } +} diff --git a/src/plugins/permission.ts b/src/plugins/permission.ts new file mode 100644 index 0000000..ee37fd8 --- /dev/null +++ b/src/plugins/permission.ts @@ -0,0 +1,195 @@ +import NProgress from '@/utils/nprogress' +import router from '@/router' +import { useUserStore } from '@/store' +import { RouteRecordRaw } from 'vue-router' + +function filterByDoubleColon(strArray: string[]): string[] { + return strArray.filter((str) => (str.match(/:/g) || []).length === 2) +} + +/** + * 从权限字符串中提取模块名称并去重 + * @param permissionStrings 权限字符串数组 + * @returns 去重后的模块名称数组 + */ +function extractAndDeduplicateModules(permissionStrings: string[]): string[] { + const modules = permissionStrings.map((permission: string) => { + // 分割字符串并取第一部分 + return permission.split(':')[0] + }) + + // 使用 Set 去重 + return [...new Set(modules)] +} + +/** + * 从路由配置中筛选出 meta 对象中存在 permissionIdentifier 字段的路由 + * @param routes 路由配置数组 + * @returns 包含 permissionIdentifier 的路由对象数组 + */ +function getRoutesWithPermissionIdentifier(routes: RouteRecordRaw[]): RouteRecordRaw[] { + const result: RouteRecordRaw[] = [] + + const traverseRoutes = (routeList: RouteRecordRaw[]) => { + for (const route of routeList) { + // 检查当前路由的 meta 中是否有 permissionIdentifier + if (route.meta && (route.meta as any).permissionIdentifier) { + result.push(route) + } + + // 如果有子路由,递归遍历 + if (route.children && route.children.length > 0) { + traverseRoutes(route.children) + } + } + } + + traverseRoutes(routes) + return result +} + +/** + * 根据权限字符串数组隐藏匹配的路由 + * @param routes 路由配置数组对象 + * @param permissionStrings 权限字符串数组 + */ +function hideRoutesByPermissions(routes: RouteRecordRaw[], permissionStrings: string[]): void { + const permissionSet = new Set(permissionStrings) + + const traverseRoutes = (routeList: RouteRecordRaw[]) => { + for (const route of routeList) { + // 检查当前路由的 meta 中是否有 permissionIdentifier + if (route.meta && (route.meta as any).permissionIdentifier) { + const routePermission = (route.meta as any).permissionIdentifier + + // 如果权限字符串数组中包含当前路由的权限标识,则隐藏该路由 + if (permissionSet.has(routePermission)) { + ;(route.meta as any).hidden = true + ;(route.meta as any)._wasHiddenByPermission = true // 标记是由权限引起的隐藏 + } else { + // 如果不再需要隐藏,移除隐藏标记 + if ((route.meta as any)._wasHiddenByPermission) { + delete (route.meta as any).hidden + delete (route.meta as any)._wasHiddenByPermission + } + } + } + + // 如果有子路由,递归遍历 + if (route.children && route.children.length > 0) { + traverseRoutes(route.children) + } + } + } + + traverseRoutes(routes) +} + +/** + * 重置有 permissionIdentifier 的路由隐藏状态 + */ +function resetPermissionBasedRoutes(routes: RouteRecordRaw[]): void { + const traverseRoutes = (routeList: RouteRecordRaw[]) => { + for (const route of routeList) { + if (route.meta && (route.meta as any).permissionIdentifier) { + // 只重置有 permissionIdentifier 的路由状态 + if ((route.meta as any).hidden && (route.meta as any)._wasHiddenByPermission) { + // 恢复原始隐藏状态或设为可见 + delete (route.meta as any).hidden + delete (route.meta as any)._wasHiddenByPermission + } + } + + if (route.children && route.children.length > 0) { + traverseRoutes(route.children) + } + } + } + + traverseRoutes(routes) +} + +export function setupPermission() { + const whiteList = ['/login'] + + router.beforeEach(async (to, from, next) => { + NProgress.start() + + try { + const isLoggedIn = useUserStore().isLoggedIn() + + // 未登录处理 + if (!isLoggedIn) { + if (whiteList.includes(to.path)) { + next() + } else { + next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) + NProgress.done() + } + return + } + + // 已登录登录页重定向 + if (to.path === '/login') { + next({ path: '/' }) + return + } + + // 路由404检查 + if (to.matched.length === 0) { + next('/404') + return + } + + // 动态标题设置 + const title = (to.params.title as string) || (to.query.title as string) + if (title) { + to.meta.title = title + } + const userStore = useUserStore() + // 如果用户信息不存在或已过期,获取用户信息 + + if (!userStore.userInfo?.id) { + // const res: any = await userStore.getUserInfo() + await userStore.getUserInfo() + + // // 获取路由配置 + // const routes = router.getRoutes() + // const routesWithPermission = getRoutesWithPermissionIdentifier(routes) + // userStore.setPermissionCharactersArr([ + // ...new Set(routesWithPermission.map((route: any) => route.meta.permissionIdentifier)) + // ]) + // const allPermissions = userStore.permissionCharactersArr + // + // resetPermissionBasedRoutes(routes) + // // 检查是否为超级管理员权限 + // if (res.data.permission_data.includes('*:*:*')) { + // // 如果是超级管理员,不对路由进行隐藏处理,所有路由都可访问 + // } else { + // // 非超级管理员,按原有逻辑处理权限 + // const userPermissions = [ + // ...filterByDoubleColon(res.data.permission_data), + // ...extractAndDeduplicateModules(res.data.permission_data) + // ] + // + // const missingPermissions = allPermissions.filter( + // (permission) => !userPermissions.includes(permission) + // ) + // hideRoutesByPermissions(routesWithPermission, missingPermissions) + // } + } + + next() + } catch (error) { + // 错误处理:重置状态并跳转登录 + console.error('Route guard error:', error) + await useUserStore().resetAllState() + next('/login') + NProgress.done() + } + }) + + router.afterEach(() => { + NProgress.done() + }) +} diff --git a/src/plugins/vxeTable.ts b/src/plugins/vxeTable.ts new file mode 100644 index 0000000..0c33e8c --- /dev/null +++ b/src/plugins/vxeTable.ts @@ -0,0 +1,70 @@ +import type { App } from 'vue' +import VXETable from 'vxe-table' // https://vxetable.cn/v4.6/#/table/start/install + +// 全局默认参数 +VXETable.setConfig({ + // 全局尺寸 + size: 'medium', + // 全局 zIndex 起始值,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡 + zIndex: 9999, + // 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据 + version: 0, + // 全局 loading 提示内容,如果为 null 则不显示文本 + loadingText: null, + table: { + showHeader: true, + showOverflow: 'tooltip', + showHeaderOverflow: 'tooltip', + autoResize: true, + // stripe: false, + border: 'inner', + // round: false, + emptyText: '暂无数据', + rowConfig: { + isHover: true, + isCurrent: true, + // 行数据的唯一主键字段名 + keyField: '_VXE_ID' + }, + columnConfig: { + resizable: false + }, + align: 'center', + headerAlign: 'center' + }, + pager: { + // size: "medium", + // 配套的样式 + perfect: false, + pageSize: 10, + pagerCount: 7, + pageSizes: [10, 20, 50], + layouts: [ + 'Total', + 'PrevJump', + 'PrevPage', + 'Number', + 'NextPage', + 'NextJump', + 'Sizes', + 'FullJump' + ] + }, + modal: { + minWidth: 500, + minHeight: 400, + lockView: true, + mask: true, + // duration: 3000, + // marginSize: 20, + dblclickZoom: false, + showTitleOverflow: true, + transfer: true, + draggable: false + } +}) + +export function setupVxeTable(app: App) { + // Vxe Table 组件完整引入 + app.use(VXETable) +} diff --git a/src/plugins/websocket.ts b/src/plugins/websocket.ts new file mode 100644 index 0000000..fe5e9c7 --- /dev/null +++ b/src/plugins/websocket.ts @@ -0,0 +1,142 @@ +import { useDictSync } from '@/composables' +import { AuthStorage } from '@/utils/auth' +// 不直接导入 store 或 userStore + +// 全局 WebSocket 实例管理 +const websocketInstances = new Map() + +// 用于防止重复初始化的状态标记 +let isInitialized = false +let dictWebSocketInstance: ReturnType | null = null + +/** + * 注册 WebSocket 实例 + */ +export function registerWebSocketInstance(key: string, instance: any) { + websocketInstances.set(key, instance) + console.log(`[WebSocketPlugin] Registered WebSocket instance: ${key}`) +} + +/** + * 获取 WebSocket 实例 + */ +export function getWebSocketInstance(key: string) { + return websocketInstances.get(key) +} + +/** + * 初始化WebSocket服务 + */ +export function setupWebSocket() { + console.log('[WebSocketPlugin] 开始初始化WebSocket服务...') + + // 检查是否已经初始化 + if (isInitialized) { + console.log('[WebSocketPlugin] WebSocket服务已经初始化,跳过重复初始化') + return + } + + // 检查环境变量是否配置 + const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT + if (!wsEndpoint) { + console.log('[WebSocketPlugin] 未配置WebSocket端点,跳过WebSocket初始化') + return + } + + // 检查是否已登录(基于是否存在访问令牌) + if (!AuthStorage.getAccessToken()) { + console.warn( + '[WebSocketPlugin] 未找到访问令牌,WebSocket初始化已跳过。用户登录后将自动重新连接。' + ) + return + } + + try { + // 延迟初始化,确保应用完全启动 + setTimeout(() => { + // 保存实例引用 + dictWebSocketInstance = useDictSync() + registerWebSocketInstance('dictSync', dictWebSocketInstance) + + // 初始化字典WebSocket服务 + dictWebSocketInstance.initWebSocket() + console.log('[WebSocketPlugin] 字典WebSocket初始化完成') + + // 初始化在线用户计数WebSocket + import('@/composables').then(({ useOnlineCount }) => { + const onlineCountInstance = useOnlineCount({ autoInit: false }) + onlineCountInstance.initWebSocket() + console.log('[WebSocketPlugin] 在线用户计数WebSocket初始化完成') + }) + + // 在窗口关闭前断开WebSocket连接 + window.addEventListener('beforeunload', handleWindowClose) + + console.log('[WebSocketPlugin] WebSocket服务初始化完成') + isInitialized = true + }, 1000) // 延迟1秒初始化 + } catch (error) { + console.error('[WebSocketPlugin] 初始化WebSocket服务失败:', error) + } +} + +/** + * 处理窗口关闭 + */ +function handleWindowClose() { + console.log('[WebSocketPlugin] 窗口即将关闭,断开WebSocket连接') + cleanupWebSocket() +} + +/** + * 清理WebSocket连接 + */ +export function cleanupWebSocket() { + // 清理字典 WebSocket + if (dictWebSocketInstance) { + try { + dictWebSocketInstance.closeWebSocket() + console.log('[WebSocketPlugin] 字典WebSocket连接已断开') + } catch (error) { + console.error('[WebSocketPlugin] 断开字典WebSocket连接失败:', error) + } + } + + // 清理所有注册的 WebSocket 实例 + websocketInstances.forEach((instance, key) => { + try { + if (instance && typeof instance.disconnect === 'function') { + instance.disconnect() + console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`) + } else if (instance && typeof instance.closeWebSocket === 'function') { + instance.closeWebSocket() + console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`) + } + } catch (error) { + console.error(`[WebSocketPlugin] 断开 ${key} WebSocket连接失败:`, error) + } + }) + + // 清空实例映射 + websocketInstances.clear() + + // 移除事件监听器 + window.removeEventListener('beforeunload', handleWindowClose) + + // 重置状态 + dictWebSocketInstance = null + isInitialized = false +} + +/** + * 重新初始化WebSocket(用于登录后重连) + */ +export function reinitializeWebSocket() { + // 先清理现有连接 + cleanupWebSocket() + + // 延迟后重新初始化 + setTimeout(() => { + setupWebSocket() + }, 500) +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..8d20c8c --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,437 @@ +import type { App } from 'vue' +import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router' + +export const Layout = () => import('@/layouts/index.vue') + +// 静态路由 +export const constantRoutes: RouteRecordRaw[] = [ + { + path: '/login', + component: () => import('@/views/login/index.vue'), + meta: { hidden: true } + }, + { + path: '/', + name: '/', + component: Layout, + redirect: '/boos/boosAccountManagement', + children: [ + // { + // path: 'dashboard', + // component: () => import('@/views/dashboard/index.vue'), + // // 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致 + // // 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude + // name: 'Dashboard', + // meta: { + // title: 'dashboard', + // icon: 'homepage', + // affix: true, + // keepAlive: true + // } + // }, + { + path: '401', + component: () => import('@/views/error/401.vue'), + meta: { hidden: true } + }, + { + path: '404', + component: () => import('@/views/error/404.vue'), + meta: { hidden: true } + } + ] + }, + // 人员管理模块 + // { + // path: '/personnel', + // component: Layout, + // name: 'Personnel', + // meta: { + // title: '人员管理', + // icon: 'setting', + // alwaysShow: true, + // permissionIdentifier: 'Personnel' + // }, + // children: [ + // { + // path: 'user', + // name: 'PersonnelUser', + // component: () => import('@/views/calibration/personnelManagement/index.vue'), + // meta: { + // title: '人事管理', + // permissionIdentifier: 'Personnel:PersonnelUser:view' + // } + // }, + // { + // path: 'department', + // name: 'Department', + // component: () => import('@/views/calibration/department/index.vue'), + // meta: { + // title: '部门管理', + // permissionIdentifier: 'Personnel:Department:view' + // } + // }, + // { + // path: 'roleManagement', + // name: 'RoleManagement', + // component: () => import('@/views/calibration/roleManagement/index.vue'), + // meta: { + // title: '角色管理', + // permissionIdentifier: 'Personnel:RoleManagement:view' + // } + // }, + // { + // path: 'teamManagement', + // name: 'TeamManagement', + // component: () => import('@/views/calibration/teamManagement/index.vue'), + // meta: { + // title: '团队管理', + // permissionIdentifier: 'Personnel:TeamManagement:view' + // } + // }, + // { + // path: 'permissionManagement', + // name: 'PermissionManagement', + // component: () => import('@/views/calibration/permissionManagement/index.vue'), + // meta: { + // title: '权限管理', + // permissionIdentifier: 'Personnel:PermissionManagement:view' + // } + // } + // ] + // }, + // // 财务管理模块 + // { + // path: '/finance', + // component: Layout, + // name: 'Finance', + // meta: { + // title: '财务管理', + // icon: 'setting', + // alwaysShow: true, + // permissionIdentifier: 'Finance' + // }, + // children: [ + // { + // path: 'onboardingRegistration', + // name: 'OnboardingRegistration', + // component: () => import('@/views/calibration/onboardingRegistration/index.vue'), + // meta: { + // title: '入职财务登记', + // permissionIdentifier: 'Finance:OnboardingRegistration:view' + // } + // }, + // { + // path: 'departure', + // name: 'Departure', + // component: () => import('@/views/calibration/departureFinancialRegistration/index.vue'), + // meta: { + // title: '离职财务登记', + // permissionIdentifier: 'Finance:Departure:view' + // } + // }, + // { + // path: 'invoiceApplication', + // name: 'InvoiceApplication', + // component: () => import('@/views/calibration/invoiceApplication/index.vue'), + // meta: { + // title: '开票申请', + // permissionIdentifier: 'Finance:InvoiceApplication:view' + // } + // }, + // { + // path: 'revenueRecognition', + // name: 'RevenueRecognition', + // component: () => import('@/views/calibration/revenueRecognition/index.vue'), + // meta: { + // title: '收入确认', + // permissionIdentifier: 'Finance:RevenueRecognition:view' + // } + // }, + // { + // path: 'accountAdjustmentApplication', + // name: 'AccountAdjustmentApplication', + // component: () => import('@/views/calibration/accountAdjustmentApplication/index.vue'), + // meta: { + // title: '调账申请', + // permissionIdentifier: 'Finance:AccountAdjustmentApplication:view' + // } + // }, + // { + // path: 'paymentApplicationForm', + // name: 'PaymentApplicationForm', + // component: () => import('@/views/calibration/paymentApplicationForm/index.vue'), + // meta: { + // title: '付款申请单', + // permissionIdentifier: 'Finance:PaymentApplicationForm:view' + // } + // }, + // { + // path: 'reimbursement', + // name: 'Reimbursement', + // component: () => import('@/views/calibration/reimbursement/index.vue'), + // meta: { + // title: '报销', + // permissionIdentifier: 'Finance:Reimbursement:view' + // } + // }, + // { + // path: 'salaryBonusAdjustment', + // name: 'SalaryBonusAdjustment', + // component: () => import('@/views/calibration/salaryBonusAdjustment/index.vue'), + // meta: { + // title: '工资/奖金变更', + // permissionIdentifier: 'Finance:SalaryBonusAdjustment:view' + // } + // } + // ] + // }, + // // 业务管理模块 + // { + // path: '/business', + // component: Layout, + // name: 'Business', + // meta: { + // title: '业务管理', + // icon: 'setting', + // alwaysShow: true, + // permissionIdentifier: 'Business' + // }, + // children: [ + // { + // path: 'conflictOfInterestSearch', + // name: 'ConflictOfInterestSearch', + // component: () => + // import('@/views/calibration/businessSystem/conflictOfInterestSearch/index.vue'), + // meta: { + // title: '利益冲突检索', + // permissionIdentifier: 'Business:ConflictOfInterestSearch:view' + // } + // }, + // { + // path: 'preRegistration', + // name: 'PreRegistration', + // component: () => import('@/views/calibration/businessSystem/preRegistration/index.vue'), + // meta: { + // title: '预立案登记', + // permissionIdentifier: 'Business:PreRegistration:view' + // } + // }, + // { + // path: 'bidRegistration', + // name: 'BidRegistration', + // component: () => import('@/views/calibration/businessSystem/bidRegistration/index.vue'), + // meta: { + // title: '投标登记', + // permissionIdentifier: 'Business:BidRegistration:view' + // } + // }, + // { + // path: 'projectRegistration', + // name: 'ProjectRegistration', + // component: () => import('@/views/calibration/businessSystem/projectRegistration/index.vue'), + // meta: { + // title: '立项登记', + // permissionIdentifier: 'Business:ProjectRegistration:view' + // } + // } + // ] + // }, + // // 案件管理模块 + // { + // path: '/case', + // component: Layout, + // name: 'Case', + // meta: { + // title: '案件管理', + // icon: 'setting', + // permissionIdentifier: 'Case' + // }, + // children: [ + // { + // path: 'caseManagement', + // name: 'CaseManagement', + // component: () => import('@/views/calibration/caseManagement/index.vue'), + // meta: { + // title: '案件管理', + // permissionIdentifier: 'Case:CaseManagement:view' + // } + // }, + // { + // path: 'caseLabel', + // name: 'CaseLabel', + // component: () => import('@/views/calibration/caseLabel/index.vue'), + // meta: { + // title: '案件标签', + // permissionIdentifier: 'Case:CaseLabel:view' + // } + // } + // ] + // }, + // 申请用印 + { + path: '/boos', + name: 'Boos', + component: Layout, + meta: { + title: 'BOSS账号管理', + icon: 'setting' + }, + children: [ + { + path: 'boosAccountManagement', + name: 'BoosAccountManagement', + component: () => import('@/views/BoosAccountManagement/index.vue'), + meta: { + title: 'BOSS账号管理' + } + } + ] + }, + + // 入库登记 + { + path: '/task', + name: 'Task', + component: Layout, + meta: { + title: '任务管理', + icon: 'setting' + }, + children: [ + { + path: 'taskManagement', + name: 'TaskManagement', + component: () => import('@/views/TaskManagement/index.vue'), + meta: { + title: '任务管理' + } + } + ] + }, + // 注册平台登记 + { + path: '/filingRegistration', + name: 'FilingRegistration', + component: Layout, + meta: { + title: '注册平台登记', + icon: 'setting', + permissionIdentifier: 'FilingRegistration' + }, + children: [ + { + path: 'registrationPlatform', + name: 'RegistrationPlatform', + component: () => import('@/views/calibration/registrationPlatform/index.vue'), + meta: { + title: '注册平台登记', + permissionIdentifier: 'FilingRegistration:RegistrationPlatform:view' + } + } + ] + }, + // 公告 + { + path: '/notice', + name: 'Notice', + component: Layout, + meta: { + title: '公告管理', + icon: 'setting', + permissionIdentifier: 'Notice' + }, + children: [ + { + path: 'announcementManagement', + name: 'AnnouncementManagement', + component: () => import('@/views/calibration/announcementManagement/index.vue'), + meta: { + title: '公告管理', + permissionIdentifier: 'Notice:AnnouncementManagement:view' + } + } + ] + }, + // 律所标准文件 + { + path: '/lawyer-file', + name: 'LawyerFile', + component: Layout, + meta: { + title: '律所标准文件', + icon: 'setting', + permissionIdentifier: 'LawyerFile' + }, + children: [ + { + path: 'lawFirmStandardDocuments', + name: 'LawFirmStandardDocuments', + component: () => import('@/views/calibration/lawFirmStandardDocuments/index.vue'), + meta: { + title: '律所标准文件', + permissionIdentifier: 'LawyerFile:LawFirmStandardDocuments:view' + } + } + ] + }, + { + path: '/lmportantSchedule', + name: 'LmportantSchedule', + component: Layout, + meta: { + title: '日程管理', + icon: 'setting', + permissionIdentifier: 'LmportantSchedule' + }, + children: [ + { + path: 'lmportantScheduleManagement', + name: 'LmportantScheduleManagement', + component: () => import('@/views/calibration/lmportantScheduleManagement/index.vue'), + meta: { + title: '日程管理', + permissionIdentifier: 'LmportantSchedule:LmportantScheduleManagement:view' + } + } + ] + }, + { + path: '/system', + name: 'System', + component: Layout, + meta: { + title: '制度管理', + icon: 'setting', + permissionIdentifier: 'System' + }, + children: [ + { + path: 'systemManagement', + name: 'SystemManagement', + component: () => import('@/views/calibration/systemManagement/index.vue'), + meta: { + title: '制度管理', + permissionIdentifier: 'System:SystemManagement:view' + } + } + ] + } +] + +/** + * 创建路由 + */ +const router = createRouter({ + history: createWebHashHistory(), + routes: constantRoutes, + // 刷新时,滚动条位置还原 + scrollBehavior: () => ({ left: 0, top: 0 }) +}) + +// 全局注册 router +export function setupRouter(app: App) { + app.use(router) +} + +export default router diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..8c931b8 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,67 @@ +import { LayoutMode, ComponentSize, SidebarColor, ThemeMode, LanguageEnum } from './enums' + +const { pkg } = __APP_INFO__ + +// 检查用户的操作系统是否使用深色模式 +const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)') + +export const defaultSettings: AppSettings = { + // 系统Title + title: pkg.name, + // 系统版本 + version: pkg.version, + // 是否显示设置 + showSettings: true, + // 是否显示标签视图 + showTagsView: true, + // 是否显示应用Logo + showAppLogo: true, + // 布局方式,默认为左侧布局 + layout: LayoutMode.LEFT, + // 主题,根据操作系统的色彩方案自动选择 + theme: mediaQueryList.matches ? ThemeMode.DARK : ThemeMode.LIGHT, + // 组件大小 default | medium | small | large + size: ComponentSize.DEFAULT, + // 语言 + language: LanguageEnum.ZH_CN, + // 主题颜色 - 修改此值时需同步修改 src/styles/variables.scss + themeColor: '#4080FF', + // 是否显示水印 + showWatermark: false, + // 水印内容 + watermarkContent: pkg.name, + // 侧边栏配色方案 + sidebarColorScheme: SidebarColor.CLASSIC_BLUE, + // 是否启用 AI 助手 + enableAiAssistant: false +} + +/** + * 认证功能配置 + */ +export const authConfig = { + /** + * Token自动刷新开关 + * + * true: 启用自动刷新 - ACCESS_TOKEN_INVALID时尝试刷新token + * false: 禁用自动刷新 - ACCESS_TOKEN_INVALID时直接跳转登录页 + * + * 适用场景:后端没有刷新接口或不需要自动刷新的项目可设为false + */ + enableTokenRefresh: true +} as const + +// 主题色预设 - 经典配色方案 +// 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值 +export const themeColorPresets = [ + '#4080FF', // Arco Design 蓝 - 现代感强 + '#1890FF', // Ant Design 蓝 - 经典商务 + '#409EFF', // Element Plus 蓝 - 清新自然 + '#FA8C16', // 活力橙 - 温暖友好 + '#722ED1', // 优雅紫 - 高端大气 + '#13C2C2', // 青色 - 科技感 + '#52C41A', // 成功绿 - 活力清新 + '#F5222D', // 警示红 - 醒目强烈 + '#2F54EB', // 深蓝 - 稳重专业 + '#EB2F96' // 品红 - 时尚个性 +] diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..dce5a36 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +import { createPinia } from 'pinia' + +const store = createPinia() + +// 全局注册 store +export function setupStore(app: App) { + app.use(store) +} + +export * from './modules/app-store' +export * from './modules/permission-store' +export * from './modules/settings-store' +export * from './modules/tags-view-store' +export * from './modules/user-store' +export * from './modules/dict-store' +export { store } diff --git a/src/store/modules/app-store.ts b/src/store/modules/app-store.ts new file mode 100644 index 0000000..be9980e --- /dev/null +++ b/src/store/modules/app-store.ts @@ -0,0 +1,108 @@ +import { defaultSettings } from '@/settings' + +// 导入 Element Plus 中英文语言包 +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import en from 'element-plus/es/locale/lang/en' +import { store } from '@/store' +import { DeviceEnum } from '@/enums/settings/device-enum' +import { SidebarStatus } from '@/enums/settings/layout-enum' +import { STORAGE_KEYS } from '@/constants' + +export const useAppStore = defineStore('app', () => { + // 设备类型 + const device = useStorage(STORAGE_KEYS.DEVICE, DeviceEnum.DESKTOP) + // 布局大小 + const size = useStorage(STORAGE_KEYS.SIZE, defaultSettings.size) + // 语言 + const language = useStorage(STORAGE_KEYS.LANGUAGE, defaultSettings.language) + // 侧边栏状态 + const sidebarStatus = useStorage(STORAGE_KEYS.SIDEBAR_STATUS, SidebarStatus.CLOSED) + const sidebar = reactive({ + opened: sidebarStatus.value === SidebarStatus.OPENED, + withoutAnimation: false + }) + + // 顶部菜单激活路径 + const activeTopMenuPath = useStorage(STORAGE_KEYS.ACTIVE_TOP_MENU_PATH, '') + + /** + * 根据语言标识读取对应的语言包 + */ + const locale = computed(() => { + if (language?.value == 'en') { + return en + } else { + return zhCn + } + }) + + // 切换侧边栏 + function toggleSidebar() { + sidebar.opened = !sidebar.opened + sidebarStatus.value = sidebar.opened ? SidebarStatus.OPENED : SidebarStatus.CLOSED + } + + // 关闭侧边栏 + function closeSideBar() { + sidebar.opened = false + sidebarStatus.value = SidebarStatus.CLOSED + } + + // 打开侧边栏 + function openSideBar() { + sidebar.opened = true + sidebarStatus.value = SidebarStatus.OPENED + } + + // 切换设备 + function toggleDevice(val: string) { + device.value = val + } + + /** + * 改变布局大小 + * + * @param val 布局大小 default | small | large + */ + function changeSize(val: string) { + size.value = val + } + /** + * 切换语言 + * + * @param val + */ + function changeLanguage(val: string) { + language.value = val + } + /** + * 混合模式顶部切换 + */ + function activeTopMenu(val: string) { + activeTopMenuPath.value = val + } + return { + device, + sidebar, + language, + locale, + size, + activeTopMenu, + toggleDevice, + changeSize, + changeLanguage, + toggleSidebar, + closeSideBar, + openSideBar, + activeTopMenuPath + } +}) + +/** + * 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。 + * 官方文档解释了如何在组件外部使用 Pinia Store: + * https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component + */ +export function useAppStoreHook() { + return useAppStore(store) +} diff --git a/src/store/modules/dict-store.ts b/src/store/modules/dict-store.ts new file mode 100644 index 0000000..3f2286b --- /dev/null +++ b/src/store/modules/dict-store.ts @@ -0,0 +1,79 @@ +import { store } from '@/store' +import DictAPI, { type DictItemOption } from '@/api/system/dict-api' +import { STORAGE_KEYS } from '@/constants' + +export const useDictStore = defineStore('dict', () => { + // 字典数据缓存 + const dictCache = useStorage>(STORAGE_KEYS.DICT_CACHE, {}) + + // 请求队列(防止重复请求) + const requestQueue: Record> = {} + + /** + * 缓存字典数据 + * @param dictCode 字典编码 + * @param data 字典项列表 + */ + const cacheDictItems = (dictCode: string, data: DictItemOption[]) => { + dictCache.value[dictCode] = data + } + + /** + * 加载字典数据(如果缓存中没有则请求) + * @param dictCode 字典编码 + */ + const loadDictItems = async (dictCode: string) => { + if (dictCache.value[dictCode]) return + // 防止重复请求 + if (!requestQueue[dictCode]) { + requestQueue[dictCode] = DictAPI.getDictItems(dictCode) + .then((data) => { + cacheDictItems(dictCode, data) + Reflect.deleteProperty(requestQueue, dictCode) + }) + .catch((error) => { + // 请求失败,清理队列,允许重试 + Reflect.deleteProperty(requestQueue, dictCode) + throw error + }) + } + await requestQueue[dictCode] + } + + /** + * 获取字典项列表 + * @param dictCode 字典编码 + * @returns 字典项列表 + */ + const getDictItems = (dictCode: string): DictItemOption[] => { + return dictCache.value[dictCode] || [] + } + + /** + * 移除指定字典项 + * @param dictCode 字典编码 + */ + const removeDictItem = (dictCode: string) => { + if (dictCache.value[dictCode]) { + Reflect.deleteProperty(dictCache.value, dictCode) + } + } + + /** + * 清空字典缓存 + */ + const clearDictCache = () => { + dictCache.value = {} + } + + return { + loadDictItems, + getDictItems, + removeDictItem, + clearDictCache + } +}) + +export function useDictStoreHook() { + return useDictStore(store) +} diff --git a/src/store/modules/permission-store.ts b/src/store/modules/permission-store.ts new file mode 100644 index 0000000..4120e46 --- /dev/null +++ b/src/store/modules/permission-store.ts @@ -0,0 +1,103 @@ +import type { RouteRecordRaw } from 'vue-router' +import { constantRoutes } from '@/router' +import { store } from '@/store' +import router from '@/router' + +import MenuAPI, { type RouteVO } from '@/api/system/menu-api' +const modules = import.meta.glob('../../views/**/**.vue') +const Layout = () => import('../../layouts/index.vue') + +export const usePermissionStore = defineStore('permission', () => { + // 所有路由(只使用静态路由) + const routes = ref(constantRoutes) + // 混合布局的左侧菜单路由 + const mixLayoutSideMenus = ref([]) + // 动态路由是否已生成 + const isRouteGenerated = ref(false) + + /** 生成动态路由 */ + async function generateRoutes(): Promise { + try { + const data = await MenuAPI.getRoutes() // 获取当前登录人的菜单路由 + const dynamicRoutes = transformRoutes(data) + + routes.value = [...constantRoutes, ...dynamicRoutes] + isRouteGenerated.value = true + + return dynamicRoutes + } catch (error) { + // 路由生成失败,重置状态 + isRouteGenerated.value = false + throw error + } + } + + /** 设置混合布局左侧菜单 */ + const setMixLayoutSideMenus = (parentPath: string) => { + const parentMenu = routes.value.find((item) => item.path === parentPath) + mixLayoutSideMenus.value = parentMenu?.children || [] + } + + /** 重置路由状态 */ + const resetRouter = () => { + // 移除动态添加的路由 + const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean)) + routes.value.forEach((route) => { + if (route.name && !constantRouteNames.has(route.name)) { + router.removeRoute(route.name) + } + }) + + // 重置所有状态 + routes.value = [...constantRoutes] + mixLayoutSideMenus.value = [] + isRouteGenerated.value = false + } + + return { + routes, + mixLayoutSideMenus, + isRouteGenerated, + generateRoutes, + setMixLayoutSideMenus, + resetRouter + } +}) + +/** + * 转换后端路由数据为Vue Router配置 + * 处理组件路径映射和Layout层级嵌套 + */ +const transformRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteRecordRaw[] => { + return routes.map((route) => { + const { component, children, ...args } = route + + // 处理组件:顶层或非Layout保留组件,中间层Layout设为undefined + const processedComponent = isTopLevel || component !== 'Layout' ? component : undefined + + const normalizedRoute = { ...args } as RouteRecordRaw + + if (!processedComponent) { + // 多级菜单的父级菜单,不需要组件 + normalizedRoute.component = undefined + } else { + // 动态导入组件,Layout特殊处理,找不到组件时返回404 + normalizedRoute.component = + processedComponent === 'Layout' + ? Layout + : modules[`../../views/${processedComponent}.vue`] || modules[`../../views/error/404.vue`] + } + + // 递归处理子路由 + if (children && children.length > 0) { + normalizedRoute.children = transformRoutes(children, false) + } + + return normalizedRoute + }) +} + +/** 非组件环境使用权限store */ +export function usePermissionStoreHook() { + return usePermissionStore(store) +} diff --git a/src/store/modules/settings-store.ts b/src/store/modules/settings-store.ts new file mode 100644 index 0000000..daecb10 --- /dev/null +++ b/src/store/modules/settings-store.ts @@ -0,0 +1,176 @@ +import { defaultSettings } from '@/settings' +import { SidebarColor, ThemeMode } from '@/enums/settings/theme-enum' +import type { LayoutMode } from '@/enums/settings/layout-enum' +import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from '@/utils/theme' +import { STORAGE_KEYS } from '@/constants' + +// 🎯 设置项类型定义 +interface SettingsState { + // 界面显示设置 + settingsVisible: boolean + showTagsView: boolean + showAppLogo: boolean + showWatermark: boolean + enableAiAssistant: boolean + + // 布局设置 + layout: LayoutMode + sidebarColorScheme: string + + // 主题设置 + theme: ThemeMode + themeColor: string +} + +// 🎯 可变更的设置项类型 +type MutableSetting = Exclude +type SettingValue = SettingsState[K] + +export const useSettingsStore = defineStore('setting', () => { + // 设置面板可见性 + const settingsVisible = ref(false) + + // 是否显示标签页视图 + const showTagsView = useStorage( + STORAGE_KEYS.SHOW_TAGS_VIEW, + defaultSettings.showTagsView + ) + + // 是否显示应用Logo + const showAppLogo = useStorage(STORAGE_KEYS.SHOW_APP_LOGO, defaultSettings.showAppLogo) + + // 是否显示水印 + const showWatermark = useStorage( + STORAGE_KEYS.SHOW_WATERMARK, + defaultSettings.showWatermark + ) + + // 是否启用 AI 助手 + const enableAiAssistant = useStorage( + STORAGE_KEYS.ENABLE_AI_ASSISTANT, + defaultSettings.enableAiAssistant + ) + + // 侧边栏配色方案 + const sidebarColorScheme = useStorage( + STORAGE_KEYS.SIDEBAR_COLOR_SCHEME, + defaultSettings.sidebarColorScheme + ) + + // 布局模式 + const layout = useStorage(STORAGE_KEYS.LAYOUT, defaultSettings.layout as LayoutMode) + + // 主题颜色 + const themeColor = useStorage(STORAGE_KEYS.THEME_COLOR, defaultSettings.themeColor) + + // 主题模式(亮色/暗色) + const theme = useStorage(STORAGE_KEYS.THEME, defaultSettings.theme) + + // 设置项映射,用于统一管理 + const settingsMap = { + showTagsView, + showAppLogo, + showWatermark, + enableAiAssistant, + sidebarColorScheme, + layout + } as const + + // 监听主题变化,自动应用样式 + watch( + [theme, themeColor], + ([newTheme, newThemeColor]: [ThemeMode, string]) => { + toggleDarkMode(newTheme === ThemeMode.DARK) + const colors = generateThemeColors(newThemeColor, newTheme) + applyTheme(colors) + }, + { immediate: true } + ) + + // 监听侧边栏配色变化 + watch( + [sidebarColorScheme], + ([newSidebarColorScheme]) => { + toggleSidebarColor(newSidebarColorScheme === SidebarColor.CLASSIC_BLUE) + }, + { immediate: true } + ) + + // 通用设置更新方法 + function updateSetting(key: K, value: SettingValue): void { + const setting = settingsMap[key] + if (setting) { + ;(setting as Ref).value = value + } + } + + // 主题更新方法 + function updateTheme(newTheme: ThemeMode): void { + theme.value = newTheme + } + + function updateThemeColor(newColor: string): void { + themeColor.value = newColor + } + + function updateSidebarColorScheme(newScheme: string): void { + sidebarColorScheme.value = newScheme + } + + function updateLayout(newLayout: LayoutMode): void { + layout.value = newLayout + } + + // 设置面板控制 + function toggleSettingsPanel(): void { + settingsVisible.value = !settingsVisible.value + } + + function showSettingsPanel(): void { + settingsVisible.value = true + } + + function hideSettingsPanel(): void { + settingsVisible.value = false + } + + // 重置所有设置 + function resetSettings(): void { + showTagsView.value = defaultSettings.showTagsView + showAppLogo.value = defaultSettings.showAppLogo + showWatermark.value = defaultSettings.showWatermark + enableAiAssistant.value = defaultSettings.enableAiAssistant + sidebarColorScheme.value = defaultSettings.sidebarColorScheme + layout.value = defaultSettings.layout as LayoutMode + themeColor.value = defaultSettings.themeColor + theme.value = defaultSettings.theme + } + + return { + // 状态 + settingsVisible, + showTagsView, + showAppLogo, + showWatermark, + enableAiAssistant, + sidebarColorScheme, + layout, + themeColor, + theme, + + // 更新方法 + updateSetting, + updateTheme, + updateThemeColor, + updateSidebarColorScheme, + updateLayout, + + // 面板控制 + toggleSettingsPanel, + showSettingsPanel, + hideSettingsPanel, + + // 重置功能 + resetSettings + } +}) diff --git a/src/store/modules/tags-view-store.ts b/src/store/modules/tags-view-store.ts new file mode 100644 index 0000000..2520382 --- /dev/null +++ b/src/store/modules/tags-view-store.ts @@ -0,0 +1,275 @@ +export const useTagsViewStore = defineStore('tagsView', () => { + const visitedViews = ref([]) + const cachedViews = ref([]) + const router = useRouter() + const route = useRoute() + + /** + * 添加已访问视图到已访问视图列表中 + */ + function addVisitedView(view: TagView) { + // 如果已经存在于已访问的视图列表中或者是重定向地址,则不再添加 + if (view.path.startsWith('/redirect')) { + return + } + if (visitedViews.value.some((v) => v.path === view.path)) { + return + } + // 如果视图是固定的(affix),则在已访问的视图列表的开头添加 + if (view.affix) { + visitedViews.value.unshift(view) + } else { + // 如果视图不是固定的,则在已访问的视图列表的末尾添加 + visitedViews.value.push(view) + } + } + + /** + * 添加缓存视图到缓存视图列表中 + */ + function addCachedView({ fullPath, keepAlive }: TagView) { + // 如果缓存视图名称已经存在于缓存视图列表中,则不再添加 + if (cachedViews.value.includes(fullPath)) { + return + } + + // 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中 + if (keepAlive) { + cachedViews.value.push(fullPath) + } + } + + /** + * 从已访问视图列表中删除指定的视图 + */ + function delVisitedView(view: TagView) { + return new Promise((resolve) => { + for (const [i, v] of visitedViews.value.entries()) { + // 找到与指定视图路径匹配的视图,在已访问视图列表中删除该视图 + if (v.path === view.path) { + visitedViews.value.splice(i, 1) + break + } + } + resolve([...visitedViews.value]) + }) + } + + function delCachedView(view: TagView) { + const { fullPath } = view + return new Promise((resolve) => { + const index = cachedViews.value.indexOf(fullPath) + if (index > -1) { + cachedViews.value.splice(index, 1) + } + resolve([...cachedViews.value]) + }) + } + function delOtherVisitedViews(view: TagView) { + return new Promise((resolve) => { + visitedViews.value = visitedViews.value.filter((v) => { + return v?.affix || v.path === view.path + }) + resolve([...visitedViews.value]) + }) + } + + function delOtherCachedViews(view: TagView) { + const { fullPath } = view + return new Promise((resolve) => { + const index = cachedViews.value.indexOf(fullPath) + if (index > -1) { + cachedViews.value = cachedViews.value.slice(index, index + 1) + } else { + // if index = -1, there is no cached tags + cachedViews.value = [] + } + resolve([...cachedViews.value]) + }) + } + + function updateVisitedView(view: TagView) { + for (let v of visitedViews.value) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + } + + /** + * 根据路径更新标签名称 + * @param fullPath 路径 + * @param title 标签名称 + */ + function updateTagName(fullPath: string, title: string) { + const tag = visitedViews.value.find((tag: TagView) => tag.fullPath === fullPath) + + if (tag) { + tag.title = title + } + } + + function addView(view: TagView) { + addVisitedView(view) + addCachedView(view) + } + + function delView(view: TagView) { + return new Promise((resolve) => { + delVisitedView(view) + delCachedView(view) + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value] + }) + }) + } + + function delOtherViews(view: TagView) { + return new Promise((resolve) => { + delOtherVisitedViews(view) + delOtherCachedViews(view) + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value] + }) + }) + } + + function delLeftViews(view: TagView) { + return new Promise((resolve) => { + const currIndex = visitedViews.value.findIndex((v) => v.path === view.path) + if (currIndex === -1) { + return + } + visitedViews.value = visitedViews.value.filter((item, index) => { + if (index >= currIndex || item?.affix) { + return true + } + + const cacheIndex = cachedViews.value.indexOf(item.fullPath) + if (cacheIndex > -1) { + cachedViews.value.splice(cacheIndex, 1) + } + return false + }) + resolve({ + visitedViews: [...visitedViews.value] + }) + }) + } + + function delRightViews(view: TagView) { + return new Promise((resolve) => { + const currIndex = visitedViews.value.findIndex((v) => v.path === view.path) + if (currIndex === -1) { + return + } + visitedViews.value = visitedViews.value.filter((item, index) => { + if (index <= currIndex || item?.affix) { + return true + } + const cacheIndex = cachedViews.value.indexOf(item.fullPath) + if (cacheIndex > -1) { + cachedViews.value.splice(cacheIndex, 1) + } + return false + }) + resolve({ + visitedViews: [...visitedViews.value] + }) + }) + } + + function delAllViews() { + return new Promise((resolve) => { + const affixTags = visitedViews.value.filter((tag) => tag?.affix) + visitedViews.value = affixTags + cachedViews.value = [] + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value] + }) + }) + } + + function delAllVisitedViews() { + return new Promise((resolve) => { + const affixTags = visitedViews.value.filter((tag) => tag?.affix) + visitedViews.value = affixTags + resolve([...visitedViews.value]) + }) + } + + function delAllCachedViews() { + return new Promise((resolve) => { + cachedViews.value = [] + resolve([...cachedViews.value]) + }) + } + + /** + * 关闭当前tagView + */ + function closeCurrentView() { + const tags: TagView = { + name: route.name as string, + title: route.meta.title as string, + path: route.path, + fullPath: route.fullPath, + affix: route.meta?.affix, + keepAlive: route.meta?.keepAlive, + query: route.query + } + delView(tags).then((res: any) => { + if (isActive(tags)) { + toLastView(res.visitedViews, tags) + } + }) + } + + function isActive(tag: TagView) { + return tag.path === route.path + } + + function toLastView(visitedViews: TagView[], view?: TagView) { + const latestView = visitedViews.slice(-1)[0] + if (latestView && latestView.fullPath) { + router.push(latestView.fullPath) + } 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('/redirect' + view.fullPath) + } else { + router.push('/') + } + } + } + + return { + visitedViews, + cachedViews, + addVisitedView, + addCachedView, + delVisitedView, + delCachedView, + delOtherVisitedViews, + delOtherCachedViews, + updateVisitedView, + addView, + delView, + delOtherViews, + delLeftViews, + delRightViews, + delAllViews, + delAllVisitedViews, + delAllCachedViews, + closeCurrentView, + isActive, + toLastView, + updateTagName + } +}) diff --git a/src/store/modules/user-store.ts b/src/store/modules/user-store.ts new file mode 100644 index 0000000..4719332 --- /dev/null +++ b/src/store/modules/user-store.ts @@ -0,0 +1,165 @@ +import { store } from '@/store' + +import AuthAPI from '@/api/auth-api' +import type { UserInfo } from '@/api/system/user-api' + +import { AuthStorage } from '@/utils/auth' +import { usePermissionStoreHook } from '@/store/modules/permission-store' +import { useDictStoreHook } from '@/store/modules/dict-store' +import { useTagsViewStore } from '@/store' +import { cleanupWebSocket } from '@/plugins/websocket' +import { UserGetInfo, userLogin } from '@/api/calibration/login' + +export const useUserStore = defineStore('user', () => { + // 用户信息 + const userInfo = ref({}) + // 记住我状态 + const rememberMe = ref(AuthStorage.getRememberMe()) + const permissionCharactersArr = ref([]) + + const setPermissionCharactersArr = (arr: string[]) => { + permissionCharactersArr.value = arr + } + + // 登录 + function login(LoginFormData: any) { + return new Promise((resolve, reject) => { + // 保存记住我状态和token + rememberMe.value = true + + userLogin(LoginFormData) + .then((res: any) => { + AuthStorage.setTokens(res.data.token, res.data.token, rememberMe.value) + resolve() + }) + .catch((error) => { + reject(error) + }) + }) + } + + /** + * 获取用户信息 + * + * @returns {UserInfo} 用户信息 + */ + function getUserInfo() { + return new Promise((resolve, reject) => { + UserGetInfo() + .then((res: any) => { + if (!res) { + reject('Verification failed, please Login again.') + return + } + setUserInfo(res.data) + resolve(res) + }) + .catch((error) => { + reject(error) + }) + }) + } + + const setUserInfo = (info: any) => { + userInfo.value = info + } + /** + * 登出 + */ + function logout() { + return new Promise((resolve) => { + // 重置所有系统状态 + resetAllState() + resolve() + }) + } + + /** + * 重置所有系统状态 + * 统一处理所有清理工作,包括用户凭证、路由、缓存等 + */ + function resetAllState() { + // 1. 重置用户状态 + resetUserState() + + // 2. 重置其他模块状态 + // 重置路由 + usePermissionStoreHook().resetRouter() + // 清除字典缓存 + useDictStoreHook().clearDictCache() + // 清除标签视图 + useTagsViewStore().delAllViews() + + // 3. 清理 WebSocket 连接 + + return Promise.resolve() + } + + /** + * 重置用户状态 + * 仅处理用户模块内的状态 + */ + function resetUserState() { + // 清除用户凭证 + AuthStorage.clearAuth() + // 重置用户信息 + userInfo.value = {} as UserInfo + setPermissionCharactersArr([]) + } + + /** + * 刷新 token + */ + function refreshToken() { + const refreshToken = AuthStorage.getRefreshToken() + + if (!refreshToken) { + return Promise.reject(new Error('没有有效的刷新令牌')) + } + + return new Promise((resolve, reject) => { + AuthAPI.refreshToken(refreshToken) + .then((data) => { + const { accessToken, refreshToken: newRefreshToken } = data + // 更新令牌,保持当前记住我状态 + AuthStorage.setTokens(accessToken, newRefreshToken, AuthStorage.getRememberMe()) + resolve() + }) + .catch((error) => { + console.log(' refreshToken 刷新失败', error) + reject(error) + }) + }) + } + + const checkPermission = (permission: string): boolean => { + if (userInfo.value?.permission_data.includes('*:*:*')) { + return true + } + return userInfo.value?.permission_data.includes(permission) + } + + return { + userInfo, + rememberMe, + isLoggedIn: () => !!AuthStorage.getAccessToken(), + getUserInfo, + setUserInfo, + login, + logout, + resetAllState, + resetUserState, + refreshToken, + permissionCharactersArr, + setPermissionCharactersArr, + checkPermission + } +}) + +/** + * 在组件外部使用UserStore的钩子函数 + * @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html + */ +export function useUserStoreHook() { + return useUserStore(store) +} diff --git a/src/styles/dark/css-vars.css b/src/styles/dark/css-vars.css new file mode 100644 index 0000000..823aabf --- /dev/null +++ b/src/styles/dark/css-vars.css @@ -0,0 +1,7 @@ +/* 暗黑模式通过 CSS 自定义变量,官方链接:https://element-plus.org/zh-CN/guide/dark-mode.html#%E9%80%9A%E8%BF%87-css */ +html.dark { + .el-table { + /* 自定义表格选中高亮时当前行的背景颜色 */ + --el-table-current-row-bg-color: var(--el-fill-color-light); + } +} diff --git a/src/styles/element-plus.scss b/src/styles/element-plus.scss new file mode 100644 index 0000000..fc5b729 --- /dev/null +++ b/src/styles/element-plus.scss @@ -0,0 +1,45 @@ +$border: 1px solid var(--el-border-color-light); + +/* el-dialog */ +.el-dialog { + .el-dialog__header { + padding: 15px 20px; + margin: 0; + border-bottom: $border; + } + + .el-dialog__body { + padding: 20px; + } + + .el-dialog__footer { + padding: 15px; + border-top: $border; + } +} + +/** el-drawer */ +.el-drawer { + .el-drawer__header { + padding: 15px 20px; + margin: 0; + color: inherit; + border-bottom: $border; + } + + .el-drawer__body { + padding: 20px; + } + + .el-drawer__footer { + padding: 15px; + border-top: $border; + } +} + +// 抽屉和对话框底部按钮区域 +.dialog-footer { + display: flex; + gap: 8px; + justify-content: flex-end; +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..2265506 --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,121 @@ +@use "./reset"; +@use "./element-plus"; +// Vxe Table +@use "./vxe-table"; +@import url("./vxe-table.css"); + +.app-container { + padding: 15px; +} + +// 进度条颜色 +#nprogress .bar { + background-color: var(--el-color-primary); +} + +// 混合布局左侧菜单的hover样式 +.layout-mix .layout__sidebar--left .el-menu { + .el-menu-item { + &:hover { + // 极简白主题:使用浅灰色背景 + background-color: var(--el-fill-color-light) !important; + } + } + + .el-sub-menu__title { + &:hover { + // 极简白主题:使用浅灰色背景 + background-color: var(--el-fill-color-light) !important; + } + } +} + +// 深色主题或深蓝色侧边栏配色下的左侧菜单hover样式 +html.dark .layout-mix .layout__sidebar--left .el-menu, +html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { + .el-menu-item { + &:hover { + // 深色背景:使用CSS变量 + background-color: var(--menu-hover) !important; + } + } + + .el-sub-menu__title { + &:hover { + // 深色背景:使用CSS变量 + background-color: var(--menu-hover) !important; + } + } +} + +// 窄屏时隐藏菜单文字,只显示图标 +.hideSidebar { + // Top布局和Mix布局的水平菜单 + &.layout-top .layout__header .el-menu--horizontal, + &.layout-mix .layout__header .el-menu--horizontal { + .el-menu-item, + .el-sub-menu__title { + .menu-title, + span:not([class*="i-svg"]):not(.el-icon) { + display: none !important; + } + } + } + + // Mix布局的左侧菜单 + &.layout-mix .layout__sidebar--left .el-menu { + .el-menu-item, + .el-sub-menu__title { + .menu-title, + span:not([class*="i-svg"]):not(.el-icon) { + display: none !important; + } + } + } +} + +// 全局搜索区域样式 +.search-container { + padding: 18px 16px 0; + margin-bottom: 16px; + background-color: var(--el-bg-color-overlay); + border: 1px solid var(--el-border-color-light); + border-radius: 4px; + + .search-buttons { + margin-right: 0; + } + + .el-form-item { + margin-bottom: 18px; + } +} + +// 表格区域样式 +.data-table { + margin-bottom: 16px; + + // 表格工具栏区域 + &__toolbar { + display: flex; + justify-content: space-between; + margin-bottom: 16px; + + &--actions, + &--tools { + display: flex; + gap: 8px; + } + } + + // 表格内容区域 + &__content { + margin: 8px 0; + } + + // 分页区域 + .el-pagination { + justify-content: flex-end; + margin-top: 16px; + } +} diff --git a/src/styles/reset.scss b/src/styles/reset.scss new file mode 100644 index 0000000..b3b43c7 --- /dev/null +++ b/src/styles/reset.scss @@ -0,0 +1,77 @@ +*, +::before, +::after { + box-sizing: border-box; + border-color: currentcolor; + border-style: solid; + border-width: 0; +} + +#app { + width: 100%; + height: 100%; +} + +html { + box-sizing: border-box; + width: 100%; + height: 100%; + line-height: 1.5; + tab-size: 4; + text-size-adjust: 100%; +} + +body { + width: 100%; + height: 100%; + margin: 0; + font-family: + "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", + Arial, sans-serif; + line-height: inherit; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizelegibility; +} + +a { + color: inherit; + text-decoration: inherit; +} + +img, +svg { + display: inline-block; +} + +svg { + // 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 + vertical-align: -0.15em; +} + +ul, +li { + padding: 0; + margin: 0; + list-style: none; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +a, +a:focus, +a:hover { + color: inherit; + text-decoration: none; + cursor: pointer; +} + +a:focus, +a:active, +div:focus { + outline: none; +} diff --git a/src/styles/variables.module.scss b/src/styles/variables.module.scss new file mode 100644 index 0000000..20a624d --- /dev/null +++ b/src/styles/variables.module.scss @@ -0,0 +1,11 @@ +/* stylelint-disable property-no-unknown */ +:export { + sidebar-width: $sidebar-width; + navbar-height: $navbar-height; + tags-view-height: $tags-view-height; + menu-background: $menu-background; + menu-text: $menu-text; + menu-active-text: $menu-active-text; + menu-hover: $menu-hover; +} +/* stylelint-enable property-no-unknown */ diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..c907f33 --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,93 @@ +@forward "element-plus/theme-chalk/src/common/var.scss" with ( + $colors: ( + "primary": ( + // 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor + "base": #4080ff, + ), + "success": ( + "base": #23c343, + ), + "warning": ( + "base": #ff9a2e, + ), + "danger": ( + "base": #f76560, + ), + "info": ( + "base": #a9aeb8, + ), + ), + + $bg-color: ( + "page": #f5f8fd, + ) +); + +/** 全局SCSS变量 */ + +:root { + --menu-background: #fff; // 菜单背景色 + --menu-text: #212121; // 菜单文字颜色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定 + --menu-active-text: var( + --el-menu-active-color + ); // 菜单激活文字颜色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定 + + --menu-hover: #e6f4ff; // 菜单悬停背景色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定 + --sidebar-logo-background: #f5f5f5; // 侧边栏 Logo 背景色 + --sidebar-logo-text-color: #333; // 侧边栏 Logo 文字颜色 +} + +/** 浅色主题-深蓝色侧边栏配色 */ +html.sidebar-color-blue { + --menu-background: #304156; // 菜单背景色 + --menu-text: #bfcbd9; // 菜单文字颜色 + --menu-active-text: var(--el-menu-active-color); // 菜单激活文字颜色 + --menu-hover: #263445; // 菜单悬停背景色 + --sidebar-logo-background: #2d3748; // 侧边栏 Logo 背景色 + --sidebar-logo-text-color: #fff; // 侧边栏 Logo 文字颜色 +} + +/** 暗黑主题 */ +html.dark { + --menu-background: var(--el-bg-color-overlay); + --menu-text: #fff; + --menu-active-text: var(--el-menu-active-color); + --menu-hover: rgb(0 0 0 / 20%); + --sidebar-logo-background: rgb(0 0 0 / 20%); + --sidebar-logo-text-color: #fff; + + /** WangEditor Dark */ + /* Textarea - css vars */ + --w-e-textarea-bg-color: var(--el-bg-color); /* 深色背景 */ + --w-e-textarea-color: var(--el-text-color-primary); /* 浅色文字 */ + --w-e-textarea-border-color: var(--el-border-color); /* 较深的边框颜色 */ + --w-e-textarea-slight-border-color: var(--el-border-color-lighter); /* 更淡一些的边框颜色 */ + --w-e-textarea-slight-color: var(--el-text-color-secondary); /* 浅灰色,用于不那么重要的元素 */ + --w-e-textarea-slight-bg-color: var(--el-bg-color-overlay); /* 稍微亮一点的背景色 */ + --w-e-textarea-selected-border-color: var(--el-color-info-light-5); /* 选中元素时的高亮边框 */ + --w-e-textarea-handler-bg-color: var(--el-color-primary); /* 工具按钮或交互元素的背景色 */ + + /* Toolbar - css vars */ + --w-e-toolbar-color: var(--el-text-color-regular); /* 工具栏文字颜色 */ + --w-e-toolbar-bg-color: var(--el-bg-color); /* 工具栏背景颜色 */ + --w-e-toolbar-active-color: var(--el-text-color-primary); /* 当前激活项的文字颜色 */ + --w-e-toolbar-active-bg-color: var(--el-fill-color-light); /* 当前激活项的背景颜色 */ + --w-e-toolbar-disabled-color: var(--el-text-color-secondary); /* 禁用项的颜色 */ + --w-e-toolbar-border-color: var(--el-border-color-base); /* 工具栏边框颜色 */ + + /* Modal - css vars */ + --w-e-modal-button-bg-color: var(--el-bg-color-light-3); /* 弹出框按钮背景色 */ + --w-e-modal-button-border-color: var(--el-border-color-light); /* 弹出框按钮边框颜色 */ +} + +$menu-background: var(--menu-background); // 菜单背景色 +$menu-text: var(--menu-text); // 菜单文字颜色 +$menu-active-text: var(--menu-active-text); // 菜单激活文字颜色 +$menu-hover: var(--menu-hover); // 菜单悬停背景色 +$sidebar-logo-background: var(--sidebar-logo-background); // 侧边栏 Logo 背景色 +$sidebar-logo-text-color: var(--sidebar-logo-text-color); // 侧边栏 Logo 文字颜色 + +$sidebar-width: 210px; // 侧边栏宽度 +$sidebar-width-collapsed: 54px; // 侧边栏收缩宽度 +$navbar-height: 50px; // 导航栏高度 +$tags-view-height: 34px; // TagsView 高度 diff --git a/src/styles/vxe-table.css b/src/styles/vxe-table.css new file mode 100644 index 0000000..fc1fa4b --- /dev/null +++ b/src/styles/vxe-table.css @@ -0,0 +1,92 @@ +/** + * @description 所有主题模式下的 Vxe Table CSS 变量 + * @description 用 Element Plus 的 CSS 变量来覆写 Vxe Table 的 CSS 变量,目的是使 Vxe Table 支持多主题模式且样式统一 + * @description 在此查阅所有可自定义的变量:https://github.com/x-extends/vxe-table/blob/master/styles/css-variable.scss + */ + +:root { + /* color */ + --vxe-font-color: var(--el-text-color-regular); + --vxe-primary-color: var(--el-color-primary); + --vxe-success-color: var(--el-color-success); + --vxe-info-color: var(--el-color-info); + --vxe-warning-color: var(--el-color-warning); + --vxe-danger-color: var(--el-color-danger); + --vxe-font-lighten-color: var(--el-text-color-primary); + --vxe-primary-lighten-color: var(--el-color-primary-light-3); + --vxe-success-lighten-color: var(--el-color-success-light-3); + --vxe-info-lighten-color: var(--el-color-info-light-3); + --vxe-warning-lighten-color: var(--el-color-warning-light-3); + --vxe-danger-lighten-color: var(--el-color-danger-light-3); + --vxe-font-darken-color: var(--el-text-color-secondary); + --vxe-primary-darken-color: var(--el-color-primary-dark-2); + --vxe-success-darken-color: var(--el-color-success-dark-2); + --vxe-info-darken-color: var(--el-color-info-dark-2); + --vxe-warning-darken-color: var(--el-color-warning-dark-2); + --vxe-danger-darken-color: var(--el-color-danger-dark-2); + --vxe-font-disabled-color: var(--el-text-color-disabled); + --vxe-primary-disabled-color: var(--el-color-primary-light-5); + --vxe-success-disabled-color: var(--el-color-success-light-5); + --vxe-info-disabled-color: var(--el-color-info-light-5); + --vxe-warning-disabled-color: var(--el-color-warning-light-5); + --vxe-danger-disabled-color: var(--el-color-danger-light-5); + + /* input/radio/checkbox */ + --vxe-input-border-color: var(--el-border-color); + --vxe-input-disabled-color: var(--el-text-color-disabled); + --vxe-input-disabled-background-color: var(--el-fill-color-light); + --vxe-input-placeholder-color: var(--el-text-color-placeholder); + + /* popup */ + --vxe-table-popup-border-color: var(--el-border-color); + + /* table */ + --vxe-table-header-font-color: var(--el-text-color-regular); + --vxe-table-footer-font-color: var(--el-text-color-regular); + --vxe-table-border-color: var(--el-border-color-lighter); + --vxe-table-header-background-color: var(--el-bg-color); + --vxe-table-body-background-color: var(--el-bg-color); + --vxe-table-footer-background-color: var(--el-bg-color); + --vxe-table-row-hover-background-color: var(--el-fill-color-light); + --vxe-table-row-current-background-color: var(--el-fill-color-light); + --vxe-table-row-hover-current-background-color: var(--el-fill-color-light); + --vxe-table-checkbox-range-background-color: var(--el-fill-color-light); + + /* menu */ + --vxe-table-menu-background-color: var(--el-bg-color-overlay); + + /* loading */ + --vxe-loading-color: var(--el-color-primary); + --vxe-loading-background-color: var(--el-mask-color); + + /* validate */ + --vxe-table-validate-error-color: var(--el-color-danger); + + /* toolbar */ + --vxe-toolbar-background-color: var(--el-bg-color); + --vxe-toolbar-custom-active-background-color: var(--el-bg-color-overlay); + --vxe-toolbar-panel-background-color: var(--el-bg-color-overlay); + + /* pager */ + --vxe-pager-background-color: var(--el-bg-color); + + /* modal */ + --vxe-modal-header-background-color: var(--el-bg-color); + --vxe-modal-body-background-color: var(--el-bg-color); + --vxe-modal-border-color: var(--el-border-color); + + /* button */ + --vxe-button-default-background-color: var(--el-bg-color-overlay); + + /* input */ + --vxe-input-background-color: var(--el-fill-color-blank); + --vxe-input-panel-background-color: var(--el-fill-color-blank); + + /* form */ + --vxe-form-background-color: var(--el-bg-color); + --vxe-form-validate-error-color: var(--el-color-danger); + + /* select */ + --vxe-select-option-hover-background-color: var(--el-bg-color-overlay); + --vxe-select-panel-background-color: var(--el-bg-color); +} diff --git a/src/styles/vxe-table.scss b/src/styles/vxe-table.scss new file mode 100644 index 0000000..f7376ee --- /dev/null +++ b/src/styles/vxe-table.scss @@ -0,0 +1,39 @@ +// 自定义 Vxe Table 样式 + +.vxe-grid { + // 表单 + &--form-wrapper { + .vxe-form { + padding: 10px 20px; + margin-bottom: 20px; + } + } + + // 工具栏 + &--toolbar-wrapper { + .vxe-toolbar { + padding: 20px; + } + } + + // 分页 + &--pager-wrapper { + .vxe-pager { + height: 70px; + padding: 0 20px; + + &--wrapper { + // 参考 Bootstrap 的响应式设计 WIDTH = 768 + @media screen and (width <= 768px) { + .vxe-pager--total, + .vxe-pager--sizes, + .vxe-pager--jump, + .vxe-pager--jump-prev, + .vxe-pager--jump-next { + display: none; + } + } + } + } + } +} diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts new file mode 100644 index 0000000..20b192e --- /dev/null +++ b/src/types/auto-imports.d.ts @@ -0,0 +1,991 @@ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const EffectScope: (typeof import("vue"))["EffectScope"]; + const ElForm: (typeof import("element-plus/es"))["ElForm"]; + const ElMessage: (typeof import("element-plus/es"))["ElMessage"]; + const ElMessageBox: (typeof import("element-plus/es"))["ElMessageBox"]; + const ElNotification: (typeof import("element-plus/es"))["ElNotification"]; + const ElTree: (typeof import("element-plus/es"))["ElTree"]; + const acceptHMRUpdate: (typeof import("pinia"))["acceptHMRUpdate"]; + const asyncComputed: (typeof import("@vueuse/core"))["asyncComputed"]; + const autoResetRef: (typeof import("@vueuse/core"))["autoResetRef"]; + const computed: (typeof import("vue"))["computed"]; + const computedAsync: (typeof import("@vueuse/core"))["computedAsync"]; + const computedEager: (typeof import("@vueuse/core"))["computedEager"]; + const computedInject: (typeof import("@vueuse/core"))["computedInject"]; + const computedWithControl: (typeof import("@vueuse/core"))["computedWithControl"]; + const controlledComputed: (typeof import("@vueuse/core"))["controlledComputed"]; + const controlledRef: (typeof import("@vueuse/core"))["controlledRef"]; + const createApp: (typeof import("vue"))["createApp"]; + const createEventHook: (typeof import("@vueuse/core"))["createEventHook"]; + const createGlobalState: (typeof import("@vueuse/core"))["createGlobalState"]; + const createInjectionState: (typeof import("@vueuse/core"))["createInjectionState"]; + const createPinia: (typeof import("pinia"))["createPinia"]; + const createReactiveFn: (typeof import("@vueuse/core"))["createReactiveFn"]; + const createReusableTemplate: (typeof import("@vueuse/core"))["createReusableTemplate"]; + const createSharedComposable: (typeof import("@vueuse/core"))["createSharedComposable"]; + const createTemplatePromise: (typeof import("@vueuse/core"))["createTemplatePromise"]; + const createUnrefFn: (typeof import("@vueuse/core"))["createUnrefFn"]; + const customRef: (typeof import("vue"))["customRef"]; + const debouncedRef: (typeof import("@vueuse/core"))["debouncedRef"]; + const debouncedWatch: (typeof import("@vueuse/core"))["debouncedWatch"]; + const defineAsyncComponent: (typeof import("vue"))["defineAsyncComponent"]; + const defineComponent: (typeof import("vue"))["defineComponent"]; + const defineStore: (typeof import("pinia"))["defineStore"]; + const eagerComputed: (typeof import("@vueuse/core"))["eagerComputed"]; + const effectScope: (typeof import("vue"))["effectScope"]; + const extendRef: (typeof import("@vueuse/core"))["extendRef"]; + const getActivePinia: (typeof import("pinia"))["getActivePinia"]; + const getCurrentInstance: (typeof import("vue"))["getCurrentInstance"]; + const getCurrentScope: (typeof import("vue"))["getCurrentScope"]; + const h: (typeof import("vue"))["h"]; + const ignorableWatch: (typeof import("@vueuse/core"))["ignorableWatch"]; + const inject: (typeof import("vue"))["inject"]; + const injectLocal: (typeof import("@vueuse/core"))["injectLocal"]; + const isDefined: (typeof import("@vueuse/core"))["isDefined"]; + const isProxy: (typeof import("vue"))["isProxy"]; + const isReactive: (typeof import("vue"))["isReactive"]; + const isReadonly: (typeof import("vue"))["isReadonly"]; + const isRef: (typeof import("vue"))["isRef"]; + const makeDestructurable: (typeof import("@vueuse/core"))["makeDestructurable"]; + const mapActions: (typeof import("pinia"))["mapActions"]; + const mapGetters: (typeof import("pinia"))["mapGetters"]; + const mapState: (typeof import("pinia"))["mapState"]; + const mapStores: (typeof import("pinia"))["mapStores"]; + const mapWritableState: (typeof import("pinia"))["mapWritableState"]; + const markRaw: (typeof import("vue"))["markRaw"]; + const nextTick: (typeof import("vue"))["nextTick"]; + const onActivated: (typeof import("vue"))["onActivated"]; + const onBeforeMount: (typeof import("vue"))["onBeforeMount"]; + const onBeforeRouteLeave: (typeof import("vue-router"))["onBeforeRouteLeave"]; + const onBeforeRouteUpdate: (typeof import("vue-router"))["onBeforeRouteUpdate"]; + const onBeforeUnmount: (typeof import("vue"))["onBeforeUnmount"]; + const onBeforeUpdate: (typeof import("vue"))["onBeforeUpdate"]; + const onClickOutside: (typeof import("@vueuse/core"))["onClickOutside"]; + const onDeactivated: (typeof import("vue"))["onDeactivated"]; + const onErrorCaptured: (typeof import("vue"))["onErrorCaptured"]; + const onKeyStroke: (typeof import("@vueuse/core"))["onKeyStroke"]; + const onLongPress: (typeof import("@vueuse/core"))["onLongPress"]; + const onMounted: (typeof import("vue"))["onMounted"]; + const onRenderTracked: (typeof import("vue"))["onRenderTracked"]; + const onRenderTriggered: (typeof import("vue"))["onRenderTriggered"]; + const onScopeDispose: (typeof import("vue"))["onScopeDispose"]; + const onServerPrefetch: (typeof import("vue"))["onServerPrefetch"]; + const onStartTyping: (typeof import("@vueuse/core"))["onStartTyping"]; + const onUnmounted: (typeof import("vue"))["onUnmounted"]; + const onUpdated: (typeof import("vue"))["onUpdated"]; + const pausableWatch: (typeof import("@vueuse/core"))["pausableWatch"]; + const provide: (typeof import("vue"))["provide"]; + const provideLocal: (typeof import("@vueuse/core"))["provideLocal"]; + const reactify: (typeof import("@vueuse/core"))["reactify"]; + const reactifyObject: (typeof import("@vueuse/core"))["reactifyObject"]; + const reactive: (typeof import("vue"))["reactive"]; + const reactiveComputed: (typeof import("@vueuse/core"))["reactiveComputed"]; + const reactiveOmit: (typeof import("@vueuse/core"))["reactiveOmit"]; + const reactivePick: (typeof import("@vueuse/core"))["reactivePick"]; + const readonly: (typeof import("vue"))["readonly"]; + const ref: (typeof import("vue"))["ref"]; + const refAutoReset: (typeof import("@vueuse/core"))["refAutoReset"]; + const refDebounced: (typeof import("@vueuse/core"))["refDebounced"]; + const refDefault: (typeof import("@vueuse/core"))["refDefault"]; + const refThrottled: (typeof import("@vueuse/core"))["refThrottled"]; + const refWithControl: (typeof import("@vueuse/core"))["refWithControl"]; + const resolveComponent: (typeof import("vue"))["resolveComponent"]; + const resolveRef: (typeof import("@vueuse/core"))["resolveRef"]; + const resolveUnref: (typeof import("@vueuse/core"))["resolveUnref"]; + const setActivePinia: (typeof import("pinia"))["setActivePinia"]; + const setMapStoreSuffix: (typeof import("pinia"))["setMapStoreSuffix"]; + const shallowReactive: (typeof import("vue"))["shallowReactive"]; + const shallowReadonly: (typeof import("vue"))["shallowReadonly"]; + const shallowRef: (typeof import("vue"))["shallowRef"]; + const storeToRefs: (typeof import("pinia"))["storeToRefs"]; + const syncRef: (typeof import("@vueuse/core"))["syncRef"]; + const syncRefs: (typeof import("@vueuse/core"))["syncRefs"]; + const templateRef: (typeof import("@vueuse/core"))["templateRef"]; + const throttledRef: (typeof import("@vueuse/core"))["throttledRef"]; + const throttledWatch: (typeof import("@vueuse/core"))["throttledWatch"]; + const toRaw: (typeof import("vue"))["toRaw"]; + const toReactive: (typeof import("@vueuse/core"))["toReactive"]; + const toRef: (typeof import("vue"))["toRef"]; + const toRefs: (typeof import("vue"))["toRefs"]; + const toValue: (typeof import("vue"))["toValue"]; + const triggerRef: (typeof import("vue"))["triggerRef"]; + const tryOnBeforeMount: (typeof import("@vueuse/core"))["tryOnBeforeMount"]; + const tryOnBeforeUnmount: (typeof import("@vueuse/core"))["tryOnBeforeUnmount"]; + const tryOnMounted: (typeof import("@vueuse/core"))["tryOnMounted"]; + const tryOnScopeDispose: (typeof import("@vueuse/core"))["tryOnScopeDispose"]; + const tryOnUnmounted: (typeof import("@vueuse/core"))["tryOnUnmounted"]; + const unref: (typeof import("vue"))["unref"]; + const unrefElement: (typeof import("@vueuse/core"))["unrefElement"]; + const until: (typeof import("@vueuse/core"))["until"]; + const useActiveElement: (typeof import("@vueuse/core"))["useActiveElement"]; + const useAnimate: (typeof import("@vueuse/core"))["useAnimate"]; + const useArrayDifference: (typeof import("@vueuse/core"))["useArrayDifference"]; + const useArrayEvery: (typeof import("@vueuse/core"))["useArrayEvery"]; + const useArrayFilter: (typeof import("@vueuse/core"))["useArrayFilter"]; + const useArrayFind: (typeof import("@vueuse/core"))["useArrayFind"]; + const useArrayFindIndex: (typeof import("@vueuse/core"))["useArrayFindIndex"]; + const useArrayFindLast: (typeof import("@vueuse/core"))["useArrayFindLast"]; + const useArrayIncludes: (typeof import("@vueuse/core"))["useArrayIncludes"]; + const useArrayJoin: (typeof import("@vueuse/core"))["useArrayJoin"]; + const useArrayMap: (typeof import("@vueuse/core"))["useArrayMap"]; + const useArrayReduce: (typeof import("@vueuse/core"))["useArrayReduce"]; + const useArraySome: (typeof import("@vueuse/core"))["useArraySome"]; + const useArrayUnique: (typeof import("@vueuse/core"))["useArrayUnique"]; + const useAsyncQueue: (typeof import("@vueuse/core"))["useAsyncQueue"]; + const useAsyncState: (typeof import("@vueuse/core"))["useAsyncState"]; + const useAttrs: (typeof import("vue"))["useAttrs"]; + const useBase64: (typeof import("@vueuse/core"))["useBase64"]; + const useBattery: (typeof import("@vueuse/core"))["useBattery"]; + const useBluetooth: (typeof import("@vueuse/core"))["useBluetooth"]; + const useBreakpoints: (typeof import("@vueuse/core"))["useBreakpoints"]; + const useBroadcastChannel: (typeof import("@vueuse/core"))["useBroadcastChannel"]; + const useBrowserLocation: (typeof import("@vueuse/core"))["useBrowserLocation"]; + const useCached: (typeof import("@vueuse/core"))["useCached"]; + const useClipboard: (typeof import("@vueuse/core"))["useClipboard"]; + const useClipboardItems: (typeof import("@vueuse/core"))["useClipboardItems"]; + const useCloned: (typeof import("@vueuse/core"))["useCloned"]; + const useColorMode: (typeof import("@vueuse/core"))["useColorMode"]; + const useConfirmDialog: (typeof import("@vueuse/core"))["useConfirmDialog"]; + const useCounter: (typeof import("@vueuse/core"))["useCounter"]; + const useCssModule: (typeof import("vue"))["useCssModule"]; + const useCssVar: (typeof import("@vueuse/core"))["useCssVar"]; + const useCssVars: (typeof import("vue"))["useCssVars"]; + const useCurrentElement: (typeof import("@vueuse/core"))["useCurrentElement"]; + const useCycleList: (typeof import("@vueuse/core"))["useCycleList"]; + const useDark: (typeof import("@vueuse/core"))["useDark"]; + const useDateFormat: (typeof import("@vueuse/core"))["useDateFormat"]; + const useDebounce: (typeof import("@vueuse/core"))["useDebounce"]; + const useDebounceFn: (typeof import("@vueuse/core"))["useDebounceFn"]; + const useDebouncedRefHistory: (typeof import("@vueuse/core"))["useDebouncedRefHistory"]; + const useDeviceMotion: (typeof import("@vueuse/core"))["useDeviceMotion"]; + const useDeviceOrientation: (typeof import("@vueuse/core"))["useDeviceOrientation"]; + const useDevicePixelRatio: (typeof import("@vueuse/core"))["useDevicePixelRatio"]; + const useDevicesList: (typeof import("@vueuse/core"))["useDevicesList"]; + const useDisplayMedia: (typeof import("@vueuse/core"))["useDisplayMedia"]; + const useDocumentVisibility: (typeof import("@vueuse/core"))["useDocumentVisibility"]; + const useDraggable: (typeof import("@vueuse/core"))["useDraggable"]; + const useDropZone: (typeof import("@vueuse/core"))["useDropZone"]; + const useElementBounding: (typeof import("@vueuse/core"))["useElementBounding"]; + const useElementByPoint: (typeof import("@vueuse/core"))["useElementByPoint"]; + const useElementHover: (typeof import("@vueuse/core"))["useElementHover"]; + const useElementSize: (typeof import("@vueuse/core"))["useElementSize"]; + const useElementVisibility: (typeof import("@vueuse/core"))["useElementVisibility"]; + const useEventBus: (typeof import("@vueuse/core"))["useEventBus"]; + const useEventListener: (typeof import("@vueuse/core"))["useEventListener"]; + const useEventSource: (typeof import("@vueuse/core"))["useEventSource"]; + const useEyeDropper: (typeof import("@vueuse/core"))["useEyeDropper"]; + const useFavicon: (typeof import("@vueuse/core"))["useFavicon"]; + const useFetch: (typeof import("@vueuse/core"))["useFetch"]; + const useFileDialog: (typeof import("@vueuse/core"))["useFileDialog"]; + const useFileSystemAccess: (typeof import("@vueuse/core"))["useFileSystemAccess"]; + const useFocus: (typeof import("@vueuse/core"))["useFocus"]; + const useFocusWithin: (typeof import("@vueuse/core"))["useFocusWithin"]; + const useFps: (typeof import("@vueuse/core"))["useFps"]; + const useFullscreen: (typeof import("@vueuse/core"))["useFullscreen"]; + const useGamepad: (typeof import("@vueuse/core"))["useGamepad"]; + const useGeolocation: (typeof import("@vueuse/core"))["useGeolocation"]; + const useI18n: (typeof import("vue-i18n"))["useI18n"]; + const useIdle: (typeof import("@vueuse/core"))["useIdle"]; + const useImage: (typeof import("@vueuse/core"))["useImage"]; + const useInfiniteScroll: (typeof import("@vueuse/core"))["useInfiniteScroll"]; + const useIntersectionObserver: (typeof import("@vueuse/core"))["useIntersectionObserver"]; + const useInterval: (typeof import("@vueuse/core"))["useInterval"]; + const useIntervalFn: (typeof import("@vueuse/core"))["useIntervalFn"]; + const useKeyModifier: (typeof import("@vueuse/core"))["useKeyModifier"]; + const useLastChanged: (typeof import("@vueuse/core"))["useLastChanged"]; + const useLink: (typeof import("vue-router"))["useLink"]; + const useLocalStorage: (typeof import("@vueuse/core"))["useLocalStorage"]; + const useMagicKeys: (typeof import("@vueuse/core"))["useMagicKeys"]; + const useManualRefHistory: (typeof import("@vueuse/core"))["useManualRefHistory"]; + const useMediaControls: (typeof import("@vueuse/core"))["useMediaControls"]; + const useMediaQuery: (typeof import("@vueuse/core"))["useMediaQuery"]; + const useMemoize: (typeof import("@vueuse/core"))["useMemoize"]; + const useMemory: (typeof import("@vueuse/core"))["useMemory"]; + const useMounted: (typeof import("@vueuse/core"))["useMounted"]; + const useMouse: (typeof import("@vueuse/core"))["useMouse"]; + const useMouseInElement: (typeof import("@vueuse/core"))["useMouseInElement"]; + const useMousePressed: (typeof import("@vueuse/core"))["useMousePressed"]; + const useMutationObserver: (typeof import("@vueuse/core"))["useMutationObserver"]; + const useNavigatorLanguage: (typeof import("@vueuse/core"))["useNavigatorLanguage"]; + const useNetwork: (typeof import("@vueuse/core"))["useNetwork"]; + const useNow: (typeof import("@vueuse/core"))["useNow"]; + const useObjectUrl: (typeof import("@vueuse/core"))["useObjectUrl"]; + const useOffsetPagination: (typeof import("@vueuse/core"))["useOffsetPagination"]; + const useOnline: (typeof import("@vueuse/core"))["useOnline"]; + const usePageLeave: (typeof import("@vueuse/core"))["usePageLeave"]; + const useParallax: (typeof import("@vueuse/core"))["useParallax"]; + const useParentElement: (typeof import("@vueuse/core"))["useParentElement"]; + const usePerformanceObserver: (typeof import("@vueuse/core"))["usePerformanceObserver"]; + const usePermission: (typeof import("@vueuse/core"))["usePermission"]; + const usePointer: (typeof import("@vueuse/core"))["usePointer"]; + const usePointerLock: (typeof import("@vueuse/core"))["usePointerLock"]; + const usePointerSwipe: (typeof import("@vueuse/core"))["usePointerSwipe"]; + const usePreferredColorScheme: (typeof import("@vueuse/core"))["usePreferredColorScheme"]; + const usePreferredContrast: (typeof import("@vueuse/core"))["usePreferredContrast"]; + const usePreferredDark: (typeof import("@vueuse/core"))["usePreferredDark"]; + const usePreferredLanguages: (typeof import("@vueuse/core"))["usePreferredLanguages"]; + const usePreferredReducedMotion: (typeof import("@vueuse/core"))["usePreferredReducedMotion"]; + const usePrevious: (typeof import("@vueuse/core"))["usePrevious"]; + const useRafFn: (typeof import("@vueuse/core"))["useRafFn"]; + const useRefHistory: (typeof import("@vueuse/core"))["useRefHistory"]; + const useResizeObserver: (typeof import("@vueuse/core"))["useResizeObserver"]; + const useRoute: (typeof import("vue-router"))["useRoute"]; + const useRouter: (typeof import("vue-router"))["useRouter"]; + const useScreenOrientation: (typeof import("@vueuse/core"))["useScreenOrientation"]; + const useScreenSafeArea: (typeof import("@vueuse/core"))["useScreenSafeArea"]; + const useScriptTag: (typeof import("@vueuse/core"))["useScriptTag"]; + const useScroll: (typeof import("@vueuse/core"))["useScroll"]; + const useScrollLock: (typeof import("@vueuse/core"))["useScrollLock"]; + const useSessionStorage: (typeof import("@vueuse/core"))["useSessionStorage"]; + const useShare: (typeof import("@vueuse/core"))["useShare"]; + const useSlots: (typeof import("vue"))["useSlots"]; + const useSorted: (typeof import("@vueuse/core"))["useSorted"]; + const useSpeechRecognition: (typeof import("@vueuse/core"))["useSpeechRecognition"]; + const useSpeechSynthesis: (typeof import("@vueuse/core"))["useSpeechSynthesis"]; + const useStepper: (typeof import("@vueuse/core"))["useStepper"]; + const useStorage: (typeof import("@vueuse/core"))["useStorage"]; + const useStorageAsync: (typeof import("@vueuse/core"))["useStorageAsync"]; + const useStyleTag: (typeof import("@vueuse/core"))["useStyleTag"]; + const useSupported: (typeof import("@vueuse/core"))["useSupported"]; + const useSwipe: (typeof import("@vueuse/core"))["useSwipe"]; + const useTemplateRefsList: (typeof import("@vueuse/core"))["useTemplateRefsList"]; + const useTextDirection: (typeof import("@vueuse/core"))["useTextDirection"]; + const useTextSelection: (typeof import("@vueuse/core"))["useTextSelection"]; + const useTextareaAutosize: (typeof import("@vueuse/core"))["useTextareaAutosize"]; + const useThrottle: (typeof import("@vueuse/core"))["useThrottle"]; + const useThrottleFn: (typeof import("@vueuse/core"))["useThrottleFn"]; + const useThrottledRefHistory: (typeof import("@vueuse/core"))["useThrottledRefHistory"]; + const useTimeAgo: (typeof import("@vueuse/core"))["useTimeAgo"]; + const useTimeout: (typeof import("@vueuse/core"))["useTimeout"]; + const useTimeoutFn: (typeof import("@vueuse/core"))["useTimeoutFn"]; + const useTimeoutPoll: (typeof import("@vueuse/core"))["useTimeoutPoll"]; + const useTimestamp: (typeof import("@vueuse/core"))["useTimestamp"]; + const useTitle: (typeof import("@vueuse/core"))["useTitle"]; + const useToNumber: (typeof import("@vueuse/core"))["useToNumber"]; + const useToString: (typeof import("@vueuse/core"))["useToString"]; + const useToggle: (typeof import("@vueuse/core"))["useToggle"]; + const useTransition: (typeof import("@vueuse/core"))["useTransition"]; + const useUrlSearchParams: (typeof import("@vueuse/core"))["useUrlSearchParams"]; + const useUserMedia: (typeof import("@vueuse/core"))["useUserMedia"]; + const useVModel: (typeof import("@vueuse/core"))["useVModel"]; + const useVModels: (typeof import("@vueuse/core"))["useVModels"]; + const useVibrate: (typeof import("@vueuse/core"))["useVibrate"]; + const useVirtualList: (typeof import("@vueuse/core"))["useVirtualList"]; + const useWakeLock: (typeof import("@vueuse/core"))["useWakeLock"]; + const useWebNotification: (typeof import("@vueuse/core"))["useWebNotification"]; + const useWebSocket: (typeof import("@vueuse/core"))["useWebSocket"]; + const useWebWorker: (typeof import("@vueuse/core"))["useWebWorker"]; + const useWebWorkerFn: (typeof import("@vueuse/core"))["useWebWorkerFn"]; + const useWindowFocus: (typeof import("@vueuse/core"))["useWindowFocus"]; + const useWindowScroll: (typeof import("@vueuse/core"))["useWindowScroll"]; + const useWindowSize: (typeof import("@vueuse/core"))["useWindowSize"]; + const watch: (typeof import("vue"))["watch"]; + const watchArray: (typeof import("@vueuse/core"))["watchArray"]; + const watchAtMost: (typeof import("@vueuse/core"))["watchAtMost"]; + const watchDebounced: (typeof import("@vueuse/core"))["watchDebounced"]; + const watchDeep: (typeof import("@vueuse/core"))["watchDeep"]; + const watchEffect: (typeof import("vue"))["watchEffect"]; + const watchIgnorable: (typeof import("@vueuse/core"))["watchIgnorable"]; + const watchImmediate: (typeof import("@vueuse/core"))["watchImmediate"]; + const watchOnce: (typeof import("@vueuse/core"))["watchOnce"]; + const watchPausable: (typeof import("@vueuse/core"))["watchPausable"]; + const watchPostEffect: (typeof import("vue"))["watchPostEffect"]; + const watchSyncEffect: (typeof import("vue"))["watchSyncEffect"]; + const watchThrottled: (typeof import("@vueuse/core"))["watchThrottled"]; + const watchTriggerable: (typeof import("@vueuse/core"))["watchTriggerable"]; + const watchWithFilter: (typeof import("@vueuse/core"))["watchWithFilter"]; + const whenever: (typeof import("@vueuse/core"))["whenever"]; +} +// for type re-export +declare global { + // @ts-ignore + export type { + Component, + ComponentPublicInstance, + ComputedRef, + ExtractDefaultPropTypes, + ExtractPropTypes, + ExtractPublicPropTypes, + InjectionKey, + PropType, + Ref, + VNode, + WritableComputedRef, + } from "vue"; + import("vue"); +} +// for vue template auto import +import { UnwrapRef } from "vue"; +declare module "vue" { + interface GlobalComponents {} + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef<(typeof import("vue"))["EffectScope"]>; + readonly ElMessage: UnwrapRef<(typeof import("element-plus/es"))["ElMessage"]>; + readonly ElMessageBox: UnwrapRef<(typeof import("element-plus/es"))["ElMessageBox"]>; + readonly acceptHMRUpdate: UnwrapRef<(typeof import("pinia"))["acceptHMRUpdate"]>; + readonly asyncComputed: UnwrapRef<(typeof import("@vueuse/core"))["asyncComputed"]>; + readonly autoResetRef: UnwrapRef<(typeof import("@vueuse/core"))["autoResetRef"]>; + readonly computed: UnwrapRef<(typeof import("vue"))["computed"]>; + readonly computedAsync: UnwrapRef<(typeof import("@vueuse/core"))["computedAsync"]>; + readonly computedEager: UnwrapRef<(typeof import("@vueuse/core"))["computedEager"]>; + readonly computedInject: UnwrapRef<(typeof import("@vueuse/core"))["computedInject"]>; + readonly computedWithControl: UnwrapRef<(typeof import("@vueuse/core"))["computedWithControl"]>; + readonly controlledComputed: UnwrapRef<(typeof import("@vueuse/core"))["controlledComputed"]>; + readonly controlledRef: UnwrapRef<(typeof import("@vueuse/core"))["controlledRef"]>; + readonly createApp: UnwrapRef<(typeof import("vue"))["createApp"]>; + readonly createEventHook: UnwrapRef<(typeof import("@vueuse/core"))["createEventHook"]>; + readonly createGlobalState: UnwrapRef<(typeof import("@vueuse/core"))["createGlobalState"]>; + readonly createInjectionState: UnwrapRef< + (typeof import("@vueuse/core"))["createInjectionState"] + >; + readonly createPinia: UnwrapRef<(typeof import("pinia"))["createPinia"]>; + readonly createReactiveFn: UnwrapRef<(typeof import("@vueuse/core"))["createReactiveFn"]>; + readonly createReusableTemplate: UnwrapRef< + (typeof import("@vueuse/core"))["createReusableTemplate"] + >; + readonly createSharedComposable: UnwrapRef< + (typeof import("@vueuse/core"))["createSharedComposable"] + >; + readonly createTemplatePromise: UnwrapRef< + (typeof import("@vueuse/core"))["createTemplatePromise"] + >; + readonly createUnrefFn: UnwrapRef<(typeof import("@vueuse/core"))["createUnrefFn"]>; + readonly customRef: UnwrapRef<(typeof import("vue"))["customRef"]>; + readonly debouncedRef: UnwrapRef<(typeof import("@vueuse/core"))["debouncedRef"]>; + readonly debouncedWatch: UnwrapRef<(typeof import("@vueuse/core"))["debouncedWatch"]>; + readonly defineAsyncComponent: UnwrapRef<(typeof import("vue"))["defineAsyncComponent"]>; + readonly defineComponent: UnwrapRef<(typeof import("vue"))["defineComponent"]>; + readonly defineStore: UnwrapRef<(typeof import("pinia"))["defineStore"]>; + readonly eagerComputed: UnwrapRef<(typeof import("@vueuse/core"))["eagerComputed"]>; + readonly effectScope: UnwrapRef<(typeof import("vue"))["effectScope"]>; + readonly extendRef: UnwrapRef<(typeof import("@vueuse/core"))["extendRef"]>; + readonly getActivePinia: UnwrapRef<(typeof import("pinia"))["getActivePinia"]>; + readonly getCurrentInstance: UnwrapRef<(typeof import("vue"))["getCurrentInstance"]>; + readonly getCurrentScope: UnwrapRef<(typeof import("vue"))["getCurrentScope"]>; + readonly h: UnwrapRef<(typeof import("vue"))["h"]>; + readonly ignorableWatch: UnwrapRef<(typeof import("@vueuse/core"))["ignorableWatch"]>; + readonly inject: UnwrapRef<(typeof import("vue"))["inject"]>; + readonly injectLocal: UnwrapRef<(typeof import("@vueuse/core"))["injectLocal"]>; + readonly isDefined: UnwrapRef<(typeof import("@vueuse/core"))["isDefined"]>; + readonly isProxy: UnwrapRef<(typeof import("vue"))["isProxy"]>; + readonly isReactive: UnwrapRef<(typeof import("vue"))["isReactive"]>; + readonly isReadonly: UnwrapRef<(typeof import("vue"))["isReadonly"]>; + readonly isRef: UnwrapRef<(typeof import("vue"))["isRef"]>; + readonly makeDestructurable: UnwrapRef<(typeof import("@vueuse/core"))["makeDestructurable"]>; + readonly mapActions: UnwrapRef<(typeof import("pinia"))["mapActions"]>; + readonly mapGetters: UnwrapRef<(typeof import("pinia"))["mapGetters"]>; + readonly mapState: UnwrapRef<(typeof import("pinia"))["mapState"]>; + readonly mapStores: UnwrapRef<(typeof import("pinia"))["mapStores"]>; + readonly mapWritableState: UnwrapRef<(typeof import("pinia"))["mapWritableState"]>; + readonly markRaw: UnwrapRef<(typeof import("vue"))["markRaw"]>; + readonly nextTick: UnwrapRef<(typeof import("vue"))["nextTick"]>; + readonly onActivated: UnwrapRef<(typeof import("vue"))["onActivated"]>; + readonly onBeforeMount: UnwrapRef<(typeof import("vue"))["onBeforeMount"]>; + readonly onBeforeRouteLeave: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteLeave"]>; + readonly onBeforeRouteUpdate: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteUpdate"]>; + readonly onBeforeUnmount: UnwrapRef<(typeof import("vue"))["onBeforeUnmount"]>; + readonly onBeforeUpdate: UnwrapRef<(typeof import("vue"))["onBeforeUpdate"]>; + readonly onClickOutside: UnwrapRef<(typeof import("@vueuse/core"))["onClickOutside"]>; + readonly onDeactivated: UnwrapRef<(typeof import("vue"))["onDeactivated"]>; + readonly onErrorCaptured: UnwrapRef<(typeof import("vue"))["onErrorCaptured"]>; + readonly onKeyStroke: UnwrapRef<(typeof import("@vueuse/core"))["onKeyStroke"]>; + readonly onLongPress: UnwrapRef<(typeof import("@vueuse/core"))["onLongPress"]>; + readonly onMounted: UnwrapRef<(typeof import("vue"))["onMounted"]>; + readonly onRenderTracked: UnwrapRef<(typeof import("vue"))["onRenderTracked"]>; + readonly onRenderTriggered: UnwrapRef<(typeof import("vue"))["onRenderTriggered"]>; + readonly onScopeDispose: UnwrapRef<(typeof import("vue"))["onScopeDispose"]>; + readonly onServerPrefetch: UnwrapRef<(typeof import("vue"))["onServerPrefetch"]>; + readonly onStartTyping: UnwrapRef<(typeof import("@vueuse/core"))["onStartTyping"]>; + readonly onUnmounted: UnwrapRef<(typeof import("vue"))["onUnmounted"]>; + readonly onUpdated: UnwrapRef<(typeof import("vue"))["onUpdated"]>; + readonly pausableWatch: UnwrapRef<(typeof import("@vueuse/core"))["pausableWatch"]>; + readonly provide: UnwrapRef<(typeof import("vue"))["provide"]>; + readonly provideLocal: UnwrapRef<(typeof import("@vueuse/core"))["provideLocal"]>; + readonly reactify: UnwrapRef<(typeof import("@vueuse/core"))["reactify"]>; + readonly reactifyObject: UnwrapRef<(typeof import("@vueuse/core"))["reactifyObject"]>; + readonly reactive: UnwrapRef<(typeof import("vue"))["reactive"]>; + readonly reactiveComputed: UnwrapRef<(typeof import("@vueuse/core"))["reactiveComputed"]>; + readonly reactiveOmit: UnwrapRef<(typeof import("@vueuse/core"))["reactiveOmit"]>; + readonly reactivePick: UnwrapRef<(typeof import("@vueuse/core"))["reactivePick"]>; + readonly readonly: UnwrapRef<(typeof import("vue"))["readonly"]>; + readonly ref: UnwrapRef<(typeof import("vue"))["ref"]>; + readonly refAutoReset: UnwrapRef<(typeof import("@vueuse/core"))["refAutoReset"]>; + readonly refDebounced: UnwrapRef<(typeof import("@vueuse/core"))["refDebounced"]>; + readonly refDefault: UnwrapRef<(typeof import("@vueuse/core"))["refDefault"]>; + readonly refThrottled: UnwrapRef<(typeof import("@vueuse/core"))["refThrottled"]>; + readonly refWithControl: UnwrapRef<(typeof import("@vueuse/core"))["refWithControl"]>; + readonly resolveComponent: UnwrapRef<(typeof import("vue"))["resolveComponent"]>; + readonly resolveRef: UnwrapRef<(typeof import("@vueuse/core"))["resolveRef"]>; + readonly resolveUnref: UnwrapRef<(typeof import("@vueuse/core"))["resolveUnref"]>; + readonly setActivePinia: UnwrapRef<(typeof import("pinia"))["setActivePinia"]>; + readonly setMapStoreSuffix: UnwrapRef<(typeof import("pinia"))["setMapStoreSuffix"]>; + readonly shallowReactive: UnwrapRef<(typeof import("vue"))["shallowReactive"]>; + readonly shallowReadonly: UnwrapRef<(typeof import("vue"))["shallowReadonly"]>; + readonly shallowRef: UnwrapRef<(typeof import("vue"))["shallowRef"]>; + readonly storeToRefs: UnwrapRef<(typeof import("pinia"))["storeToRefs"]>; + readonly syncRef: UnwrapRef<(typeof import("@vueuse/core"))["syncRef"]>; + readonly syncRefs: UnwrapRef<(typeof import("@vueuse/core"))["syncRefs"]>; + readonly templateRef: UnwrapRef<(typeof import("@vueuse/core"))["templateRef"]>; + readonly throttledRef: UnwrapRef<(typeof import("@vueuse/core"))["throttledRef"]>; + readonly throttledWatch: UnwrapRef<(typeof import("@vueuse/core"))["throttledWatch"]>; + readonly toRaw: UnwrapRef<(typeof import("vue"))["toRaw"]>; + readonly toReactive: UnwrapRef<(typeof import("@vueuse/core"))["toReactive"]>; + readonly toRef: UnwrapRef<(typeof import("vue"))["toRef"]>; + readonly toRefs: UnwrapRef<(typeof import("vue"))["toRefs"]>; + readonly toValue: UnwrapRef<(typeof import("vue"))["toValue"]>; + readonly triggerRef: UnwrapRef<(typeof import("vue"))["triggerRef"]>; + readonly tryOnBeforeMount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeMount"]>; + readonly tryOnBeforeUnmount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeUnmount"]>; + readonly tryOnMounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnMounted"]>; + readonly tryOnScopeDispose: UnwrapRef<(typeof import("@vueuse/core"))["tryOnScopeDispose"]>; + readonly tryOnUnmounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnUnmounted"]>; + readonly unref: UnwrapRef<(typeof import("vue"))["unref"]>; + readonly unrefElement: UnwrapRef<(typeof import("@vueuse/core"))["unrefElement"]>; + readonly until: UnwrapRef<(typeof import("@vueuse/core"))["until"]>; + readonly useActiveElement: UnwrapRef<(typeof import("@vueuse/core"))["useActiveElement"]>; + readonly useAnimate: UnwrapRef<(typeof import("@vueuse/core"))["useAnimate"]>; + readonly useArrayDifference: UnwrapRef<(typeof import("@vueuse/core"))["useArrayDifference"]>; + readonly useArrayEvery: UnwrapRef<(typeof import("@vueuse/core"))["useArrayEvery"]>; + readonly useArrayFilter: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFilter"]>; + readonly useArrayFind: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFind"]>; + readonly useArrayFindIndex: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindIndex"]>; + readonly useArrayFindLast: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindLast"]>; + readonly useArrayIncludes: UnwrapRef<(typeof import("@vueuse/core"))["useArrayIncludes"]>; + readonly useArrayJoin: UnwrapRef<(typeof import("@vueuse/core"))["useArrayJoin"]>; + readonly useArrayMap: UnwrapRef<(typeof import("@vueuse/core"))["useArrayMap"]>; + readonly useArrayReduce: UnwrapRef<(typeof import("@vueuse/core"))["useArrayReduce"]>; + readonly useArraySome: UnwrapRef<(typeof import("@vueuse/core"))["useArraySome"]>; + readonly useArrayUnique: UnwrapRef<(typeof import("@vueuse/core"))["useArrayUnique"]>; + readonly useAsyncQueue: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncQueue"]>; + readonly useAsyncState: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncState"]>; + readonly useAttrs: UnwrapRef<(typeof import("vue"))["useAttrs"]>; + readonly useBase64: UnwrapRef<(typeof import("@vueuse/core"))["useBase64"]>; + readonly useBattery: UnwrapRef<(typeof import("@vueuse/core"))["useBattery"]>; + readonly useBluetooth: UnwrapRef<(typeof import("@vueuse/core"))["useBluetooth"]>; + readonly useBreakpoints: UnwrapRef<(typeof import("@vueuse/core"))["useBreakpoints"]>; + readonly useBroadcastChannel: UnwrapRef<(typeof import("@vueuse/core"))["useBroadcastChannel"]>; + readonly useBrowserLocation: UnwrapRef<(typeof import("@vueuse/core"))["useBrowserLocation"]>; + readonly useCached: UnwrapRef<(typeof import("@vueuse/core"))["useCached"]>; + readonly useClipboard: UnwrapRef<(typeof import("@vueuse/core"))["useClipboard"]>; + readonly useClipboardItems: UnwrapRef<(typeof import("@vueuse/core"))["useClipboardItems"]>; + readonly useCloned: UnwrapRef<(typeof import("@vueuse/core"))["useCloned"]>; + readonly useColorMode: UnwrapRef<(typeof import("@vueuse/core"))["useColorMode"]>; + readonly useConfirmDialog: UnwrapRef<(typeof import("@vueuse/core"))["useConfirmDialog"]>; + readonly useCounter: UnwrapRef<(typeof import("@vueuse/core"))["useCounter"]>; + readonly useCssModule: UnwrapRef<(typeof import("vue"))["useCssModule"]>; + readonly useCssVar: UnwrapRef<(typeof import("@vueuse/core"))["useCssVar"]>; + readonly useCssVars: UnwrapRef<(typeof import("vue"))["useCssVars"]>; + readonly useCurrentElement: UnwrapRef<(typeof import("@vueuse/core"))["useCurrentElement"]>; + readonly useCycleList: UnwrapRef<(typeof import("@vueuse/core"))["useCycleList"]>; + readonly useDark: UnwrapRef<(typeof import("@vueuse/core"))["useDark"]>; + readonly useDateFormat: UnwrapRef<(typeof import("@vueuse/core"))["useDateFormat"]>; + readonly useDebounce: UnwrapRef<(typeof import("@vueuse/core"))["useDebounce"]>; + readonly useDebounceFn: UnwrapRef<(typeof import("@vueuse/core"))["useDebounceFn"]>; + readonly useDebouncedRefHistory: UnwrapRef< + (typeof import("@vueuse/core"))["useDebouncedRefHistory"] + >; + readonly useDeviceMotion: UnwrapRef<(typeof import("@vueuse/core"))["useDeviceMotion"]>; + readonly useDeviceOrientation: UnwrapRef< + (typeof import("@vueuse/core"))["useDeviceOrientation"] + >; + readonly useDevicePixelRatio: UnwrapRef<(typeof import("@vueuse/core"))["useDevicePixelRatio"]>; + readonly useDevicesList: UnwrapRef<(typeof import("@vueuse/core"))["useDevicesList"]>; + readonly useDisplayMedia: UnwrapRef<(typeof import("@vueuse/core"))["useDisplayMedia"]>; + readonly useDocumentVisibility: UnwrapRef< + (typeof import("@vueuse/core"))["useDocumentVisibility"] + >; + readonly useDraggable: UnwrapRef<(typeof import("@vueuse/core"))["useDraggable"]>; + readonly useDropZone: UnwrapRef<(typeof import("@vueuse/core"))["useDropZone"]>; + readonly useElementBounding: UnwrapRef<(typeof import("@vueuse/core"))["useElementBounding"]>; + readonly useElementByPoint: UnwrapRef<(typeof import("@vueuse/core"))["useElementByPoint"]>; + readonly useElementHover: UnwrapRef<(typeof import("@vueuse/core"))["useElementHover"]>; + readonly useElementSize: UnwrapRef<(typeof import("@vueuse/core"))["useElementSize"]>; + readonly useElementVisibility: UnwrapRef< + (typeof import("@vueuse/core"))["useElementVisibility"] + >; + readonly useEventBus: UnwrapRef<(typeof import("@vueuse/core"))["useEventBus"]>; + readonly useEventListener: UnwrapRef<(typeof import("@vueuse/core"))["useEventListener"]>; + readonly useEventSource: UnwrapRef<(typeof import("@vueuse/core"))["useEventSource"]>; + readonly useEyeDropper: UnwrapRef<(typeof import("@vueuse/core"))["useEyeDropper"]>; + readonly useFavicon: UnwrapRef<(typeof import("@vueuse/core"))["useFavicon"]>; + readonly useFetch: UnwrapRef<(typeof import("@vueuse/core"))["useFetch"]>; + readonly useFileDialog: UnwrapRef<(typeof import("@vueuse/core"))["useFileDialog"]>; + readonly useFileSystemAccess: UnwrapRef<(typeof import("@vueuse/core"))["useFileSystemAccess"]>; + readonly useFocus: UnwrapRef<(typeof import("@vueuse/core"))["useFocus"]>; + readonly useFocusWithin: UnwrapRef<(typeof import("@vueuse/core"))["useFocusWithin"]>; + readonly useFps: UnwrapRef<(typeof import("@vueuse/core"))["useFps"]>; + readonly useFullscreen: UnwrapRef<(typeof import("@vueuse/core"))["useFullscreen"]>; + readonly useGamepad: UnwrapRef<(typeof import("@vueuse/core"))["useGamepad"]>; + readonly useGeolocation: UnwrapRef<(typeof import("@vueuse/core"))["useGeolocation"]>; + readonly useI18n: UnwrapRef<(typeof import("vue-i18n"))["useI18n"]>; + readonly useIdle: UnwrapRef<(typeof import("@vueuse/core"))["useIdle"]>; + readonly useImage: UnwrapRef<(typeof import("@vueuse/core"))["useImage"]>; + readonly useInfiniteScroll: UnwrapRef<(typeof import("@vueuse/core"))["useInfiniteScroll"]>; + readonly useIntersectionObserver: UnwrapRef< + (typeof import("@vueuse/core"))["useIntersectionObserver"] + >; + readonly useInterval: UnwrapRef<(typeof import("@vueuse/core"))["useInterval"]>; + readonly useIntervalFn: UnwrapRef<(typeof import("@vueuse/core"))["useIntervalFn"]>; + readonly useKeyModifier: UnwrapRef<(typeof import("@vueuse/core"))["useKeyModifier"]>; + readonly useLastChanged: UnwrapRef<(typeof import("@vueuse/core"))["useLastChanged"]>; + readonly useLink: UnwrapRef<(typeof import("vue-router"))["useLink"]>; + readonly useLocalStorage: UnwrapRef<(typeof import("@vueuse/core"))["useLocalStorage"]>; + readonly useMagicKeys: UnwrapRef<(typeof import("@vueuse/core"))["useMagicKeys"]>; + readonly useManualRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useManualRefHistory"]>; + readonly useMediaControls: UnwrapRef<(typeof import("@vueuse/core"))["useMediaControls"]>; + readonly useMediaQuery: UnwrapRef<(typeof import("@vueuse/core"))["useMediaQuery"]>; + readonly useMemoize: UnwrapRef<(typeof import("@vueuse/core"))["useMemoize"]>; + readonly useMemory: UnwrapRef<(typeof import("@vueuse/core"))["useMemory"]>; + readonly useMounted: UnwrapRef<(typeof import("@vueuse/core"))["useMounted"]>; + readonly useMouse: UnwrapRef<(typeof import("@vueuse/core"))["useMouse"]>; + readonly useMouseInElement: UnwrapRef<(typeof import("@vueuse/core"))["useMouseInElement"]>; + readonly useMousePressed: UnwrapRef<(typeof import("@vueuse/core"))["useMousePressed"]>; + readonly useMutationObserver: UnwrapRef<(typeof import("@vueuse/core"))["useMutationObserver"]>; + readonly useNavigatorLanguage: UnwrapRef< + (typeof import("@vueuse/core"))["useNavigatorLanguage"] + >; + readonly useNetwork: UnwrapRef<(typeof import("@vueuse/core"))["useNetwork"]>; + readonly useNow: UnwrapRef<(typeof import("@vueuse/core"))["useNow"]>; + readonly useObjectUrl: UnwrapRef<(typeof import("@vueuse/core"))["useObjectUrl"]>; + readonly useOffsetPagination: UnwrapRef<(typeof import("@vueuse/core"))["useOffsetPagination"]>; + readonly useOnline: UnwrapRef<(typeof import("@vueuse/core"))["useOnline"]>; + readonly usePageLeave: UnwrapRef<(typeof import("@vueuse/core"))["usePageLeave"]>; + readonly useParallax: UnwrapRef<(typeof import("@vueuse/core"))["useParallax"]>; + readonly useParentElement: UnwrapRef<(typeof import("@vueuse/core"))["useParentElement"]>; + readonly usePerformanceObserver: UnwrapRef< + (typeof import("@vueuse/core"))["usePerformanceObserver"] + >; + readonly usePermission: UnwrapRef<(typeof import("@vueuse/core"))["usePermission"]>; + readonly usePointer: UnwrapRef<(typeof import("@vueuse/core"))["usePointer"]>; + readonly usePointerLock: UnwrapRef<(typeof import("@vueuse/core"))["usePointerLock"]>; + readonly usePointerSwipe: UnwrapRef<(typeof import("@vueuse/core"))["usePointerSwipe"]>; + readonly usePreferredColorScheme: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredColorScheme"] + >; + readonly usePreferredContrast: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredContrast"] + >; + readonly usePreferredDark: UnwrapRef<(typeof import("@vueuse/core"))["usePreferredDark"]>; + readonly usePreferredLanguages: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredLanguages"] + >; + readonly usePreferredReducedMotion: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredReducedMotion"] + >; + readonly usePrevious: UnwrapRef<(typeof import("@vueuse/core"))["usePrevious"]>; + readonly useRafFn: UnwrapRef<(typeof import("@vueuse/core"))["useRafFn"]>; + readonly useRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useRefHistory"]>; + readonly useResizeObserver: UnwrapRef<(typeof import("@vueuse/core"))["useResizeObserver"]>; + readonly useRoute: UnwrapRef<(typeof import("vue-router"))["useRoute"]>; + readonly useRouter: UnwrapRef<(typeof import("vue-router"))["useRouter"]>; + readonly useScreenOrientation: UnwrapRef< + (typeof import("@vueuse/core"))["useScreenOrientation"] + >; + readonly useScreenSafeArea: UnwrapRef<(typeof import("@vueuse/core"))["useScreenSafeArea"]>; + readonly useScriptTag: UnwrapRef<(typeof import("@vueuse/core"))["useScriptTag"]>; + readonly useScroll: UnwrapRef<(typeof import("@vueuse/core"))["useScroll"]>; + readonly useScrollLock: UnwrapRef<(typeof import("@vueuse/core"))["useScrollLock"]>; + readonly useSessionStorage: UnwrapRef<(typeof import("@vueuse/core"))["useSessionStorage"]>; + readonly useShare: UnwrapRef<(typeof import("@vueuse/core"))["useShare"]>; + readonly useSlots: UnwrapRef<(typeof import("vue"))["useSlots"]>; + readonly useSorted: UnwrapRef<(typeof import("@vueuse/core"))["useSorted"]>; + readonly useSpeechRecognition: UnwrapRef< + (typeof import("@vueuse/core"))["useSpeechRecognition"] + >; + readonly useSpeechSynthesis: UnwrapRef<(typeof import("@vueuse/core"))["useSpeechSynthesis"]>; + readonly useStepper: UnwrapRef<(typeof import("@vueuse/core"))["useStepper"]>; + readonly useStorage: UnwrapRef<(typeof import("@vueuse/core"))["useStorage"]>; + readonly useStorageAsync: UnwrapRef<(typeof import("@vueuse/core"))["useStorageAsync"]>; + readonly useStyleTag: UnwrapRef<(typeof import("@vueuse/core"))["useStyleTag"]>; + readonly useSupported: UnwrapRef<(typeof import("@vueuse/core"))["useSupported"]>; + readonly useSwipe: UnwrapRef<(typeof import("@vueuse/core"))["useSwipe"]>; + readonly useTemplateRefsList: UnwrapRef<(typeof import("@vueuse/core"))["useTemplateRefsList"]>; + readonly useTextDirection: UnwrapRef<(typeof import("@vueuse/core"))["useTextDirection"]>; + readonly useTextSelection: UnwrapRef<(typeof import("@vueuse/core"))["useTextSelection"]>; + readonly useTextareaAutosize: UnwrapRef<(typeof import("@vueuse/core"))["useTextareaAutosize"]>; + readonly useThrottle: UnwrapRef<(typeof import("@vueuse/core"))["useThrottle"]>; + readonly useThrottleFn: UnwrapRef<(typeof import("@vueuse/core"))["useThrottleFn"]>; + readonly useThrottledRefHistory: UnwrapRef< + (typeof import("@vueuse/core"))["useThrottledRefHistory"] + >; + readonly useTimeAgo: UnwrapRef<(typeof import("@vueuse/core"))["useTimeAgo"]>; + readonly useTimeout: UnwrapRef<(typeof import("@vueuse/core"))["useTimeout"]>; + readonly useTimeoutFn: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutFn"]>; + readonly useTimeoutPoll: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutPoll"]>; + readonly useTimestamp: UnwrapRef<(typeof import("@vueuse/core"))["useTimestamp"]>; + readonly useTitle: UnwrapRef<(typeof import("@vueuse/core"))["useTitle"]>; + readonly useToNumber: UnwrapRef<(typeof import("@vueuse/core"))["useToNumber"]>; + readonly useToString: UnwrapRef<(typeof import("@vueuse/core"))["useToString"]>; + readonly useToggle: UnwrapRef<(typeof import("@vueuse/core"))["useToggle"]>; + readonly useTransition: UnwrapRef<(typeof import("@vueuse/core"))["useTransition"]>; + readonly useUrlSearchParams: UnwrapRef<(typeof import("@vueuse/core"))["useUrlSearchParams"]>; + readonly useUserMedia: UnwrapRef<(typeof import("@vueuse/core"))["useUserMedia"]>; + readonly useVModel: UnwrapRef<(typeof import("@vueuse/core"))["useVModel"]>; + readonly useVModels: UnwrapRef<(typeof import("@vueuse/core"))["useVModels"]>; + readonly useVibrate: UnwrapRef<(typeof import("@vueuse/core"))["useVibrate"]>; + readonly useVirtualList: UnwrapRef<(typeof import("@vueuse/core"))["useVirtualList"]>; + readonly useWakeLock: UnwrapRef<(typeof import("@vueuse/core"))["useWakeLock"]>; + readonly useWebNotification: UnwrapRef<(typeof import("@vueuse/core"))["useWebNotification"]>; + readonly useWebSocket: UnwrapRef<(typeof import("@vueuse/core"))["useWebSocket"]>; + readonly useWebWorker: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorker"]>; + readonly useWebWorkerFn: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorkerFn"]>; + readonly useWindowFocus: UnwrapRef<(typeof import("@vueuse/core"))["useWindowFocus"]>; + readonly useWindowScroll: UnwrapRef<(typeof import("@vueuse/core"))["useWindowScroll"]>; + readonly useWindowSize: UnwrapRef<(typeof import("@vueuse/core"))["useWindowSize"]>; + readonly watch: UnwrapRef<(typeof import("vue"))["watch"]>; + readonly watchArray: UnwrapRef<(typeof import("@vueuse/core"))["watchArray"]>; + readonly watchAtMost: UnwrapRef<(typeof import("@vueuse/core"))["watchAtMost"]>; + readonly watchDebounced: UnwrapRef<(typeof import("@vueuse/core"))["watchDebounced"]>; + readonly watchDeep: UnwrapRef<(typeof import("@vueuse/core"))["watchDeep"]>; + readonly watchEffect: UnwrapRef<(typeof import("vue"))["watchEffect"]>; + readonly watchIgnorable: UnwrapRef<(typeof import("@vueuse/core"))["watchIgnorable"]>; + readonly watchImmediate: UnwrapRef<(typeof import("@vueuse/core"))["watchImmediate"]>; + readonly watchOnce: UnwrapRef<(typeof import("@vueuse/core"))["watchOnce"]>; + readonly watchPausable: UnwrapRef<(typeof import("@vueuse/core"))["watchPausable"]>; + readonly watchPostEffect: UnwrapRef<(typeof import("vue"))["watchPostEffect"]>; + readonly watchSyncEffect: UnwrapRef<(typeof import("vue"))["watchSyncEffect"]>; + readonly watchThrottled: UnwrapRef<(typeof import("@vueuse/core"))["watchThrottled"]>; + readonly watchTriggerable: UnwrapRef<(typeof import("@vueuse/core"))["watchTriggerable"]>; + readonly watchWithFilter: UnwrapRef<(typeof import("@vueuse/core"))["watchWithFilter"]>; + readonly whenever: UnwrapRef<(typeof import("@vueuse/core"))["whenever"]>; + } +} +declare module "@vue/runtime-core" { + interface GlobalComponents {} + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef<(typeof import("vue"))["EffectScope"]>; + readonly ElMessage: UnwrapRef<(typeof import("element-plus/es"))["ElMessage"]>; + readonly ElMessageBox: UnwrapRef<(typeof import("element-plus/es"))["ElMessageBox"]>; + readonly acceptHMRUpdate: UnwrapRef<(typeof import("pinia"))["acceptHMRUpdate"]>; + readonly asyncComputed: UnwrapRef<(typeof import("@vueuse/core"))["asyncComputed"]>; + readonly autoResetRef: UnwrapRef<(typeof import("@vueuse/core"))["autoResetRef"]>; + readonly computed: UnwrapRef<(typeof import("vue"))["computed"]>; + readonly computedAsync: UnwrapRef<(typeof import("@vueuse/core"))["computedAsync"]>; + readonly computedEager: UnwrapRef<(typeof import("@vueuse/core"))["computedEager"]>; + readonly computedInject: UnwrapRef<(typeof import("@vueuse/core"))["computedInject"]>; + readonly computedWithControl: UnwrapRef<(typeof import("@vueuse/core"))["computedWithControl"]>; + readonly controlledComputed: UnwrapRef<(typeof import("@vueuse/core"))["controlledComputed"]>; + readonly controlledRef: UnwrapRef<(typeof import("@vueuse/core"))["controlledRef"]>; + readonly createApp: UnwrapRef<(typeof import("vue"))["createApp"]>; + readonly createEventHook: UnwrapRef<(typeof import("@vueuse/core"))["createEventHook"]>; + readonly createGlobalState: UnwrapRef<(typeof import("@vueuse/core"))["createGlobalState"]>; + readonly createInjectionState: UnwrapRef< + (typeof import("@vueuse/core"))["createInjectionState"] + >; + readonly createPinia: UnwrapRef<(typeof import("pinia"))["createPinia"]>; + readonly createReactiveFn: UnwrapRef<(typeof import("@vueuse/core"))["createReactiveFn"]>; + readonly createReusableTemplate: UnwrapRef< + (typeof import("@vueuse/core"))["createReusableTemplate"] + >; + readonly createSharedComposable: UnwrapRef< + (typeof import("@vueuse/core"))["createSharedComposable"] + >; + readonly createTemplatePromise: UnwrapRef< + (typeof import("@vueuse/core"))["createTemplatePromise"] + >; + readonly createUnrefFn: UnwrapRef<(typeof import("@vueuse/core"))["createUnrefFn"]>; + readonly customRef: UnwrapRef<(typeof import("vue"))["customRef"]>; + readonly debouncedRef: UnwrapRef<(typeof import("@vueuse/core"))["debouncedRef"]>; + readonly debouncedWatch: UnwrapRef<(typeof import("@vueuse/core"))["debouncedWatch"]>; + readonly defineAsyncComponent: UnwrapRef<(typeof import("vue"))["defineAsyncComponent"]>; + readonly defineComponent: UnwrapRef<(typeof import("vue"))["defineComponent"]>; + readonly defineStore: UnwrapRef<(typeof import("pinia"))["defineStore"]>; + readonly eagerComputed: UnwrapRef<(typeof import("@vueuse/core"))["eagerComputed"]>; + readonly effectScope: UnwrapRef<(typeof import("vue"))["effectScope"]>; + readonly extendRef: UnwrapRef<(typeof import("@vueuse/core"))["extendRef"]>; + readonly getActivePinia: UnwrapRef<(typeof import("pinia"))["getActivePinia"]>; + readonly getCurrentInstance: UnwrapRef<(typeof import("vue"))["getCurrentInstance"]>; + readonly getCurrentScope: UnwrapRef<(typeof import("vue"))["getCurrentScope"]>; + readonly h: UnwrapRef<(typeof import("vue"))["h"]>; + readonly ignorableWatch: UnwrapRef<(typeof import("@vueuse/core"))["ignorableWatch"]>; + readonly inject: UnwrapRef<(typeof import("vue"))["inject"]>; + readonly injectLocal: UnwrapRef<(typeof import("@vueuse/core"))["injectLocal"]>; + readonly isDefined: UnwrapRef<(typeof import("@vueuse/core"))["isDefined"]>; + readonly isProxy: UnwrapRef<(typeof import("vue"))["isProxy"]>; + readonly isReactive: UnwrapRef<(typeof import("vue"))["isReactive"]>; + readonly isReadonly: UnwrapRef<(typeof import("vue"))["isReadonly"]>; + readonly isRef: UnwrapRef<(typeof import("vue"))["isRef"]>; + readonly makeDestructurable: UnwrapRef<(typeof import("@vueuse/core"))["makeDestructurable"]>; + readonly mapActions: UnwrapRef<(typeof import("pinia"))["mapActions"]>; + readonly mapGetters: UnwrapRef<(typeof import("pinia"))["mapGetters"]>; + readonly mapState: UnwrapRef<(typeof import("pinia"))["mapState"]>; + readonly mapStores: UnwrapRef<(typeof import("pinia"))["mapStores"]>; + readonly mapWritableState: UnwrapRef<(typeof import("pinia"))["mapWritableState"]>; + readonly markRaw: UnwrapRef<(typeof import("vue"))["markRaw"]>; + readonly nextTick: UnwrapRef<(typeof import("vue"))["nextTick"]>; + readonly onActivated: UnwrapRef<(typeof import("vue"))["onActivated"]>; + readonly onBeforeMount: UnwrapRef<(typeof import("vue"))["onBeforeMount"]>; + readonly onBeforeRouteLeave: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteLeave"]>; + readonly onBeforeRouteUpdate: UnwrapRef<(typeof import("vue-router"))["onBeforeRouteUpdate"]>; + readonly onBeforeUnmount: UnwrapRef<(typeof import("vue"))["onBeforeUnmount"]>; + readonly onBeforeUpdate: UnwrapRef<(typeof import("vue"))["onBeforeUpdate"]>; + readonly onClickOutside: UnwrapRef<(typeof import("@vueuse/core"))["onClickOutside"]>; + readonly onDeactivated: UnwrapRef<(typeof import("vue"))["onDeactivated"]>; + readonly onErrorCaptured: UnwrapRef<(typeof import("vue"))["onErrorCaptured"]>; + readonly onKeyStroke: UnwrapRef<(typeof import("@vueuse/core"))["onKeyStroke"]>; + readonly onLongPress: UnwrapRef<(typeof import("@vueuse/core"))["onLongPress"]>; + readonly onMounted: UnwrapRef<(typeof import("vue"))["onMounted"]>; + readonly onRenderTracked: UnwrapRef<(typeof import("vue"))["onRenderTracked"]>; + readonly onRenderTriggered: UnwrapRef<(typeof import("vue"))["onRenderTriggered"]>; + readonly onScopeDispose: UnwrapRef<(typeof import("vue"))["onScopeDispose"]>; + readonly onServerPrefetch: UnwrapRef<(typeof import("vue"))["onServerPrefetch"]>; + readonly onStartTyping: UnwrapRef<(typeof import("@vueuse/core"))["onStartTyping"]>; + readonly onUnmounted: UnwrapRef<(typeof import("vue"))["onUnmounted"]>; + readonly onUpdated: UnwrapRef<(typeof import("vue"))["onUpdated"]>; + readonly pausableWatch: UnwrapRef<(typeof import("@vueuse/core"))["pausableWatch"]>; + readonly provide: UnwrapRef<(typeof import("vue"))["provide"]>; + readonly provideLocal: UnwrapRef<(typeof import("@vueuse/core"))["provideLocal"]>; + readonly reactify: UnwrapRef<(typeof import("@vueuse/core"))["reactify"]>; + readonly reactifyObject: UnwrapRef<(typeof import("@vueuse/core"))["reactifyObject"]>; + readonly reactive: UnwrapRef<(typeof import("vue"))["reactive"]>; + readonly reactiveComputed: UnwrapRef<(typeof import("@vueuse/core"))["reactiveComputed"]>; + readonly reactiveOmit: UnwrapRef<(typeof import("@vueuse/core"))["reactiveOmit"]>; + readonly reactivePick: UnwrapRef<(typeof import("@vueuse/core"))["reactivePick"]>; + readonly readonly: UnwrapRef<(typeof import("vue"))["readonly"]>; + readonly ref: UnwrapRef<(typeof import("vue"))["ref"]>; + readonly refAutoReset: UnwrapRef<(typeof import("@vueuse/core"))["refAutoReset"]>; + readonly refDebounced: UnwrapRef<(typeof import("@vueuse/core"))["refDebounced"]>; + readonly refDefault: UnwrapRef<(typeof import("@vueuse/core"))["refDefault"]>; + readonly refThrottled: UnwrapRef<(typeof import("@vueuse/core"))["refThrottled"]>; + readonly refWithControl: UnwrapRef<(typeof import("@vueuse/core"))["refWithControl"]>; + readonly resolveComponent: UnwrapRef<(typeof import("vue"))["resolveComponent"]>; + readonly resolveRef: UnwrapRef<(typeof import("@vueuse/core"))["resolveRef"]>; + readonly resolveUnref: UnwrapRef<(typeof import("@vueuse/core"))["resolveUnref"]>; + readonly setActivePinia: UnwrapRef<(typeof import("pinia"))["setActivePinia"]>; + readonly setMapStoreSuffix: UnwrapRef<(typeof import("pinia"))["setMapStoreSuffix"]>; + readonly shallowReactive: UnwrapRef<(typeof import("vue"))["shallowReactive"]>; + readonly shallowReadonly: UnwrapRef<(typeof import("vue"))["shallowReadonly"]>; + readonly shallowRef: UnwrapRef<(typeof import("vue"))["shallowRef"]>; + readonly storeToRefs: UnwrapRef<(typeof import("pinia"))["storeToRefs"]>; + readonly syncRef: UnwrapRef<(typeof import("@vueuse/core"))["syncRef"]>; + readonly syncRefs: UnwrapRef<(typeof import("@vueuse/core"))["syncRefs"]>; + readonly templateRef: UnwrapRef<(typeof import("@vueuse/core"))["templateRef"]>; + readonly throttledRef: UnwrapRef<(typeof import("@vueuse/core"))["throttledRef"]>; + readonly throttledWatch: UnwrapRef<(typeof import("@vueuse/core"))["throttledWatch"]>; + readonly toRaw: UnwrapRef<(typeof import("vue"))["toRaw"]>; + readonly toReactive: UnwrapRef<(typeof import("@vueuse/core"))["toReactive"]>; + readonly toRef: UnwrapRef<(typeof import("vue"))["toRef"]>; + readonly toRefs: UnwrapRef<(typeof import("vue"))["toRefs"]>; + readonly toValue: UnwrapRef<(typeof import("vue"))["toValue"]>; + readonly triggerRef: UnwrapRef<(typeof import("vue"))["triggerRef"]>; + readonly tryOnBeforeMount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeMount"]>; + readonly tryOnBeforeUnmount: UnwrapRef<(typeof import("@vueuse/core"))["tryOnBeforeUnmount"]>; + readonly tryOnMounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnMounted"]>; + readonly tryOnScopeDispose: UnwrapRef<(typeof import("@vueuse/core"))["tryOnScopeDispose"]>; + readonly tryOnUnmounted: UnwrapRef<(typeof import("@vueuse/core"))["tryOnUnmounted"]>; + readonly unref: UnwrapRef<(typeof import("vue"))["unref"]>; + readonly unrefElement: UnwrapRef<(typeof import("@vueuse/core"))["unrefElement"]>; + readonly until: UnwrapRef<(typeof import("@vueuse/core"))["until"]>; + readonly useActiveElement: UnwrapRef<(typeof import("@vueuse/core"))["useActiveElement"]>; + readonly useAnimate: UnwrapRef<(typeof import("@vueuse/core"))["useAnimate"]>; + readonly useArrayDifference: UnwrapRef<(typeof import("@vueuse/core"))["useArrayDifference"]>; + readonly useArrayEvery: UnwrapRef<(typeof import("@vueuse/core"))["useArrayEvery"]>; + readonly useArrayFilter: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFilter"]>; + readonly useArrayFind: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFind"]>; + readonly useArrayFindIndex: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindIndex"]>; + readonly useArrayFindLast: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindLast"]>; + readonly useArrayIncludes: UnwrapRef<(typeof import("@vueuse/core"))["useArrayIncludes"]>; + readonly useArrayJoin: UnwrapRef<(typeof import("@vueuse/core"))["useArrayJoin"]>; + readonly useArrayMap: UnwrapRef<(typeof import("@vueuse/core"))["useArrayMap"]>; + readonly useArrayReduce: UnwrapRef<(typeof import("@vueuse/core"))["useArrayReduce"]>; + readonly useArraySome: UnwrapRef<(typeof import("@vueuse/core"))["useArraySome"]>; + readonly useArrayUnique: UnwrapRef<(typeof import("@vueuse/core"))["useArrayUnique"]>; + readonly useAsyncQueue: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncQueue"]>; + readonly useAsyncState: UnwrapRef<(typeof import("@vueuse/core"))["useAsyncState"]>; + readonly useAttrs: UnwrapRef<(typeof import("vue"))["useAttrs"]>; + readonly useBase64: UnwrapRef<(typeof import("@vueuse/core"))["useBase64"]>; + readonly useBattery: UnwrapRef<(typeof import("@vueuse/core"))["useBattery"]>; + readonly useBluetooth: UnwrapRef<(typeof import("@vueuse/core"))["useBluetooth"]>; + readonly useBreakpoints: UnwrapRef<(typeof import("@vueuse/core"))["useBreakpoints"]>; + readonly useBroadcastChannel: UnwrapRef<(typeof import("@vueuse/core"))["useBroadcastChannel"]>; + readonly useBrowserLocation: UnwrapRef<(typeof import("@vueuse/core"))["useBrowserLocation"]>; + readonly useCached: UnwrapRef<(typeof import("@vueuse/core"))["useCached"]>; + readonly useClipboard: UnwrapRef<(typeof import("@vueuse/core"))["useClipboard"]>; + readonly useClipboardItems: UnwrapRef<(typeof import("@vueuse/core"))["useClipboardItems"]>; + readonly useCloned: UnwrapRef<(typeof import("@vueuse/core"))["useCloned"]>; + readonly useColorMode: UnwrapRef<(typeof import("@vueuse/core"))["useColorMode"]>; + readonly useConfirmDialog: UnwrapRef<(typeof import("@vueuse/core"))["useConfirmDialog"]>; + readonly useCounter: UnwrapRef<(typeof import("@vueuse/core"))["useCounter"]>; + readonly useCssModule: UnwrapRef<(typeof import("vue"))["useCssModule"]>; + readonly useCssVar: UnwrapRef<(typeof import("@vueuse/core"))["useCssVar"]>; + readonly useCssVars: UnwrapRef<(typeof import("vue"))["useCssVars"]>; + readonly useCurrentElement: UnwrapRef<(typeof import("@vueuse/core"))["useCurrentElement"]>; + readonly useCycleList: UnwrapRef<(typeof import("@vueuse/core"))["useCycleList"]>; + readonly useDark: UnwrapRef<(typeof import("@vueuse/core"))["useDark"]>; + readonly useDateFormat: UnwrapRef<(typeof import("@vueuse/core"))["useDateFormat"]>; + readonly useDebounce: UnwrapRef<(typeof import("@vueuse/core"))["useDebounce"]>; + readonly useDebounceFn: UnwrapRef<(typeof import("@vueuse/core"))["useDebounceFn"]>; + readonly useDebouncedRefHistory: UnwrapRef< + (typeof import("@vueuse/core"))["useDebouncedRefHistory"] + >; + readonly useDeviceMotion: UnwrapRef<(typeof import("@vueuse/core"))["useDeviceMotion"]>; + readonly useDeviceOrientation: UnwrapRef< + (typeof import("@vueuse/core"))["useDeviceOrientation"] + >; + readonly useDevicePixelRatio: UnwrapRef<(typeof import("@vueuse/core"))["useDevicePixelRatio"]>; + readonly useDevicesList: UnwrapRef<(typeof import("@vueuse/core"))["useDevicesList"]>; + readonly useDisplayMedia: UnwrapRef<(typeof import("@vueuse/core"))["useDisplayMedia"]>; + readonly useDocumentVisibility: UnwrapRef< + (typeof import("@vueuse/core"))["useDocumentVisibility"] + >; + readonly useDraggable: UnwrapRef<(typeof import("@vueuse/core"))["useDraggable"]>; + readonly useDropZone: UnwrapRef<(typeof import("@vueuse/core"))["useDropZone"]>; + readonly useElementBounding: UnwrapRef<(typeof import("@vueuse/core"))["useElementBounding"]>; + readonly useElementByPoint: UnwrapRef<(typeof import("@vueuse/core"))["useElementByPoint"]>; + readonly useElementHover: UnwrapRef<(typeof import("@vueuse/core"))["useElementHover"]>; + readonly useElementSize: UnwrapRef<(typeof import("@vueuse/core"))["useElementSize"]>; + readonly useElementVisibility: UnwrapRef< + (typeof import("@vueuse/core"))["useElementVisibility"] + >; + readonly useEventBus: UnwrapRef<(typeof import("@vueuse/core"))["useEventBus"]>; + readonly useEventListener: UnwrapRef<(typeof import("@vueuse/core"))["useEventListener"]>; + readonly useEventSource: UnwrapRef<(typeof import("@vueuse/core"))["useEventSource"]>; + readonly useEyeDropper: UnwrapRef<(typeof import("@vueuse/core"))["useEyeDropper"]>; + readonly useFavicon: UnwrapRef<(typeof import("@vueuse/core"))["useFavicon"]>; + readonly useFetch: UnwrapRef<(typeof import("@vueuse/core"))["useFetch"]>; + readonly useFileDialog: UnwrapRef<(typeof import("@vueuse/core"))["useFileDialog"]>; + readonly useFileSystemAccess: UnwrapRef<(typeof import("@vueuse/core"))["useFileSystemAccess"]>; + readonly useFocus: UnwrapRef<(typeof import("@vueuse/core"))["useFocus"]>; + readonly useFocusWithin: UnwrapRef<(typeof import("@vueuse/core"))["useFocusWithin"]>; + readonly useFps: UnwrapRef<(typeof import("@vueuse/core"))["useFps"]>; + readonly useFullscreen: UnwrapRef<(typeof import("@vueuse/core"))["useFullscreen"]>; + readonly useGamepad: UnwrapRef<(typeof import("@vueuse/core"))["useGamepad"]>; + readonly useGeolocation: UnwrapRef<(typeof import("@vueuse/core"))["useGeolocation"]>; + readonly useI18n: UnwrapRef<(typeof import("vue-i18n"))["useI18n"]>; + readonly useIdle: UnwrapRef<(typeof import("@vueuse/core"))["useIdle"]>; + readonly useImage: UnwrapRef<(typeof import("@vueuse/core"))["useImage"]>; + readonly useInfiniteScroll: UnwrapRef<(typeof import("@vueuse/core"))["useInfiniteScroll"]>; + readonly useIntersectionObserver: UnwrapRef< + (typeof import("@vueuse/core"))["useIntersectionObserver"] + >; + readonly useInterval: UnwrapRef<(typeof import("@vueuse/core"))["useInterval"]>; + readonly useIntervalFn: UnwrapRef<(typeof import("@vueuse/core"))["useIntervalFn"]>; + readonly useKeyModifier: UnwrapRef<(typeof import("@vueuse/core"))["useKeyModifier"]>; + readonly useLastChanged: UnwrapRef<(typeof import("@vueuse/core"))["useLastChanged"]>; + readonly useLink: UnwrapRef<(typeof import("vue-router"))["useLink"]>; + readonly useLocalStorage: UnwrapRef<(typeof import("@vueuse/core"))["useLocalStorage"]>; + readonly useMagicKeys: UnwrapRef<(typeof import("@vueuse/core"))["useMagicKeys"]>; + readonly useManualRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useManualRefHistory"]>; + readonly useMediaControls: UnwrapRef<(typeof import("@vueuse/core"))["useMediaControls"]>; + readonly useMediaQuery: UnwrapRef<(typeof import("@vueuse/core"))["useMediaQuery"]>; + readonly useMemoize: UnwrapRef<(typeof import("@vueuse/core"))["useMemoize"]>; + readonly useMemory: UnwrapRef<(typeof import("@vueuse/core"))["useMemory"]>; + readonly useMounted: UnwrapRef<(typeof import("@vueuse/core"))["useMounted"]>; + readonly useMouse: UnwrapRef<(typeof import("@vueuse/core"))["useMouse"]>; + readonly useMouseInElement: UnwrapRef<(typeof import("@vueuse/core"))["useMouseInElement"]>; + readonly useMousePressed: UnwrapRef<(typeof import("@vueuse/core"))["useMousePressed"]>; + readonly useMutationObserver: UnwrapRef<(typeof import("@vueuse/core"))["useMutationObserver"]>; + readonly useNavigatorLanguage: UnwrapRef< + (typeof import("@vueuse/core"))["useNavigatorLanguage"] + >; + readonly useNetwork: UnwrapRef<(typeof import("@vueuse/core"))["useNetwork"]>; + readonly useNow: UnwrapRef<(typeof import("@vueuse/core"))["useNow"]>; + readonly useObjectUrl: UnwrapRef<(typeof import("@vueuse/core"))["useObjectUrl"]>; + readonly useOffsetPagination: UnwrapRef<(typeof import("@vueuse/core"))["useOffsetPagination"]>; + readonly useOnline: UnwrapRef<(typeof import("@vueuse/core"))["useOnline"]>; + readonly usePageLeave: UnwrapRef<(typeof import("@vueuse/core"))["usePageLeave"]>; + readonly useParallax: UnwrapRef<(typeof import("@vueuse/core"))["useParallax"]>; + readonly useParentElement: UnwrapRef<(typeof import("@vueuse/core"))["useParentElement"]>; + readonly usePerformanceObserver: UnwrapRef< + (typeof import("@vueuse/core"))["usePerformanceObserver"] + >; + readonly usePermission: UnwrapRef<(typeof import("@vueuse/core"))["usePermission"]>; + readonly usePointer: UnwrapRef<(typeof import("@vueuse/core"))["usePointer"]>; + readonly usePointerLock: UnwrapRef<(typeof import("@vueuse/core"))["usePointerLock"]>; + readonly usePointerSwipe: UnwrapRef<(typeof import("@vueuse/core"))["usePointerSwipe"]>; + readonly usePreferredColorScheme: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredColorScheme"] + >; + readonly usePreferredContrast: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredContrast"] + >; + readonly usePreferredDark: UnwrapRef<(typeof import("@vueuse/core"))["usePreferredDark"]>; + readonly usePreferredLanguages: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredLanguages"] + >; + readonly usePreferredReducedMotion: UnwrapRef< + (typeof import("@vueuse/core"))["usePreferredReducedMotion"] + >; + readonly usePrevious: UnwrapRef<(typeof import("@vueuse/core"))["usePrevious"]>; + readonly useRafFn: UnwrapRef<(typeof import("@vueuse/core"))["useRafFn"]>; + readonly useRefHistory: UnwrapRef<(typeof import("@vueuse/core"))["useRefHistory"]>; + readonly useResizeObserver: UnwrapRef<(typeof import("@vueuse/core"))["useResizeObserver"]>; + readonly useRoute: UnwrapRef<(typeof import("vue-router"))["useRoute"]>; + readonly useRouter: UnwrapRef<(typeof import("vue-router"))["useRouter"]>; + readonly useScreenOrientation: UnwrapRef< + (typeof import("@vueuse/core"))["useScreenOrientation"] + >; + readonly useScreenSafeArea: UnwrapRef<(typeof import("@vueuse/core"))["useScreenSafeArea"]>; + readonly useScriptTag: UnwrapRef<(typeof import("@vueuse/core"))["useScriptTag"]>; + readonly useScroll: UnwrapRef<(typeof import("@vueuse/core"))["useScroll"]>; + readonly useScrollLock: UnwrapRef<(typeof import("@vueuse/core"))["useScrollLock"]>; + readonly useSessionStorage: UnwrapRef<(typeof import("@vueuse/core"))["useSessionStorage"]>; + readonly useShare: UnwrapRef<(typeof import("@vueuse/core"))["useShare"]>; + readonly useSlots: UnwrapRef<(typeof import("vue"))["useSlots"]>; + readonly useSorted: UnwrapRef<(typeof import("@vueuse/core"))["useSorted"]>; + readonly useSpeechRecognition: UnwrapRef< + (typeof import("@vueuse/core"))["useSpeechRecognition"] + >; + readonly useSpeechSynthesis: UnwrapRef<(typeof import("@vueuse/core"))["useSpeechSynthesis"]>; + readonly useStepper: UnwrapRef<(typeof import("@vueuse/core"))["useStepper"]>; + readonly useStorage: UnwrapRef<(typeof import("@vueuse/core"))["useStorage"]>; + readonly useStorageAsync: UnwrapRef<(typeof import("@vueuse/core"))["useStorageAsync"]>; + readonly useStyleTag: UnwrapRef<(typeof import("@vueuse/core"))["useStyleTag"]>; + readonly useSupported: UnwrapRef<(typeof import("@vueuse/core"))["useSupported"]>; + readonly useSwipe: UnwrapRef<(typeof import("@vueuse/core"))["useSwipe"]>; + readonly useTemplateRefsList: UnwrapRef<(typeof import("@vueuse/core"))["useTemplateRefsList"]>; + readonly useTextDirection: UnwrapRef<(typeof import("@vueuse/core"))["useTextDirection"]>; + readonly useTextSelection: UnwrapRef<(typeof import("@vueuse/core"))["useTextSelection"]>; + readonly useTextareaAutosize: UnwrapRef<(typeof import("@vueuse/core"))["useTextareaAutosize"]>; + readonly useThrottle: UnwrapRef<(typeof import("@vueuse/core"))["useThrottle"]>; + readonly useThrottleFn: UnwrapRef<(typeof import("@vueuse/core"))["useThrottleFn"]>; + readonly useThrottledRefHistory: UnwrapRef< + (typeof import("@vueuse/core"))["useThrottledRefHistory"] + >; + readonly useTimeAgo: UnwrapRef<(typeof import("@vueuse/core"))["useTimeAgo"]>; + readonly useTimeout: UnwrapRef<(typeof import("@vueuse/core"))["useTimeout"]>; + readonly useTimeoutFn: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutFn"]>; + readonly useTimeoutPoll: UnwrapRef<(typeof import("@vueuse/core"))["useTimeoutPoll"]>; + readonly useTimestamp: UnwrapRef<(typeof import("@vueuse/core"))["useTimestamp"]>; + readonly useTitle: UnwrapRef<(typeof import("@vueuse/core"))["useTitle"]>; + readonly useToNumber: UnwrapRef<(typeof import("@vueuse/core"))["useToNumber"]>; + readonly useToString: UnwrapRef<(typeof import("@vueuse/core"))["useToString"]>; + readonly useToggle: UnwrapRef<(typeof import("@vueuse/core"))["useToggle"]>; + readonly useTransition: UnwrapRef<(typeof import("@vueuse/core"))["useTransition"]>; + readonly useUrlSearchParams: UnwrapRef<(typeof import("@vueuse/core"))["useUrlSearchParams"]>; + readonly useUserMedia: UnwrapRef<(typeof import("@vueuse/core"))["useUserMedia"]>; + readonly useVModel: UnwrapRef<(typeof import("@vueuse/core"))["useVModel"]>; + readonly useVModels: UnwrapRef<(typeof import("@vueuse/core"))["useVModels"]>; + readonly useVibrate: UnwrapRef<(typeof import("@vueuse/core"))["useVibrate"]>; + readonly useVirtualList: UnwrapRef<(typeof import("@vueuse/core"))["useVirtualList"]>; + readonly useWakeLock: UnwrapRef<(typeof import("@vueuse/core"))["useWakeLock"]>; + readonly useWebNotification: UnwrapRef<(typeof import("@vueuse/core"))["useWebNotification"]>; + readonly useWebSocket: UnwrapRef<(typeof import("@vueuse/core"))["useWebSocket"]>; + readonly useWebWorker: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorker"]>; + readonly useWebWorkerFn: UnwrapRef<(typeof import("@vueuse/core"))["useWebWorkerFn"]>; + readonly useWindowFocus: UnwrapRef<(typeof import("@vueuse/core"))["useWindowFocus"]>; + readonly useWindowScroll: UnwrapRef<(typeof import("@vueuse/core"))["useWindowScroll"]>; + readonly useWindowSize: UnwrapRef<(typeof import("@vueuse/core"))["useWindowSize"]>; + readonly watch: UnwrapRef<(typeof import("vue"))["watch"]>; + readonly watchArray: UnwrapRef<(typeof import("@vueuse/core"))["watchArray"]>; + readonly watchAtMost: UnwrapRef<(typeof import("@vueuse/core"))["watchAtMost"]>; + readonly watchDebounced: UnwrapRef<(typeof import("@vueuse/core"))["watchDebounced"]>; + readonly watchDeep: UnwrapRef<(typeof import("@vueuse/core"))["watchDeep"]>; + readonly watchEffect: UnwrapRef<(typeof import("vue"))["watchEffect"]>; + readonly watchIgnorable: UnwrapRef<(typeof import("@vueuse/core"))["watchIgnorable"]>; + readonly watchImmediate: UnwrapRef<(typeof import("@vueuse/core"))["watchImmediate"]>; + readonly watchOnce: UnwrapRef<(typeof import("@vueuse/core"))["watchOnce"]>; + readonly watchPausable: UnwrapRef<(typeof import("@vueuse/core"))["watchPausable"]>; + readonly watchPostEffect: UnwrapRef<(typeof import("vue"))["watchPostEffect"]>; + readonly watchSyncEffect: UnwrapRef<(typeof import("vue"))["watchSyncEffect"]>; + readonly watchThrottled: UnwrapRef<(typeof import("@vueuse/core"))["watchThrottled"]>; + readonly watchTriggerable: UnwrapRef<(typeof import("@vueuse/core"))["watchTriggerable"]>; + readonly watchWithFilter: UnwrapRef<(typeof import("@vueuse/core"))["watchWithFilter"]>; + readonly whenever: UnwrapRef<(typeof import("@vueuse/core"))["whenever"]>; + } +} diff --git a/src/types/components.d.ts b/src/types/components.d.ts new file mode 100644 index 0000000..6e194f0 --- /dev/null +++ b/src/types/components.d.ts @@ -0,0 +1,90 @@ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module "vue" { + export interface GlobalComponents { + AppLink: (typeof import("./../components/AppLink/index.vue"))["default"]; + Breadcrumb: (typeof import("./../components/Breadcrumb/index.vue"))["default"]; + CopyButton: (typeof import("./../components/CopyButton/index.vue"))["default"]; + CURD: (typeof import("./../components/CURD/index.vue"))["default"]; + Dict: (typeof import("./../components/Dict/index.vue"))["default"]; + DictLabel: (typeof import("./../components/Dict/DictLabel.vue"))["default"]; + ECharts: (typeof import("./../components/ECharts/index.vue"))["default"]; + ElBacktop: (typeof import("element-plus/es"))["ElBacktop"]; + ElBreadcrumb: (typeof import("element-plus/es"))["ElBreadcrumb"]; + ElBreadcrumbItem: (typeof import("element-plus/es"))["ElBreadcrumbItem"]; + ElButton: (typeof import("element-plus/es"))["ElButton"]; + ElCard: (typeof import("element-plus/es"))["ElCard"]; + ElCascader: (typeof import("element-plus/es"))["ElCascader"]; + ElCheckbox: (typeof import("element-plus/es"))["ElCheckbox"]; + ElCheckboxGroup: (typeof import("element-plus/es"))["ElCheckboxGroup"]; + ElCol: (typeof import("element-plus/es"))["ElCol"]; + ElColorPicker: (typeof import("element-plus/es"))["ElColorPicker"]; + ElConfigProvider: (typeof import("element-plus/es"))["ElConfigProvider"]; + ElDatePicker: (typeof import("element-plus/es"))["ElDatePicker"]; + ElDialog: (typeof import("element-plus/es"))["ElDialog"]; + ElDivider: (typeof import("element-plus/es"))["ElDivider"]; + ElDrawer: (typeof import("element-plus/es"))["ElDrawer"]; + ElDropdown: (typeof import("element-plus/es"))["ElDropdown"]; + ElDropdownItem: (typeof import("element-plus/es"))["ElDropdownItem"]; + ElDropdownMenu: (typeof import("element-plus/es"))["ElDropdownMenu"]; + ElForm: (typeof import("element-plus/es"))["ElForm"]; + ElFormItem: (typeof import("element-plus/es"))["ElFormItem"]; + ElIcon: (typeof import("element-plus/es"))["ElIcon"]; + ElImage: (typeof import("element-plus/es"))["ElImage"]; + ElInput: (typeof import("element-plus/es"))["ElInput"]; + ElInputTag: (typeof import("element-plus/es"))["ElInputTag"]; + ElInputNumber: (typeof import("element-plus/es"))["ElInputNumber"]; + ElLink: (typeof import("element-plus/es"))["ElLink"]; + ElMenu: (typeof import("element-plus/es"))["ElMenu"]; + ElMenuItem: (typeof import("element-plus/es"))["ElMenuItem"]; + ElOption: (typeof import("element-plus/es"))["ElOption"]; + ElPagination: (typeof import("element-plus/es"))["ElPagination"]; + ElPopover: (typeof import("element-plus/es"))["ElPopover"]; + ElRadio: (typeof import("element-plus/es"))["ElRadio"]; + ElRadioGroup: (typeof import("element-plus/es"))["ElRadioGroup"]; + ElRow: (typeof import("element-plus/es"))["ElRow"]; + ElScrollbar: (typeof import("element-plus/es"))["ElScrollbar"]; + ElSelect: (typeof import("element-plus/es"))["ElSelect"]; + ElStatistic: (typeof import("element-plus/es"))["ElStatistic"]; + ElSubMenu: (typeof import("element-plus/es"))["ElSubMenu"]; + ElSwitch: (typeof import("element-plus/es"))["ElSwitch"]; + ElTable: (typeof import("element-plus/es"))["ElTable"]; + ElTableColumn: (typeof import("element-plus/es"))["ElTableColumn"]; + ElTag: (typeof import("element-plus/es"))["ElTag"]; + ElText: (typeof import("element-plus/es"))["ElText"]; + ElTimeSelect: (typeof import("element-plus/es"))["ElTimeSelect"]; + ElTooltip: (typeof import("element-plus/es"))["ElTooltip"]; + ElTree: (typeof import("element-plus/es"))["ElTree"]; + ElTreeSelect: (typeof import("element-plus/es"))["ElTreeSelect"]; + ElUpload: (typeof import("element-plus/es"))["ElUpload"]; + ElWatermark: (typeof import("element-plus/es"))["ElWatermark"]; + ElSkeleton: (typeof import("element-plus/es"))["ElSkeleton"]; + FileUpload: (typeof import("./../components/Upload/FileUpload.vue"))["default"]; + Form: (typeof import("./../components/CURD/Form.vue"))["default"]; + Fullscreen: (typeof import("./../components/Fullscreen/index.vue"))["default"]; + GithubCorner: (typeof import("./../components/GithubCorner/index.vue"))["default"]; + Hamburger: (typeof import("./../components/Hamburger/index.vue"))["default"]; + IconSelect: (typeof import("./../components/IconSelect/index.vue"))["default"]; + LangSelect: (typeof import("./../components/LangSelect/index.vue"))["default"]; + MenuSearch: (typeof import("./../components/MenuSearch/index.vue"))["default"]; + MultiImageUpload: (typeof import("./../components/Upload/MultiImageUpload.vue"))["default"]; + Notification: (typeof import("./../components/Notification/index.vue"))["default"]; + PageContent: (typeof import("./../components/CURD/PageContent.vue"))["default"]; + PageModal: (typeof import("./../components/CURD/PageModal.vue"))["default"]; + PageSearch: (typeof import("./../components/CURD/PageSearch.vue"))["default"]; + Pagination: (typeof import("./../components/Pagination/index.vue"))["default"]; + RouterLink: (typeof import("vue-router"))["RouterLink"]; + RouterView: (typeof import("vue-router"))["RouterView"]; + SingleImageUpload: (typeof import("./../components/Upload/SingleImageUpload.vue"))["default"]; + SizeSelect: (typeof import("./../components/SizeSelect/index.vue"))["default"]; + TableSelect: (typeof import("./../components/TableSelect/index.vue"))["default"]; + WangEditor: (typeof import("./../components/WangEditor/index.vue"))["default"]; + } + export interface ComponentCustomProperties { + vLoading: (typeof import("element-plus/es"))["ElLoadingDirective"]; + } +} diff --git a/src/types/env.d.ts b/src/types/env.d.ts new file mode 100644 index 0000000..868a94d --- /dev/null +++ b/src/types/env.d.ts @@ -0,0 +1,35 @@ +// https://cn.vitejs.dev/guide/env-and-mode + +// TypeScript 类型提示都为 string: https://github.com/vitejs/vite/issues/6930 +interface ImportMetaEnv { + /** 应用端口 */ + VITE_APP_PORT: number + /** 应用名称 */ + VITE_APP_NAME: string + /** API 基础路径(代理前缀) */ + VITE_APP_BASE_API: string + /** API 地址 */ + VITE_APP_API_URL: string + /** 是否开启 Mock 服务 */ + VITE_MOCK_DEV_SERVER: boolean +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +/** + * 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示 + */ +declare const __APP_INFO__: { + pkg: { + name: string + version: string + engines: { + node: string + } + dependencies: Record + devDependencies: Record + } + buildTimestamp: number +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..5a0d0f6 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,111 @@ +declare global { + /** + * 响应数据 + */ + interface ApiResponse { + code: string + data: T + msg: string + } + + /** + * 分页查询参数 + */ + interface PageQuery { + pageNum: number + pageSize: number + } + + /** + * 分页响应对象 + */ + interface PageResult { + /** 数据列表 */ + list: T + /** 总数 */ + total: number + } + + /** + * 页签对象 + */ + interface TagView { + /** 页签名称 */ + name: string + /** 页签标题 */ + title: string + /** 页签路由路径 */ + path: string + /** 页签路由完整路径 */ + fullPath: string + /** 页签图标 */ + icon?: string + /** 是否固定页签 */ + affix?: boolean + /** 是否开启缓存 */ + keepAlive?: boolean + /** 路由查询参数 */ + query?: any + } + + /** + * 系统设置 + */ + interface AppSettings { + /** 系统标题 */ + title: string + /** 系统版本 */ + version: string + /** 是否显示设置 */ + showSettings: boolean + /** 是否显示多标签导航 */ + showTagsView: boolean + /** 是否显示应用Logo */ + showAppLogo: boolean + /** 导航栏布局(left|top|mix) */ + layout: 'left' | 'top' | 'mix' + /** 主题颜色 */ + themeColor: string + /** 主题模式(dark|light) */ + theme: import('@/enums/settings/theme-enum').ThemeMode + /** 布局大小(default |large |small) */ + size: string + /** 语言( zh-cn| en) */ + language: string + /** 是否显示水印 */ + showWatermark: boolean + /** 水印内容 */ + watermarkContent: string + /** 侧边栏配色方案 */ + sidebarColorScheme: 'classic-blue' | 'minimal-white' + /** 是否启用 AI 助手 */ + enableAiAssistant: boolean + } + + /** + * 下拉选项数据类型 + */ + interface OptionType { + /** 值 */ + value: string | number + /** 文本 */ + label: string + /** 子列表 */ + children?: OptionType[] + } + + /** + * 导入结果 + */ + interface ExcelResult { + /** 状态码 */ + code: string + /** 无效数据条数 */ + invalidCount: number + /** 有效数据条数 */ + validCount: number + /** 错误信息 */ + messageList: Array + } +} +export {} diff --git a/src/types/router.d.ts b/src/types/router.d.ts new file mode 100644 index 0000000..8f992ad --- /dev/null +++ b/src/types/router.d.ts @@ -0,0 +1,54 @@ +import 'vue-router' + +declare module 'vue-router' { + // https://router.vuejs.org/zh/guide/advanced/meta.html#typescript + // 可以通过扩展 RouteMeta 接口来输入 meta 字段 + interface RouteMeta { + /** + * 菜单名称 + * @example 'Dashboard' + */ + title?: string + + /** + * 菜单图标 + * @example 'el-icon-edit' + */ + icon?: string + + /** + * 是否隐藏菜单 + * true 隐藏, false 显示 + * @default false + */ + hidden?: boolean + + /** + * 始终显示父级菜单,即使只有一个子菜单 + * true 显示父级菜单, false 隐藏父级菜单,显示唯一子节点 + * @default false + */ + alwaysShow?: boolean + + /** + * 是否固定在页签上 + * true 固定, false 不固定 + * @default false + */ + affix?: boolean + + /** + * 是否缓存页面 + * true 缓存, false 不缓存 + * @default false + */ + keepAlive?: boolean + + /** + * 是否在面包屑导航中隐藏 + * true 隐藏, false 显示 + * @default false + */ + breadcrumb?: boolean + } +} diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts new file mode 100644 index 0000000..2b97bd9 --- /dev/null +++ b/src/types/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/src/types/socket.d.ts b/src/types/socket.d.ts new file mode 100644 index 0000000..34c3e65 --- /dev/null +++ b/src/types/socket.d.ts @@ -0,0 +1,6 @@ +// https://github.com/sockjs/sockjs-client/issues/565 + +declare module 'sockjs-client/dist/sockjs.min.js' { + import Client from 'sockjs-client' + export default Client +} diff --git a/src/utils/TimerManager/index.ts b/src/utils/TimerManager/index.ts new file mode 100644 index 0000000..88f250b --- /dev/null +++ b/src/utils/TimerManager/index.ts @@ -0,0 +1,30 @@ +export class TimerManager { + private timerId: number | null = null + + start(callback: () => void) { + // 清理之前的计时器 + this.stop() + + // 立即执行一次 + callback() + + // 启动新的计时器 + this.timerId = setInterval(callback, 5000) as unknown as number + } + + stop() { + if (this.timerId !== null) { + clearInterval(this.timerId) + this.timerId = null + } + } +} + +export function createTimer() { + const timerManager = new TimerManager() + + return { + startTimer: (callback: () => void) => timerManager.start(callback), + stopTimer: () => timerManager.stop() + } +} diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..e258c70 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,76 @@ +import { Storage } from './storage' +import { AUTH_KEYS, ROLE_ROOT } from '@/constants' +import { useUserStoreHook } from '@/store/modules/user-store' +import router from '@/router' + +// 负责本地凭证与偏好的读写 +export const AuthStorage = { + getAccessToken(): string { + const isRememberMe = Storage.get(AUTH_KEYS.REMEMBER_ME, false) + return isRememberMe + ? Storage.get(AUTH_KEYS.ACCESS_TOKEN, '') + : Storage.sessionGet(AUTH_KEYS.ACCESS_TOKEN, '') + }, + + getRefreshToken(): string { + const isRememberMe = Storage.get(AUTH_KEYS.REMEMBER_ME, false) + return isRememberMe + ? Storage.get(AUTH_KEYS.REFRESH_TOKEN, '') + : Storage.sessionGet(AUTH_KEYS.REFRESH_TOKEN, '') + }, + + setTokens(accessToken: string, refreshToken: string, rememberMe: boolean): void { + Storage.set(AUTH_KEYS.REMEMBER_ME, rememberMe) + if (rememberMe) { + Storage.set(AUTH_KEYS.ACCESS_TOKEN, accessToken) + Storage.set(AUTH_KEYS.REFRESH_TOKEN, refreshToken) + } else { + Storage.sessionSet(AUTH_KEYS.ACCESS_TOKEN, accessToken) + Storage.sessionSet(AUTH_KEYS.REFRESH_TOKEN, refreshToken) + Storage.remove(AUTH_KEYS.ACCESS_TOKEN) + Storage.remove(AUTH_KEYS.REFRESH_TOKEN) + } + }, + + clearAuth(): void { + Storage.remove(AUTH_KEYS.ACCESS_TOKEN) + Storage.remove(AUTH_KEYS.REFRESH_TOKEN) + Storage.sessionRemove(AUTH_KEYS.ACCESS_TOKEN) + Storage.sessionRemove(AUTH_KEYS.REFRESH_TOKEN) + }, + + getRememberMe(): boolean { + return Storage.get(AUTH_KEYS.REMEMBER_ME, false) + } +} + +/** + * 权限判断 - 已移除 + */ +export function hasPerm(value?: string | string[], type?: 'button' | 'role'): boolean { + return true +} + +/** + * 重定向到登录页面 + */ +export async function redirectToLogin(message: string = '请重新登录'): Promise { + ElNotification({ + title: '提示', + message, + type: 'warning', + duration: 3000 + }) + + await useUserStoreHook().resetAllState() + + try { + // 跳转到登录页,保留当前路由用于登录后跳转 + const currentPath = router.currentRoute.value.fullPath + await router.push(`/login?redirect=${encodeURIComponent(currentPath)}`) + } catch (error) { + console.error('Redirect to login error:', error) + // 强制跳转,即使路由重定向失败 + window.location.href = '/login' + } +} diff --git a/src/utils/auxiliaryFunction/index.ts b/src/utils/auxiliaryFunction/index.ts new file mode 100644 index 0000000..0f2f11b --- /dev/null +++ b/src/utils/auxiliaryFunction/index.ts @@ -0,0 +1,257 @@ +/** + * 节流函数 - 限制函数执行频率 + * @param func 需要节流的函数 + * @param delay 节流延迟时间(毫秒) + * @returns 节流后的函数 + */ +export function throttle any>( + func: T, + delay: number = 300 +): (...args: Parameters) => void { + let lastExecTime = 0 + + return function (this: ThisParameterType, ...args: Parameters) { + const now = Date.now() + + // 只有超过延迟时间才执行 + if (now - lastExecTime >= delay) { + func.apply(this, args) + lastExecTime = now + } + } +} + +/** + * 防抖函数 + * @param func 需要防抖的函数 + * @param delay 延迟时间(毫秒) + * @returns 防抖后的函数 + */ +export function debounce any>( + func: T, + delay: number = 300 +): (...args: Parameters) => void { + let timeoutId: NodeJS.Timeout | null = null + + return function (this: ThisParameterType, ...args: Parameters) { + if (timeoutId) { + clearTimeout(timeoutId) + } + + timeoutId = setTimeout(() => { + func.apply(this, args) + timeoutId = null + }, delay) + } +} + +/** + * 将文件路径数组转换为对象数组 + * @param filePaths 文件路径数组或单个文件路径字符串 + * @returns 包含文件信息的对象数组 + */ +export function convertFilePathsToObject(filePaths: string | string[]): Array<{ + url: string + name: string + domain: string + uuid: string + extension: string // 新增后缀名字段 +}> { + // 如果是字符串,先转换为数组 + const paths = Array.isArray(filePaths) ? filePaths : [filePaths] + + return paths.map((path) => { + // 确保path是字符串类型 + if (typeof path !== 'string') { + return { + url: '', + domain: '', + name: '', + uuid: '', + extension: '' // 新增后缀名字段 + } + } + + // 自动补全协议头 + let fullPath = path + if (!path.startsWith('http://') && !path.startsWith('https://')) { + fullPath = `http://${path}` + } + + // 分割域名和文件部分 + const urlObj = new URL(fullPath) + const domain = urlObj.origin + const filePart = urlObj.pathname.substring(1) // 移除开头的 / + + // 优化文件名解析逻辑 + // 对于路径 "cd78824383f7新建 文本文档.txt" + // 我们不再强制要求下划线分隔格式,而是提取完整的文件名 + let name = '' + let uuid = '' + let extension = '' // 新增后缀名变量 + + // 尝试从文件名中提取 UUID(假设 UUID 是文件名开头的一段字母数字组合) + const uuidMatch = filePart.match(/^([a-f0-9]+)/i) + if (uuidMatch && uuidMatch[1]) { + uuid = uuidMatch[1] + // 文件名是去掉 UUID 后的部分 + name = filePart.substring(uuid.length) + // 如果文件名以斜杠开头则去除 + if (name.startsWith('/')) { + name = name.substring(1) + } + } else { + // 如果没有匹配到 UUID 格式,整个作为文件名处理 + name = filePart + } + + // 如果 name 为空,则使用完整文件部分作为文件名 + if (!name) { + name = filePart + } + + // 提取文件扩展名 + const lastDotIndex = name.lastIndexOf('.') + if (lastDotIndex > 0) { + // 确保点号不在开头 + extension = name.substring(lastDotIndex + 1).toLowerCase() + name = name.substring(0, lastDotIndex) // 移除扩展名部分,只保留文件名 + } + + // 解码 URL 编码的文件名 + try { + name = decodeURIComponent(name) + } catch (e) { + // 如果解码失败,保持原始文件名 + console.warn('文件名解码失败:', name) + } + + return { + url: fullPath, + domain, + name, + uuid, + extension // 新增后缀名字段 + } + }) +} + +// 安全的文件信息提取函数 +// 更新返回类型定义 +export function getFileInfo(fileStr: string): { + url: string + name: string + extension: string // 新增扩展名字段 +} | null { + try { + if (!fileStr) return null + + // 如果是有效的JSON字符串,按原有逻辑处理 + if (isValidJson(fileStr)) { + const parsed = JSON.parse(fileStr) + const files = convertFilePathsToObject(parsed) + return files && files.length > 0 + ? { + url: files[0].url, + name: files[0].name, + extension: files[0].extension // 新增扩展名 + } + : null + } + // 如果是URL字符串,直接使用 convertFilePathsToObject 处理 + else if (typeof fileStr === 'string') { + const files = convertFilePathsToObject(fileStr) + return files && files.length > 0 + ? { + url: files[0].url, + name: files[0].name, + extension: files[0].extension // 新增扩展名 + } + : null + } + + return null + } catch (e) { + console.error('解析文件信息失败:', e) + return null + } +} + +export function deepCloneByJSON(obj: any) { + try { + return JSON.parse(JSON.stringify(obj)) + } catch (error) { + console.error('深拷贝失败:', error) + return obj + } +} + +export function isString(value: any): boolean { + return typeof value === 'string' +} +export function isFile(obj: any): obj is File { + return obj instanceof File +} +// JSON验证 +export function isValidJson(str: string): boolean { + try { + JSON.parse(str) + return true + } catch (e) { + return false + } +} + +export function isValidFormat(data: any): boolean { + let parsedData = data + + // 如果输入是字符串,尝试解析为 JSON + if (typeof data === 'string') { + try { + parsedData = JSON.parse(data) + } catch (e) { + return false // JSON 解析失败,直接返回 false + } + } + + // 检查是否为数组 + if (!Array.isArray(parsedData)) { + return false + } + + // 检查数组中的每个元素 + return parsedData.every((item) => { + // 检查是否为对象且不为 null + if (typeof item !== 'object' || item === null) { + return false + } + + // 检查必需的属性是否存在,且类型是否正确 + const hasIndex = typeof item.index === 'number' + const hasName = typeof item.name === 'string' + const hasIdNumber = typeof item.idNumber === 'string' + + return hasIndex && hasName && hasIdNumber + }) +} + +/** + * 比较当前时间与指定时间(精确到秒) + * @param timeParam - 要比较的时间参数,支持日期字符串、时间戳或Date对象 + * @returns 当前时间小于timeParam时返回true,否则返回false + */ +export function isCurrentTimeLessThan(timeParam: string | number | Date): boolean { + // 将传入的时间参数转换为Date对象 + const paramTime = new Date(timeParam) + + // 获取当前时间 + const currentTime = new Date() + + // 检查时间参数是否有效 + if (isNaN(paramTime.getTime())) { + throw new Error('Invalid time parameter provided') + } + + // 比较时间(精确到秒):当前时间 < 参数时间 返回true,否则返回false + return Math.floor(currentTime.getTime() / 1000) < Math.floor(paramTime.getTime() / 1000) +} diff --git a/src/utils/functionDialogBox/index.ts b/src/utils/functionDialogBox/index.ts new file mode 100644 index 0000000..ab4f3ac --- /dev/null +++ b/src/utils/functionDialogBox/index.ts @@ -0,0 +1,102 @@ +import { ElButton, ElDialog } from 'element-plus' +import { Component, DefineComponent } from 'vue' +import { h, createApp } from 'vue' +import { throttle } from '@/utils/auxiliaryFunction' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/es/locale/lang/zh-cn' + +// 定义模态框属性接口 +interface ModalProps { + title?: string + width?: string | number + ok?: (result: any) => void + cancel?: () => void + footerVisible?: boolean + [key: string]: any // 允许其他 Element Plus Dialog 属性 +} + +interface DialogInstance { + unmount: () => void +} + +// 函数式弹窗组件 +export const functionDialogBox = ( + component: Component | DefineComponent | null, + props: Record, + modalProps: ModalProps +): DialogInstance => { + const open = ref(true) + const formRef = ref() + const isLoading = ref(false) + const dialog = () => + h( + ElDialog, + { + closeOnClickModal: false, + ...modalProps, + modelValue: open.value, + onOpen() {}, + onClosed() { + app.unmount() + document.body.removeChild(div) + } + }, + { + default: () => h(component || h('div'), { ref: formRef, ...props }), + footer: () => { + if (modalProps.footerVisible !== false) { + return [ + h( + ElButton, + { + type: 'primary', + loading: isLoading.value, + onClick: confirm + }, + () => '确定' + ), + h( + ElButton, + { + onClick: cancel + }, + () => '取消' + ) + ] + } + return null + } + } + ) + + const app = createApp(dialog) + + const div = document.createElement('div') + document.body.appendChild(div) + app.use(ElementPlus, { + locale: zhCn + }) + app.mount(div) + + function unmount() { + open.value = false + } + const confirm = throttle(async () => { + isLoading.value = true + try { + await formRef.value?.submit?.() + const formData = formRef.value?.getForm?.() + modalProps?.ok?.(formData) + unmount() + } finally { + isLoading.value = false + } + }) + const cancel = throttle(() => { + modalProps?.cancel?.() + unmount() + }) + return { + unmount + } +} diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..23dea1e --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,12 @@ +// translate router.meta.title, be used in breadcrumb sidebar tagsview +import i18n from '@/lang/index' + +export function translateRouteTitle(title: any) { + // 判断是否存在国际化配置,如果没有原生返回 + const hasKey = i18n.global.te('route.' + title) + if (hasKey) { + const translatedTitle = i18n.global.t('route.' + title) + return translatedTitle + } + return title +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..21f9da8 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,58 @@ +/** + * Check if an element has a class + * @param {HTMLElement} ele + * @param {string} cls + * @returns {boolean} + */ +export function hasClass(ele: HTMLElement, cls: string) { + return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) +} + +/** + * Add class to element + * @param {HTMLElement} ele + * @param {string} cls + */ +export function addClass(ele: HTMLElement, cls: string) { + if (!hasClass(ele, cls)) ele.className += ' ' + cls +} + +/** + * Remove class from element + * @param {HTMLElement} ele + * @param {string} cls + */ +export function removeClass(ele: HTMLElement, cls: string) { + if (hasClass(ele, cls)) { + const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)') + ele.className = ele.className.replace(reg, ' ') + } +} + +/** + * 判断是否是外部链接 + * + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path: string) { + const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path) + return isExternal +} + +/** + * 格式化增长率,保留两位小数 ,并且去掉末尾的0 取绝对值 + * + * @param growthRate + * @returns + */ +export function formatGrowthRate(growthRate: number) { + if (growthRate === 0) { + return '-' + } + + const formattedRate = Math.abs(growthRate * 100) + .toFixed(2) + .replace(/\.?0+$/, '') + return formattedRate + '%' +} diff --git a/src/utils/nprogress.ts b/src/utils/nprogress.ts new file mode 100644 index 0000000..c82e1f9 --- /dev/null +++ b/src/utils/nprogress.ts @@ -0,0 +1,18 @@ +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' + +// 进度条 +NProgress.configure({ + // 动画方式 + easing: 'ease', + // 递增进度条的速度 + speed: 500, + // 是否显示加载ico + showSpinner: false, + // 自动递增间隔 + trickleSpeed: 200, + // 初始化时的最小百分比 + minimum: 0.3 +}) + +export default NProgress diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..d648b22 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,99 @@ +import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from 'axios' +import qs from 'qs' +import { ApiCodeEnum } from '@/enums/api/code-enum' +import { AuthStorage, redirectToLogin } from '@/utils/auth' +import { useTokenRefresh } from '@/composables/auth/useTokenRefresh' +import { authConfig } from '@/settings' + +// 初始化token刷新组合式函数 +const { refreshTokenAndRetry } = useTokenRefresh() + +/** + * 创建 HTTP 请求实例 + */ +const httpRequest = axios.create({ + baseURL: import.meta.env.VITE_APP_API_URL, + timeout: 20000, + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + paramsSerializer: (params) => qs.stringify(params) +}) + +/** + * 请求拦截器 - 添加 Authorization 头 + */ +httpRequest.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const accessToken = AuthStorage.getAccessToken() + // 如果 Authorization 设置为 no-auth,则不携带 Token + if (config.headers.Authorization !== 'no-auth' && accessToken) { + config.headers.Authorization = `${accessToken}` + } else { + delete config.headers.Authorization + } + + return config + }, + (error) => { + console.error('Request interceptor error:', error) + return Promise.reject(error) + } +) + +/** + * 响应拦截器 - 统一处理响应和错误 + */ +httpRequest.interceptors.response.use( + (response: AxiosResponse) => { + // 如果响应是二进制数据,则直接返回response对象(用于文件下载、Excel导出、图片显示等) + if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') { + return response + } + + const { code, msg } = response.data + // 请求成功 + if (code === ApiCodeEnum.SUCCESS || code == '0' || code == '200') { + return response.data as any + } + + // 业务错误 + ElMessage.error(msg || '系统出错') + return Promise.reject(new Error(msg || 'Business Error')) + }, + async (error) => { + console.error('Response interceptor error:', error) + + const { config, response } = error + + // 网络错误或服务器无响应 + if (!response) { + ElMessage.error('网络连接失败,请检查网络设置') + return Promise.reject(error) + } + + const { code, message } = response.data as any + + switch (code) { + case ApiCodeEnum.ACCESS_TOKEN_INVALID: + // Access Token 过期 + if (authConfig.enableTokenRefresh) { + // 启用了token刷新,尝试刷新 + return refreshTokenAndRetry(config, httpRequest) + } else { + // 未启用token刷新,直接跳转登录页 + await redirectToLogin('登录已过期,请重新登录') + return Promise.reject(new Error(message || 'Access Token Invalid')) + } + + case ApiCodeEnum.REFRESH_TOKEN_INVALID: + // Refresh Token 过期,跳转登录页 + await redirectToLogin('登录已过期,请重新登录') + return Promise.reject(new Error(message || 'Refresh Token Invalid')) + + default: + ElMessage.error(message || '请求失败') + return Promise.reject(new Error(message || 'Request Error')) + } + } +) + +export default httpRequest diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..5ba784c --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,101 @@ +import { STORAGE_KEYS, APP_PREFIX } from '@/constants' + +/** + * 存储工具类 + * 提供localStorage和sessionStorage操作方法 + */ +export class Storage { + /** + * localStorage 存储 + */ + static set(key: string, value: any): void { + localStorage.setItem(key, JSON.stringify(value)) + } + + static get(key: string, defaultValue?: T): T { + const value = localStorage.getItem(key) + if (!value) return defaultValue as T + + try { + return JSON.parse(value) + } catch { + // 如果解析失败,返回原始字符串 + return value as unknown as T + } + } + + static remove(key: string): void { + localStorage.removeItem(key) + } + + /** + * sessionStorage 存储 + */ + static sessionSet(key: string, value: any): void { + sessionStorage.setItem(key, JSON.stringify(value)) + } + + static sessionGet(key: string, defaultValue?: T): T { + const value = sessionStorage.getItem(key) + if (!value) return defaultValue as T + + try { + return JSON.parse(value) + } catch { + // 如果解析失败,返回原始字符串 + return value as unknown as T + } + } + + static sessionRemove(key: string): void { + sessionStorage.removeItem(key) + } + + /** + * 存储清理工具方法 + */ + // 清理指定键的存储(localStorage + sessionStorage) + static clear(key: string): void { + localStorage.removeItem(key) + sessionStorage.removeItem(key) + } + + // 批量清理存储 + static clearMultiple(keys: string[]): void { + keys.forEach((key) => { + localStorage.removeItem(key) + sessionStorage.removeItem(key) + }) + } + + // 清理指定前缀的存储 + static clearByPrefix(prefix: string): void { + // localStorage 清理 + const localKeys = Object.keys(localStorage).filter((key) => key.startsWith(prefix)) + localKeys.forEach((key) => localStorage.removeItem(key)) + + // sessionStorage 清理 + const sessionKeys = Object.keys(sessionStorage).filter((key) => key.startsWith(prefix)) + sessionKeys.forEach((key) => sessionStorage.removeItem(key)) + } + + /** + * 项目特定的清理便利方法 + */ + // 清理所有项目相关的存储 + static clearAllProject(): void { + const keys = Object.values(STORAGE_KEYS) + this.clearMultiple(keys) + } + + // 清理特定分类的存储 + static clearByCategory(category: 'auth' | 'system' | 'ui' | 'app'): void { + const prefix = `${APP_PREFIX}:${category}:` + this.clearByPrefix(prefix) + } + + // 获取所有项目相关的存储键 + static getAllProjectKeys(): string[] { + return Object.values(STORAGE_KEYS) + } +} diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..5cfd1ac --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,112 @@ +import { ThemeMode } from '@/enums' + +// 辅助函数:将十六进制颜色转换为 RGB +function hexToRgb(hex: string): [number, number, number] { + const bigint = parseInt(hex.slice(1), 16) + return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255] +} + +// 辅助函数:将 RGB 转换为十六进制颜色 +function rgbToHex(r: number, g: number, b: number): string { + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}` +} + +// 辅助函数:调整颜色亮度 +/** function adjustBrightness(hex: string, factor: number, theme: string): string { + const rgb = hexToRgb(hex); + // 是否是暗黑模式 + const isDarkMode = theme === "dark" ? 0 : 255; + const newRgb = rgb.map((val) => + Math.max(0, Math.min(255, Math.round(val + (isDarkMode - val) * factor))) + ) as [number, number, number]; + return rgbToHex(...newRgb); +} */ + +/** + * 加深颜色值 + * @param {String} color 颜色值字符串 + * @param {Number} level 加深的程度,限0-1之间 + * @returns {String} 返回处理后的颜色值 + */ +export function getDarkColor(color: string, level: number): string { + const rgb = hexToRgb(color) + for (let i = 0; i < 3; i++) rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level)) + return rgbToHex(rgb[0], rgb[1], rgb[2]) +} + +/** + * 变浅颜色值 + * @param {String} color 颜色值字符串 + * @param {Number} level 加深的程度,限0-1之间 + * @returns {String} 返回处理后的颜色值 + */ +export const getLightColor = (color: string, level: number): string => { + const rgb = hexToRgb(color) + for (let i = 0; i < 3; i++) rgb[i] = Math.round(255 * level + rgb[i] * (1 - level)) + return rgbToHex(rgb[0], rgb[1], rgb[2]) +} + +/** + * 生成主题色 + * @param primary 主题色 + * @param theme 主题类型 + */ +export function generateThemeColors(primary: string, theme: ThemeMode) { + const colors: Record = { + primary + } + + // 生成浅色变体 + for (let i = 1; i <= 9; i++) { + colors[`primary-light-${i}`] = + theme === ThemeMode.LIGHT + ? `${getLightColor(primary, i / 10)}` + : `${getDarkColor(primary, i / 10)}` + } + + // 生成深色变体 + colors['primary-dark-2'] = + theme === ThemeMode.LIGHT ? `${getLightColor(primary, 0.2)}` : `${getDarkColor(primary, 0.3)}` + + return colors +} + +export function applyTheme(colors: Record) { + const el = document.documentElement + + Object.entries(colors).forEach(([key, value]) => { + el.style.setProperty(`--el-color-${key}`, value) + }) + + // 确保主题色立即生效,强制重新渲染 + requestAnimationFrame(() => { + // 触发样式重新计算 + el.style.setProperty('--theme-update-trigger', Date.now().toString()) + }) +} + +/** + * 切换暗黑模式 + * + * @param isDark 是否启用暗黑模式 + */ +export function toggleDarkMode(isDark: boolean) { + if (isDark) { + document.documentElement.classList.add(ThemeMode.DARK) + } else { + document.documentElement.classList.remove(ThemeMode.DARK) + } +} + +/** + * 切换浅色主题下的侧边栏颜色方案 + * + * @param isBlue 布尔值,表示是否开启深蓝色侧边栏颜色方案 + */ +export function toggleSidebarColor(isBuleSidebar: boolean) { + if (isBuleSidebar) { + document.documentElement.classList.add('sidebar-color-blue') + } else { + document.documentElement.classList.remove('sidebar-color-blue') + } +} diff --git a/src/views/BoosAccountManagement/components/BoosAccountDetails.vue b/src/views/BoosAccountManagement/components/BoosAccountDetails.vue new file mode 100644 index 0000000..11c9665 --- /dev/null +++ b/src/views/BoosAccountManagement/components/BoosAccountDetails.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/views/BoosAccountManagement/components/BoosAccountForm.vue b/src/views/BoosAccountManagement/components/BoosAccountForm.vue new file mode 100644 index 0000000..ebec391 --- /dev/null +++ b/src/views/BoosAccountManagement/components/BoosAccountForm.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/views/BoosAccountManagement/index.vue b/src/views/BoosAccountManagement/index.vue new file mode 100644 index 0000000..ba56f2f --- /dev/null +++ b/src/views/BoosAccountManagement/index.vue @@ -0,0 +1,257 @@ + + + diff --git a/src/views/TaskManagement/components/TaskDetails.vue b/src/views/TaskManagement/components/TaskDetails.vue new file mode 100644 index 0000000..13d026b --- /dev/null +++ b/src/views/TaskManagement/components/TaskDetails.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/views/TaskManagement/components/TaskForm.vue b/src/views/TaskManagement/components/TaskForm.vue new file mode 100644 index 0000000..876a20b --- /dev/null +++ b/src/views/TaskManagement/components/TaskForm.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/views/TaskManagement/index.vue b/src/views/TaskManagement/index.vue new file mode 100644 index 0000000..820aea0 --- /dev/null +++ b/src/views/TaskManagement/index.vue @@ -0,0 +1,254 @@ + + + diff --git a/src/views/calibration/accountAdjustmentApplication/components/DeptTree.vue b/src/views/calibration/accountAdjustmentApplication/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/accountAdjustmentApplication/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/accountAdjustmentApplication/components/UserImport.vue b/src/views/calibration/accountAdjustmentApplication/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/accountAdjustmentApplication/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/accountAdjustmentApplication/index.vue b/src/views/calibration/accountAdjustmentApplication/index.vue new file mode 100644 index 0000000..5b9354d --- /dev/null +++ b/src/views/calibration/accountAdjustmentApplication/index.vue @@ -0,0 +1,323 @@ + + + diff --git a/src/views/calibration/announcementManagement/components/AnnouncementForm.vue b/src/views/calibration/announcementManagement/components/AnnouncementForm.vue new file mode 100644 index 0000000..b1a79c9 --- /dev/null +++ b/src/views/calibration/announcementManagement/components/AnnouncementForm.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/src/views/calibration/announcementManagement/index.vue b/src/views/calibration/announcementManagement/index.vue new file mode 100644 index 0000000..b7fe927 --- /dev/null +++ b/src/views/calibration/announcementManagement/index.vue @@ -0,0 +1,234 @@ + + + diff --git a/src/views/calibration/applicationForSealApproval/components/StampingForm.vue b/src/views/calibration/applicationForSealApproval/components/StampingForm.vue new file mode 100644 index 0000000..ebc0bea --- /dev/null +++ b/src/views/calibration/applicationForSealApproval/components/StampingForm.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/src/views/calibration/applicationForSealApproval/index.vue b/src/views/calibration/applicationForSealApproval/index.vue new file mode 100644 index 0000000..6dce7c0 --- /dev/null +++ b/src/views/calibration/applicationForSealApproval/index.vue @@ -0,0 +1,294 @@ + + + diff --git a/src/views/calibration/businessSystem/bidRegistration/components/BidRegistrationForm.vue b/src/views/calibration/businessSystem/bidRegistration/components/BidRegistrationForm.vue new file mode 100644 index 0000000..f97af67 --- /dev/null +++ b/src/views/calibration/businessSystem/bidRegistration/components/BidRegistrationForm.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/src/views/calibration/businessSystem/bidRegistration/components/BusinessProjectForm.vue b/src/views/calibration/businessSystem/bidRegistration/components/BusinessProjectForm.vue new file mode 100644 index 0000000..1ec4e3f --- /dev/null +++ b/src/views/calibration/businessSystem/bidRegistration/components/BusinessProjectForm.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/src/views/calibration/businessSystem/bidRegistration/index.vue b/src/views/calibration/businessSystem/bidRegistration/index.vue new file mode 100644 index 0000000..5a1b2b2 --- /dev/null +++ b/src/views/calibration/businessSystem/bidRegistration/index.vue @@ -0,0 +1,313 @@ + + + + diff --git a/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BidRegistrationForm.vue b/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BidRegistrationForm.vue new file mode 100644 index 0000000..7deed6a --- /dev/null +++ b/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BidRegistrationForm.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BusinessProjectForm.vue b/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BusinessProjectForm.vue new file mode 100644 index 0000000..1ec4e3f --- /dev/null +++ b/src/views/calibration/businessSystem/conflictOfInterestSearch/components/BusinessProjectForm.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/src/views/calibration/businessSystem/conflictOfInterestSearch/index.vue b/src/views/calibration/businessSystem/conflictOfInterestSearch/index.vue new file mode 100644 index 0000000..b57a314 --- /dev/null +++ b/src/views/calibration/businessSystem/conflictOfInterestSearch/index.vue @@ -0,0 +1,486 @@ + + + + + + diff --git a/src/views/calibration/businessSystem/preRegistration/components/PreRegistrationForm.vue b/src/views/calibration/businessSystem/preRegistration/components/PreRegistrationForm.vue new file mode 100644 index 0000000..edda6c6 --- /dev/null +++ b/src/views/calibration/businessSystem/preRegistration/components/PreRegistrationForm.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/src/views/calibration/businessSystem/preRegistration/index.vue b/src/views/calibration/businessSystem/preRegistration/index.vue new file mode 100644 index 0000000..887c971 --- /dev/null +++ b/src/views/calibration/businessSystem/preRegistration/index.vue @@ -0,0 +1,307 @@ + + + diff --git a/src/views/calibration/businessSystem/projectRegistration/components/AdjustTheYearForm.vue b/src/views/calibration/businessSystem/projectRegistration/components/AdjustTheYearForm.vue new file mode 100644 index 0000000..99018ec --- /dev/null +++ b/src/views/calibration/businessSystem/projectRegistration/components/AdjustTheYearForm.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/views/calibration/businessSystem/projectRegistration/components/BidRegistrationForm.vue b/src/views/calibration/businessSystem/projectRegistration/components/BidRegistrationForm.vue new file mode 100644 index 0000000..7deed6a --- /dev/null +++ b/src/views/calibration/businessSystem/projectRegistration/components/BidRegistrationForm.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/src/views/calibration/businessSystem/projectRegistration/components/BusinessProjectForm.vue b/src/views/calibration/businessSystem/projectRegistration/components/BusinessProjectForm.vue new file mode 100644 index 0000000..69f4531 --- /dev/null +++ b/src/views/calibration/businessSystem/projectRegistration/components/BusinessProjectForm.vue @@ -0,0 +1,555 @@ + + + + + diff --git a/src/views/calibration/businessSystem/projectRegistration/index.vue b/src/views/calibration/businessSystem/projectRegistration/index.vue new file mode 100644 index 0000000..2b1c955 --- /dev/null +++ b/src/views/calibration/businessSystem/projectRegistration/index.vue @@ -0,0 +1,342 @@ + + + + diff --git a/src/views/calibration/caseLabel/components/CaseLabelForm.vue b/src/views/calibration/caseLabel/components/CaseLabelForm.vue new file mode 100644 index 0000000..3e60046 --- /dev/null +++ b/src/views/calibration/caseLabel/components/CaseLabelForm.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/views/calibration/caseLabel/index.vue b/src/views/calibration/caseLabel/index.vue new file mode 100644 index 0000000..64b966c --- /dev/null +++ b/src/views/calibration/caseLabel/index.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/CaseLogComponent/components/CaseLogForm.vue b/src/views/calibration/caseManagement/components/CaseLogComponent/components/CaseLogForm.vue new file mode 100644 index 0000000..03f1c45 --- /dev/null +++ b/src/views/calibration/caseManagement/components/CaseLogComponent/components/CaseLogForm.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/CaseLogComponent/index.vue b/src/views/calibration/caseManagement/components/CaseLogComponent/index.vue new file mode 100644 index 0000000..e699f53 --- /dev/null +++ b/src/views/calibration/caseManagement/components/CaseLogComponent/index.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/CaseManagementForm.vue b/src/views/calibration/caseManagement/components/CaseManagementForm.vue new file mode 100644 index 0000000..f1ae4de --- /dev/null +++ b/src/views/calibration/caseManagement/components/CaseManagementForm.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/ChangeApplicationComponent/components/CaseInvoiceForm.vue b/src/views/calibration/caseManagement/components/ChangeApplicationComponent/components/CaseInvoiceForm.vue new file mode 100644 index 0000000..092c951 --- /dev/null +++ b/src/views/calibration/caseManagement/components/ChangeApplicationComponent/components/CaseInvoiceForm.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/ChangeApplicationComponent/index.vue b/src/views/calibration/caseManagement/components/ChangeApplicationComponent/index.vue new file mode 100644 index 0000000..a4c2b96 --- /dev/null +++ b/src/views/calibration/caseManagement/components/ChangeApplicationComponent/index.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/CumulativeReceivedPaymentForm.vue b/src/views/calibration/caseManagement/components/CumulativeReceivedPaymentForm.vue new file mode 100644 index 0000000..f5122cc --- /dev/null +++ b/src/views/calibration/caseManagement/components/CumulativeReceivedPaymentForm.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/views/calibration/caseManagement/components/UploadOfDataForm.vue b/src/views/calibration/caseManagement/components/UploadOfDataForm.vue new file mode 100644 index 0000000..42b249e --- /dev/null +++ b/src/views/calibration/caseManagement/components/UploadOfDataForm.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/views/calibration/caseManagement/index.vue b/src/views/calibration/caseManagement/index.vue new file mode 100644 index 0000000..18374b0 --- /dev/null +++ b/src/views/calibration/caseManagement/index.vue @@ -0,0 +1,604 @@ + + + + + diff --git a/src/views/calibration/department/index.vue b/src/views/calibration/department/index.vue new file mode 100644 index 0000000..a33023b --- /dev/null +++ b/src/views/calibration/department/index.vue @@ -0,0 +1,258 @@ + + + diff --git a/src/views/calibration/departureFinancialRegistration/components/SeparationRegistrationForm.vue b/src/views/calibration/departureFinancialRegistration/components/SeparationRegistrationForm.vue new file mode 100644 index 0000000..78172ee --- /dev/null +++ b/src/views/calibration/departureFinancialRegistration/components/SeparationRegistrationForm.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/views/calibration/departureFinancialRegistration/components/StampingForm.vue b/src/views/calibration/departureFinancialRegistration/components/StampingForm.vue new file mode 100644 index 0000000..c4b6fd4 --- /dev/null +++ b/src/views/calibration/departureFinancialRegistration/components/StampingForm.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/src/views/calibration/departureFinancialRegistration/index.vue b/src/views/calibration/departureFinancialRegistration/index.vue new file mode 100644 index 0000000..8898cff --- /dev/null +++ b/src/views/calibration/departureFinancialRegistration/index.vue @@ -0,0 +1,284 @@ + + + diff --git a/src/views/calibration/inventoryRegistration/components/StampingForm.vue b/src/views/calibration/inventoryRegistration/components/StampingForm.vue new file mode 100644 index 0000000..0c99431 --- /dev/null +++ b/src/views/calibration/inventoryRegistration/components/StampingForm.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/views/calibration/inventoryRegistration/index.vue b/src/views/calibration/inventoryRegistration/index.vue new file mode 100644 index 0000000..6ae3efd --- /dev/null +++ b/src/views/calibration/inventoryRegistration/index.vue @@ -0,0 +1,260 @@ + + + diff --git a/src/views/calibration/invoiceApplication/components/DeptTree.vue b/src/views/calibration/invoiceApplication/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/invoiceApplication/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/invoiceApplication/components/UserImport.vue b/src/views/calibration/invoiceApplication/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/invoiceApplication/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/invoiceApplication/index.vue b/src/views/calibration/invoiceApplication/index.vue new file mode 100644 index 0000000..2151f2b --- /dev/null +++ b/src/views/calibration/invoiceApplication/index.vue @@ -0,0 +1,346 @@ + + + diff --git a/src/views/calibration/lawFirmStandardDocuments/components/LawFirmStandardsForm.vue b/src/views/calibration/lawFirmStandardDocuments/components/LawFirmStandardsForm.vue new file mode 100644 index 0000000..8a01b05 --- /dev/null +++ b/src/views/calibration/lawFirmStandardDocuments/components/LawFirmStandardsForm.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/views/calibration/lawFirmStandardDocuments/index.vue b/src/views/calibration/lawFirmStandardDocuments/index.vue new file mode 100644 index 0000000..f805bda --- /dev/null +++ b/src/views/calibration/lawFirmStandardDocuments/index.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/src/views/calibration/lmportantScheduleManagement/components/LmportantScheduleForm.vue b/src/views/calibration/lmportantScheduleManagement/components/LmportantScheduleForm.vue new file mode 100644 index 0000000..2683855 --- /dev/null +++ b/src/views/calibration/lmportantScheduleManagement/components/LmportantScheduleForm.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/views/calibration/lmportantScheduleManagement/index.vue b/src/views/calibration/lmportantScheduleManagement/index.vue new file mode 100644 index 0000000..e116f3d --- /dev/null +++ b/src/views/calibration/lmportantScheduleManagement/index.vue @@ -0,0 +1,265 @@ + + + diff --git a/src/views/calibration/onboardingRegistration/components/DepartmentSelectionForm.vue b/src/views/calibration/onboardingRegistration/components/DepartmentSelectionForm.vue new file mode 100644 index 0000000..45565d9 --- /dev/null +++ b/src/views/calibration/onboardingRegistration/components/DepartmentSelectionForm.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/views/calibration/onboardingRegistration/index.vue b/src/views/calibration/onboardingRegistration/index.vue new file mode 100644 index 0000000..2facedc --- /dev/null +++ b/src/views/calibration/onboardingRegistration/index.vue @@ -0,0 +1,294 @@ + + + diff --git a/src/views/calibration/paymentApplicationForm/components/DeptTree.vue b/src/views/calibration/paymentApplicationForm/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/paymentApplicationForm/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/paymentApplicationForm/components/UserImport.vue b/src/views/calibration/paymentApplicationForm/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/paymentApplicationForm/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/paymentApplicationForm/index.vue b/src/views/calibration/paymentApplicationForm/index.vue new file mode 100644 index 0000000..acbe6d0 --- /dev/null +++ b/src/views/calibration/paymentApplicationForm/index.vue @@ -0,0 +1,368 @@ + + + diff --git a/src/views/calibration/permissionManagement/components/permissionForm.vue b/src/views/calibration/permissionManagement/components/permissionForm.vue new file mode 100644 index 0000000..8fe4362 --- /dev/null +++ b/src/views/calibration/permissionManagement/components/permissionForm.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/src/views/calibration/permissionManagement/index.vue b/src/views/calibration/permissionManagement/index.vue new file mode 100644 index 0000000..c70a425 --- /dev/null +++ b/src/views/calibration/permissionManagement/index.vue @@ -0,0 +1,235 @@ + + + diff --git a/src/views/calibration/personnelManagement/components/DeptTree.vue b/src/views/calibration/personnelManagement/components/DeptTree.vue new file mode 100644 index 0000000..072822b --- /dev/null +++ b/src/views/calibration/personnelManagement/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/personnelManagement/components/EditorPasswordForm.vue b/src/views/calibration/personnelManagement/components/EditorPasswordForm.vue new file mode 100644 index 0000000..90fd452 --- /dev/null +++ b/src/views/calibration/personnelManagement/components/EditorPasswordForm.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/src/views/calibration/personnelManagement/components/UserImport.vue b/src/views/calibration/personnelManagement/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/personnelManagement/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/personnelManagement/index.vue b/src/views/calibration/personnelManagement/index.vue new file mode 100644 index 0000000..5fefabf --- /dev/null +++ b/src/views/calibration/personnelManagement/index.vue @@ -0,0 +1,1088 @@ + + + + + + + diff --git a/src/views/calibration/registrationPlatform/components/RegistrationPlatformForm.vue b/src/views/calibration/registrationPlatform/components/RegistrationPlatformForm.vue new file mode 100644 index 0000000..5820eb3 --- /dev/null +++ b/src/views/calibration/registrationPlatform/components/RegistrationPlatformForm.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/src/views/calibration/registrationPlatform/index.vue b/src/views/calibration/registrationPlatform/index.vue new file mode 100644 index 0000000..b921de5 --- /dev/null +++ b/src/views/calibration/registrationPlatform/index.vue @@ -0,0 +1,221 @@ + + + diff --git a/src/views/calibration/reimbursement/components/DeptTree.vue b/src/views/calibration/reimbursement/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/reimbursement/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/reimbursement/components/UserImport.vue b/src/views/calibration/reimbursement/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/reimbursement/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/reimbursement/index.vue b/src/views/calibration/reimbursement/index.vue new file mode 100644 index 0000000..ee8d9c9 --- /dev/null +++ b/src/views/calibration/reimbursement/index.vue @@ -0,0 +1,349 @@ + + + diff --git a/src/views/calibration/revenueRecognition/components/DeptTree.vue b/src/views/calibration/revenueRecognition/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/revenueRecognition/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/revenueRecognition/components/UserImport.vue b/src/views/calibration/revenueRecognition/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/revenueRecognition/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/revenueRecognition/index.vue b/src/views/calibration/revenueRecognition/index.vue new file mode 100644 index 0000000..8d31fbb --- /dev/null +++ b/src/views/calibration/revenueRecognition/index.vue @@ -0,0 +1,343 @@ + + + diff --git a/src/views/calibration/roleManagement/components/GrantPermissionsForm.vue b/src/views/calibration/roleManagement/components/GrantPermissionsForm.vue new file mode 100644 index 0000000..9776afb --- /dev/null +++ b/src/views/calibration/roleManagement/components/GrantPermissionsForm.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/views/calibration/roleManagement/components/RoleForm.vue b/src/views/calibration/roleManagement/components/RoleForm.vue new file mode 100644 index 0000000..c30bd39 --- /dev/null +++ b/src/views/calibration/roleManagement/components/RoleForm.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/views/calibration/roleManagement/index.vue b/src/views/calibration/roleManagement/index.vue new file mode 100644 index 0000000..eab496f --- /dev/null +++ b/src/views/calibration/roleManagement/index.vue @@ -0,0 +1,261 @@ + + + diff --git a/src/views/calibration/salaryBonusAdjustment/components/DeptTree.vue b/src/views/calibration/salaryBonusAdjustment/components/DeptTree.vue new file mode 100644 index 0000000..a0a12af --- /dev/null +++ b/src/views/calibration/salaryBonusAdjustment/components/DeptTree.vue @@ -0,0 +1,83 @@ + + + + diff --git a/src/views/calibration/salaryBonusAdjustment/components/UserImport.vue b/src/views/calibration/salaryBonusAdjustment/components/UserImport.vue new file mode 100644 index 0000000..3b3b138 --- /dev/null +++ b/src/views/calibration/salaryBonusAdjustment/components/UserImport.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/views/calibration/salaryBonusAdjustment/index.vue b/src/views/calibration/salaryBonusAdjustment/index.vue new file mode 100644 index 0000000..d62e655 --- /dev/null +++ b/src/views/calibration/salaryBonusAdjustment/index.vue @@ -0,0 +1,340 @@ + + + diff --git a/src/views/calibration/systemManagement/components/SystemForm.vue b/src/views/calibration/systemManagement/components/SystemForm.vue new file mode 100644 index 0000000..69a6379 --- /dev/null +++ b/src/views/calibration/systemManagement/components/SystemForm.vue @@ -0,0 +1,161 @@ + + + diff --git a/src/views/calibration/systemManagement/index.vue b/src/views/calibration/systemManagement/index.vue new file mode 100644 index 0000000..2892153 --- /dev/null +++ b/src/views/calibration/systemManagement/index.vue @@ -0,0 +1,234 @@ + + + diff --git a/src/views/calibration/teamManagement/components/GrantPermissionsForm.vue b/src/views/calibration/teamManagement/components/GrantPermissionsForm.vue new file mode 100644 index 0000000..9776afb --- /dev/null +++ b/src/views/calibration/teamManagement/components/GrantPermissionsForm.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/views/calibration/teamManagement/components/RoleForm.vue b/src/views/calibration/teamManagement/components/RoleForm.vue new file mode 100644 index 0000000..c30bd39 --- /dev/null +++ b/src/views/calibration/teamManagement/components/RoleForm.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/views/calibration/teamManagement/components/TeamForm.vue b/src/views/calibration/teamManagement/components/TeamForm.vue new file mode 100644 index 0000000..bda5880 --- /dev/null +++ b/src/views/calibration/teamManagement/components/TeamForm.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/views/calibration/teamManagement/index.vue b/src/views/calibration/teamManagement/index.vue new file mode 100644 index 0000000..fe54f35 --- /dev/null +++ b/src/views/calibration/teamManagement/index.vue @@ -0,0 +1,230 @@ + + + diff --git a/src/views/dashboard/AnnouncementDetails.vue b/src/views/dashboard/AnnouncementDetails.vue new file mode 100644 index 0000000..ed60255 --- /dev/null +++ b/src/views/dashboard/AnnouncementDetails.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/views/dashboard/config.ts b/src/views/dashboard/config.ts new file mode 100644 index 0000000..43cde8f --- /dev/null +++ b/src/views/dashboard/config.ts @@ -0,0 +1,106 @@ +export const quickConfigurationList = [ + { + name: 'PersonnelUser', + title: '人事管理' + }, + { + name: 'Department', + title: '部门管理' + }, + { + name: 'RoleManagement', + title: '角色管理' + }, + { + name: 'TeamManagement', + title: '团队管理' + }, + { + name: 'PermissionManagement', + title: '权限管理' + }, + { + name: 'OnboardingRegistration', + title: '入职财务登记' + }, + { + name: 'Departure', + title: '离职财务登记' + }, + { + name: 'InvoiceApplication', + title: '开票申请' + }, + { + name: 'RevenueRecognition', + title: '收入确认' + }, + { + name: 'AccountAdjustmentApplication', + title: '调账申请' + }, + { + name: 'PaymentApplicationForm', + title: '付款申请单' + }, + { + name: 'Reimbursement', + title: '报销' + }, + { + name: 'SalaryBonusAdjustment', + title: '工资/奖金变更' + }, + { + name: 'ConflictOfInterestSearch', + title: '利益冲突检索' + }, + { + name: 'PreRegistration', + title: '预立案登记' + }, + { + name: 'BidRegistration', + title: '投标登记' + }, + { + name: 'ProjectRegistration', + title: '立项登记' + }, + { + name: 'CaseManagement', + title: '案件管理' + }, + { + name: 'CaseLabel', + title: '案件标签' + }, + { + name: 'ApplicationForSealApproval', + title: '申请用印' + }, + { + name: 'StampPerformanceIndex', + title: '业绩展示' + }, + { + name: 'InventoryRegistration', + title: '入库登记' + }, + { + name: 'RegistrationPlatform', + title: '注册平台登记' + }, + { + name: 'AnnouncementManagement', + title: '公告管理' + }, + { + name: 'LawFirmStandardDocuments', + title: '律所标准文件' + }, + { + name: 'LmportantScheduleManagement', + title: '日程管理' + } +] diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 0000000..7c81769 --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,588 @@ + + + + + diff --git a/src/views/error/401.vue b/src/views/error/401.vue new file mode 100644 index 0000000..38cee4a --- /dev/null +++ b/src/views/error/401.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/views/error/404.vue b/src/views/error/404.vue new file mode 100644 index 0000000..0d9a5c6 --- /dev/null +++ b/src/views/error/404.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/views/login/components/Login.vue b/src/views/login/components/Login.vue new file mode 100644 index 0000000..9d26351 --- /dev/null +++ b/src/views/login/components/Login.vue @@ -0,0 +1,156 @@ + + + + diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..1d42b40 --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4b4ebba --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["esnext", "dom"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + + // 严格性和类型检查相关配置 + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // 模块和兼容性相关配置 + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "resolveJsonModule": true, + + // 调试和兼容性相关配置 + "sourceMap": true, + "useDefineForClassFields": true, + "allowJs": true, + + // 类型声明相关配置 + "types": ["node", "vite/client", "element-plus/global"] + }, + + "include": [ + "mock/**/*.ts", + "src/**/*.ts", + "src/**/*.vue", + "vite.config.ts", + "eslint.config.ts", + "uno.config.ts" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/uno.config.ts b/uno.config.ts new file mode 100644 index 0000000..eaf5eb3 --- /dev/null +++ b/uno.config.ts @@ -0,0 +1,82 @@ +import { + defineConfig, + presetAttributify, + presetIcons, + presetTypography, + presetUno, + presetWebFonts, + transformerDirectives, + transformerVariantGroup +} from 'unocss' + +import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders' +import fs from 'fs' + +// 本地SVG图标目录 +const iconsDir = './src/assets/icons' + +// 读取本地 SVG 目录,自动生成 safelist +const generateSafeList = () => { + try { + return fs + .readdirSync(iconsDir) + .filter((file) => file.endsWith('.svg')) + .map((file) => `i-svg:${file.replace('.svg', '')}`) + } catch (error) { + console.error('无法读取图标目录:', error) + return [] + } +} + +export default defineConfig({ + // 自定义快捷类 + shortcuts: { + 'wh-full': 'w-full h-full', + 'flex-center': 'flex justify-center items-center', + 'flex-x-center': 'flex justify-center', + 'flex-y-center': 'flex items-center', + 'flex-x-start': 'flex items-center justify-start', + 'flex-x-between': 'flex items-center justify-between', + 'flex-x-end': 'flex items-center justify-end' + }, + theme: { + colors: { + primary: 'var(--el-color-primary)', + primary_dark: 'var(--el-color-primary-light-5)' + }, + breakpoints: Object.fromEntries( + [640, 768, 1024, 1280, 1536, 1920, 2560].map((size, index) => [ + ['sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl'][index], + `${size}px` + ]) + ) + }, + presets: [ + presetUno(), + presetAttributify(), + presetIcons({ + // 额外属性 + extraProperties: { + display: 'inline-block', + width: '1em', + height: '1em' + }, + // 图表集合 + collections: { + // svg 是图标集合名称,使用 `i-svg:图标名` 调用 + svg: FileSystemIconLoader(iconsDir, (svg) => { + // 如果 `fill` 没有定义,则添加 `fill="currentColor"` + return svg.includes('fill="') ? svg : svg.replace(/^ { + const env = loadEnv(mode, process.cwd()) + const isProduction = mode === 'production' + + return { + base: './', // 设置为相对路径,支持直接打开index.html + resolve: { + alias: { + '@': pathSrc + } + }, + css: { + preprocessorOptions: { + // 定义全局 SCSS 变量 + scss: { + additionalData: `@use "@/styles/variables.scss" as *;` + } + } + }, + server: { + host: '0.0.0.0', + port: +(env.VITE_APP_PORT as string), + open: true, + proxy: { + '/dev-api/api2': { + changeOrigin: true, + // target: 'http://47.108.113.7:8000', + // target: 'http://47.108.113.7:8000', + // target: 'http://192.168.31.69:8006', + target: 'http://199.168.137.123:8000', + rewrite: (path: string) => { + return path.replace(/^\/dev-api\/api2/, '') + } + }, + // 代理 /dev-api 的请求 + [env.VITE_APP_BASE_API as string]: { + changeOrigin: true, + // 代理目标地址:https://api.youlai.tech + target: env.VITE_APP_API_URL as string, + rewrite: (path: string) => { + return path.replace(new RegExp('^' + (env.VITE_APP_BASE_API as string)), '') + } + } + } + }, + plugins: [ + vue(), + ...(env.VITE_MOCK_DEV_SERVER === 'true' ? [mockDevServerPlugin()] : []), + UnoCSS(), + // API 自动导入 + AutoImport({ + // 导入 Vue 函数,如:ref, reactive, toRef 等 + imports: ['vue', '@vueuse/core', 'pinia', 'vue-router', 'vue-i18n'], + resolvers: [ + // 导入 Element Plus函数,如:ElMessage, ElMessageBox 等 + ElementPlusResolver({ importStyle: 'sass' }) + ], + eslintrc: { + enabled: false, + filepath: './.eslintrc-auto-import.json', + globalsPropValue: true + }, + vueTemplate: true, + // 导入函数类型声明文件路径 (false:关闭自动生成) + dts: false + // dts: "src/types/auto-imports.d.ts", + }), + // 组件自动导入 + Components({ + resolvers: [ + // 导入 Element Plus 组件 + ElementPlusResolver({ importStyle: 'sass' }) + ], + // 指定自定义组件位置(默认:src/components) + dirs: ['src/components', 'src/**/components'], + // 导入组件类型声明文件路径 (false:关闭自动生成) + dts: false + // dts: "src/types/components.d.ts", + }) + ], + // 预加载项目必需的组件 + optimizeDeps: { + include: [ + 'vue', + 'vue-router', + 'element-plus', + 'pinia', + 'axios', + '@vueuse/core', + 'codemirror-editor-vue3', + 'default-passive-events', + 'exceljs', + 'path-to-regexp', + 'echarts/core', + 'echarts/renderers', + 'echarts/charts', + 'echarts/components', + 'vue-i18n', + 'nprogress', + 'sortablejs', + 'qs', + 'path-browserify', + '@stomp/stompjs', + '@element-plus/icons-vue', + 'element-plus/es', + 'element-plus/es/locale/lang/en', + 'element-plus/es/locale/lang/zh-cn', + 'element-plus/es/components/alert/style/index', + 'element-plus/es/components/avatar/style/index', + 'element-plus/es/components/backtop/style/index', + 'element-plus/es/components/badge/style/index', + 'element-plus/es/components/base/style/index', + 'element-plus/es/components/breadcrumb-item/style/index', + 'element-plus/es/components/breadcrumb/style/index', + 'element-plus/es/components/button/style/index', + 'element-plus/es/components/card/style/index', + 'element-plus/es/components/cascader/style/index', + 'element-plus/es/components/checkbox-group/style/index', + 'element-plus/es/components/checkbox/style/index', + 'element-plus/es/components/col/style/index', + 'element-plus/es/components/color-picker/style/index', + 'element-plus/es/components/config-provider/style/index', + 'element-plus/es/components/date-picker/style/index', + 'element-plus/es/components/descriptions-item/style/index', + 'element-plus/es/components/descriptions/style/index', + 'element-plus/es/components/dialog/style/index', + 'element-plus/es/components/divider/style/index', + 'element-plus/es/components/drawer/style/index', + 'element-plus/es/components/dropdown-item/style/index', + 'element-plus/es/components/dropdown-menu/style/index', + 'element-plus/es/components/dropdown/style/index', + 'element-plus/es/components/empty/style/index', + 'element-plus/es/components/form-item/style/index', + 'element-plus/es/components/form/style/index', + 'element-plus/es/components/icon/style/index', + 'element-plus/es/components/image-viewer/style/index', + 'element-plus/es/components/image/style/index', + 'element-plus/es/components/input-number/style/index', + 'element-plus/es/components/input-tag/style/index', + 'element-plus/es/components/input/style/index', + 'element-plus/es/components/link/style/index', + 'element-plus/es/components/loading/style/index', + 'element-plus/es/components/menu-item/style/index', + 'element-plus/es/components/menu/style/index', + 'element-plus/es/components/message-box/style/index', + 'element-plus/es/components/message/style/index', + 'element-plus/es/components/notification/style/index', + 'element-plus/es/components/option/style/index', + 'element-plus/es/components/pagination/style/index', + 'element-plus/es/components/popover/style/index', + 'element-plus/es/components/progress/style/index', + 'element-plus/es/components/radio-button/style/index', + 'element-plus/es/components/radio-group/style/index', + 'element-plus/es/components/radio/style/index', + 'element-plus/es/components/row/style/index', + 'element-plus/es/components/scrollbar/style/index', + 'element-plus/es/components/select/style/index', + 'element-plus/es/components/skeleton-item/style/index', + 'element-plus/es/components/skeleton/style/index', + 'element-plus/es/components/step/style/index', + 'element-plus/es/components/steps/style/index', + 'element-plus/es/components/sub-menu/style/index', + 'element-plus/es/components/switch/style/index', + 'element-plus/es/components/tab-pane/style/index', + 'element-plus/es/components/table-column/style/index', + 'element-plus/es/components/table/style/index', + 'element-plus/es/components/tabs/style/index', + 'element-plus/es/components/tag/style/index', + 'element-plus/es/components/text/style/index', + 'element-plus/es/components/time-picker/style/index', + 'element-plus/es/components/time-select/style/index', + 'element-plus/es/components/timeline-item/style/index', + 'element-plus/es/components/timeline/style/index', + 'element-plus/es/components/tooltip/style/index', + 'element-plus/es/components/tree-select/style/index', + 'element-plus/es/components/tree/style/index', + 'element-plus/es/components/upload/style/index', + 'element-plus/es/components/watermark/style/index', + 'element-plus/es/components/checkbox-button/style/index', + 'element-plus/es/components/space/style/index' + ] + }, + // 构建配置 + build: { + chunkSizeWarningLimit: 2000, // 消除打包大小超过500kb警告 + minify: isProduction ? ('terser' as const) : false, // 只在生产环境启用压缩 + terserOptions: isProduction + ? { + compress: { + keep_infinity: true, // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的性能问题 + drop_console: true, // 生产环境去除 console.log, console.warn, console.error 等 + drop_debugger: true, // 生产环境去除 debugger + pure_funcs: ['console.log', 'console.info'] // 移除指定的函数调用 + }, + format: { + comments: false // 删除注释 + } + } + : {}, + rollupOptions: { + output: { + // manualChunks: { + // "vue-i18n": ["vue-i18n"], + // }, + // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值 + entryFileNames: 'js/[name].[hash].js', + // 用于命名代码拆分时创建的共享块的输出命名 + chunkFileNames: 'js/[name].[hash].js', + // 用于输出静态资源的命名,[ext]表示文件扩展名 + assetFileNames: (assetInfo: any) => { + const info = assetInfo.name.split('.') + let extType = info[info.length - 1] + // console.log('文件信息', assetInfo.name) + if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) { + extType = 'media' + } else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) { + extType = 'img' + } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) { + extType = 'fonts' + } + return `${extType}/[name].[hash].[ext]` + } + } + } + }, + define: { + __APP_INFO__: JSON.stringify(__APP_INFO__) + } + } +})