From 963448382d3c07378fa4c9da36a2e4c5f6638b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=82=A5=E7=BE=8A?= <1048382248@qq.com> Date: Tue, 4 Nov 2025 16:08:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20=E6=96=B0=E7=89=88UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + VideoAnalysis/Components/App.razor | 25 - VideoAnalysis/Components/Error.razor | 36 - .../Components/Layouts/BasicLayout.razor | 36 - .../Components/Layouts/BasicLayout.razor.cs | 106 - .../Components/Pages/Dto/TaskShowRoute.cs | 15 - .../Components/Pages/EvaluationProject.razor | 88 - .../Pages/EvaluationProject.razor.cs | 154 - .../Pages/EvaluationProject.razor.css | 9 - VideoAnalysis/Components/Pages/Login.razor | 54 - VideoAnalysis/Components/Pages/Login.razor.cs | 72 - .../Components/Pages/Login.razor.css | 242 - .../Pages/NodeSubscriptionPage.razor | 56 - .../Pages/NodeSubscriptionPage.razor.cs | 131 - .../Pages/NodeSubscriptionPage.razor.css | 9 - .../Components/Pages/VideoTaskPage.razor | 103 - .../Components/Pages/VideoTaskPage.razor.cs | 187 - .../Components/Pages/VideoTaskPage.razor.css | 8 - .../Components/Pages/VideoTaskShow.razor | 135 - .../Components/Pages/VideoTaskShow.razor.cs | 129 - .../Components/Pages/VideoTaskShow.razor.css | 118 - VideoAnalysis/Components/Resources/I18n.cs | 6 - VideoAnalysis/Components/Resources/I18n.resx | 129 - .../Components/Resources/I18n.zh-CN.resx | 129 - VideoAnalysis/Components/Routes.razor | 18 - VideoAnalysis/Components/_Imports.razor | 15 - VideoAnalysis/Expand/AuthorizeExpand.cs | 70 + VideoAnalysis/Expand/SearchExpand.cs | 48 - VideoAnalysis/GlobalUsings.cs | 3 +- VideoAnalysis/Learn.VideoAnalysis.csproj | 25 +- VideoAnalysis/Program.cs | 6 +- VideoAnalysis/WebUI/.browserslistrc | 4 + VideoAnalysis/WebUI/.dockerignore | 21 + VideoAnalysis/WebUI/.editorconfig | 14 + VideoAnalysis/WebUI/.env | 5 + VideoAnalysis/WebUI/.env.development | 17 + VideoAnalysis/WebUI/.env.production | 19 + VideoAnalysis/WebUI/.env.staging | 22 + VideoAnalysis/WebUI/.lintstagedrc | 20 + VideoAnalysis/WebUI/.markdownlint.json | 11 + VideoAnalysis/WebUI/.npmrc | 4 + VideoAnalysis/WebUI/.nvmrc | 1 + VideoAnalysis/WebUI/.prettierrc.js | 9 + VideoAnalysis/WebUI/.stylelintignore | 4 + VideoAnalysis/WebUI/Dockerfile | 20 + VideoAnalysis/WebUI/LICENSE | 21 + VideoAnalysis/WebUI/README.en-US.md | 47 + VideoAnalysis/WebUI/README.md | 51 + VideoAnalysis/WebUI/build/cdn.ts | 55 + VideoAnalysis/WebUI/build/compress.ts | 63 + VideoAnalysis/WebUI/build/info.ts | 57 + VideoAnalysis/WebUI/build/optimize.ts | 29 + VideoAnalysis/WebUI/build/plugins.ts | 66 + VideoAnalysis/WebUI/build/utils.ts | 110 + VideoAnalysis/WebUI/commitlint.config.js | 35 + VideoAnalysis/WebUI/eslint.config.js | 173 + VideoAnalysis/WebUI/index.html | 88 + VideoAnalysis/WebUI/package.json | 159 + VideoAnalysis/WebUI/pnpm-lock.yaml | 7329 +++++++++++++++++ VideoAnalysis/WebUI/postcss.config.js | 8 + VideoAnalysis/WebUI/public/favicon.ico | Bin 0 -> 1270 bytes VideoAnalysis/WebUI/public/logo.svg | 1 + .../WebUI/public/platform-config.json | 26 + VideoAnalysis/WebUI/src/App.vue | 26 + VideoAnalysis/WebUI/src/api/enum.ts | 20 + VideoAnalysis/WebUI/src/api/hTable.ts | 31 + VideoAnalysis/WebUI/src/api/routes.ts | 15 + VideoAnalysis/WebUI/src/api/user.ts | 43 + VideoAnalysis/WebUI/src/api/videoTask.ts | 58 + .../WebUI/src/assets/iconfont/iconfont.css | 27 + .../WebUI/src/assets/iconfont/iconfont.js | 68 + .../WebUI/src/assets/iconfont/iconfont.json | 30 + .../WebUI/src/assets/iconfont/iconfont.ttf | Bin 0 -> 3904 bytes .../WebUI/src/assets/iconfont/iconfont.woff | Bin 0 -> 2484 bytes .../WebUI/src/assets/iconfont/iconfont.woff2 | Bin 0 -> 2016 bytes .../WebUI/src/assets/login/avatar.svg | 1 + VideoAnalysis/WebUI/src/assets/login/bg.png | Bin 0 -> 17468 bytes .../WebUI/src/assets/login/illustration.svg | 1 + VideoAnalysis/WebUI/src/assets/status/403.svg | 1 + VideoAnalysis/WebUI/src/assets/status/404.svg | 1 + VideoAnalysis/WebUI/src/assets/status/500.svg | 1 + .../WebUI/src/assets/svg/back_top.svg | 1 + VideoAnalysis/WebUI/src/assets/svg/dark.svg | 1 + VideoAnalysis/WebUI/src/assets/svg/day.svg | 1 + .../WebUI/src/assets/svg/enter_outlined.svg | 1 + .../WebUI/src/assets/svg/exit_screen.svg | 1 + .../WebUI/src/assets/svg/full_screen.svg | 1 + .../WebUI/src/assets/svg/keyboard_esc.svg | 1 + VideoAnalysis/WebUI/src/assets/svg/system.svg | 1 + .../WebUI/src/assets/table-bar/collapse.svg | 1 + .../WebUI/src/assets/table-bar/drag.svg | 1 + .../WebUI/src/assets/table-bar/expand.svg | 1 + .../WebUI/src/assets/table-bar/refresh.svg | 1 + .../WebUI/src/assets/table-bar/settings.svg | 1 + VideoAnalysis/WebUI/src/assets/user.jpg | Bin 0 -> 3694 bytes .../WebUI/src/components/ReAuth/index.ts | 5 + .../WebUI/src/components/ReAuth/src/auth.tsx | 20 + .../WebUI/src/components/ReCol/index.ts | 29 + .../WebUI/src/components/ReDialog/index.ts | 69 + .../WebUI/src/components/ReDialog/index.vue | 206 + .../WebUI/src/components/ReDialog/type.ts | 275 + .../WebUI/src/components/ReIcon/index.ts | 12 + .../WebUI/src/components/ReIcon/src/hooks.ts | 63 + .../src/components/ReIcon/src/iconfont.ts | 47 + .../ReIcon/src/iconifyIconOffline.ts | 47 + .../ReIcon/src/iconifyIconOnline.ts | 31 + .../src/components/ReIcon/src/offlineIcon.ts | 23 + .../WebUI/src/components/ReIcon/src/types.ts | 20 + .../WebUI/src/components/RePerms/index.ts | 5 + .../src/components/RePerms/src/perms.tsx | 20 + .../src/components/RePureTableBar/index.ts | 5 + .../src/components/RePureTableBar/src/bar.tsx | 393 + .../WebUI/src/components/ReSegmented/index.ts | 8 + .../src/components/ReSegmented/src/index.css | 156 + .../src/components/ReSegmented/src/index.tsx | 216 + .../src/components/ReSegmented/src/type.ts | 20 + .../WebUI/src/components/ReText/index.ts | 7 + .../WebUI/src/components/ReText/src/index.vue | 69 + .../WebUI/src/components/hTable/hTable.ts | 454 + .../src/components/hTable/hTableEdit.vue | 230 + .../WebUI/src/components/hTable/index.vue | 634 ++ VideoAnalysis/WebUI/src/config/index.ts | 55 + .../WebUI/src/directives/auth/index.ts | 15 + .../WebUI/src/directives/copy/index.ts | 33 + VideoAnalysis/WebUI/src/directives/index.ts | 6 + .../WebUI/src/directives/longpress/index.ts | 63 + .../WebUI/src/directives/optimize/index.ts | 68 + .../WebUI/src/directives/perms/index.ts | 15 + .../WebUI/src/directives/ripple/index.scss | 48 + .../WebUI/src/directives/ripple/index.ts | 229 + .../layout/components/lay-content/index.vue | 213 + .../layout/components/lay-footer/index.vue | 31 + .../src/layout/components/lay-frame/index.vue | 79 + .../layout/components/lay-navbar/index.vue | 128 + .../lay-notice/components/NoticeItem.vue | 177 + .../lay-notice/components/NoticeList.vue | 23 + .../src/layout/components/lay-notice/data.ts | 97 + .../layout/components/lay-notice/index.vue | 91 + .../src/layout/components/lay-panel/index.vue | 145 + .../lay-search/components/SearchFooter.vue | 61 + .../lay-search/components/SearchHistory.vue | 198 + .../components/SearchHistoryItem.vue | 52 + .../lay-search/components/SearchModal.vue | 334 + .../lay-search/components/SearchResult.vue | 113 + .../layout/components/lay-search/index.vue | 21 + .../src/layout/components/lay-search/types.ts | 20 + .../layout/components/lay-setting/index.vue | 631 ++ .../components/lay-sidebar/NavHorizontal.vue | 123 + .../layout/components/lay-sidebar/NavMix.vue | 143 + .../components/lay-sidebar/NavVertical.vue | 137 + .../components/SidebarBreadCrumb.vue | 120 + .../components/SidebarCenterCollapse.vue | 70 + .../components/SidebarExtraIcon.vue | 20 + .../components/SidebarFullScreen.vue | 30 + .../lay-sidebar/components/SidebarItem.vue | 231 + .../components/SidebarLeftCollapse.vue | 69 + .../components/SidebarLinkItem.vue | 32 + .../lay-sidebar/components/SidebarLogo.vue | 72 + .../components/SidebarTopCollapse.vue | 33 + .../lay-tag/components/TagChrome.vue | 33 + .../src/layout/components/lay-tag/index.scss | 371 + .../src/layout/components/lay-tag/index.vue | 684 ++ VideoAnalysis/WebUI/src/layout/frame.vue | 91 + .../WebUI/src/layout/hooks/useBoolean.ts | 26 + .../src/layout/hooks/useDataThemeChange.ts | 138 + .../WebUI/src/layout/hooks/useLayout.ts | 58 + .../WebUI/src/layout/hooks/useMultiFrame.ts | 25 + .../WebUI/src/layout/hooks/useNav.ts | 157 + .../WebUI/src/layout/hooks/useTag.ts | 245 + VideoAnalysis/WebUI/src/layout/index.vue | 235 + VideoAnalysis/WebUI/src/layout/redirect.vue | 24 + VideoAnalysis/WebUI/src/layout/types.ts | 92 + VideoAnalysis/WebUI/src/main.ts | 64 + VideoAnalysis/WebUI/src/plugins/echarts.ts | 44 + .../WebUI/src/plugins/elementPlus.ts | 248 + VideoAnalysis/WebUI/src/router/index.ts | 215 + .../WebUI/src/router/modules/error.ts | 37 + .../WebUI/src/router/modules/home.ts | 34 + .../WebUI/src/router/modules/remaining.ts | 30 + VideoAnalysis/WebUI/src/router/utils.ts | 409 + VideoAnalysis/WebUI/src/store/index.ts | 9 + VideoAnalysis/WebUI/src/store/modules/app.ts | 85 + .../WebUI/src/store/modules/epTheme.ts | 49 + .../WebUI/src/store/modules/multiTags.ts | 145 + .../WebUI/src/store/modules/permission.ts | 74 + .../WebUI/src/store/modules/settings.ts | 35 + VideoAnalysis/WebUI/src/store/modules/user.ts | 110 + VideoAnalysis/WebUI/src/store/types.ts | 47 + VideoAnalysis/WebUI/src/store/utils.ts | 28 + VideoAnalysis/WebUI/src/style/dark.scss | 182 + .../WebUI/src/style/element-plus.scss | 189 + VideoAnalysis/WebUI/src/style/index.scss | 37 + VideoAnalysis/WebUI/src/style/login.css | 96 + VideoAnalysis/WebUI/src/style/reset.scss | 250 + VideoAnalysis/WebUI/src/style/sidebar.scss | 719 ++ VideoAnalysis/WebUI/src/style/tailwind.css | 46 + VideoAnalysis/WebUI/src/style/theme.scss | 95 + VideoAnalysis/WebUI/src/style/transition.scss | 54 + VideoAnalysis/WebUI/src/utils/auth.ts | 140 + .../WebUI/src/utils/globalPolyfills.ts | 7 + VideoAnalysis/WebUI/src/utils/http/index.ts | 271 + VideoAnalysis/WebUI/src/utils/http/types.d.ts | 60 + .../WebUI/src/utils/localforage/index.ts | 109 + .../WebUI/src/utils/localforage/types.d.ts | 166 + VideoAnalysis/WebUI/src/utils/message.ts | 89 + VideoAnalysis/WebUI/src/utils/mitt.ts | 14 + .../WebUI/src/utils/preventDefault.ts | 28 + VideoAnalysis/WebUI/src/utils/print.ts | 223 + .../WebUI/src/utils/progress/index.ts | 17 + VideoAnalysis/WebUI/src/utils/propTypes.ts | 39 + VideoAnalysis/WebUI/src/utils/responsive.ts | 42 + VideoAnalysis/WebUI/src/utils/rules.ts | 75 + VideoAnalysis/WebUI/src/utils/sso.ts | 59 + VideoAnalysis/WebUI/src/utils/tree.ts | 188 + VideoAnalysis/WebUI/src/views/error/403.vue | 70 + VideoAnalysis/WebUI/src/views/error/404.vue | 70 + VideoAnalysis/WebUI/src/views/error/500.vue | 70 + .../WebUI/src/views/welcome/index.vue | 223 + .../WebUI/src/views/welcome/showTask.vue | 223 + VideoAnalysis/WebUI/stylelint.config.js | 87 + VideoAnalysis/WebUI/tsconfig.json | 55 + VideoAnalysis/WebUI/types/directives.d.ts | 28 + .../WebUI/types/global-components.d.ts | 135 + VideoAnalysis/WebUI/types/global.d.ts | 193 + VideoAnalysis/WebUI/types/index.d.ts | 80 + VideoAnalysis/WebUI/types/router.d.ts | 109 + VideoAnalysis/WebUI/types/shims-tsx.d.ts | 24 + VideoAnalysis/WebUI/types/shims-vue.d.ts | 11 + VideoAnalysis/WebUI/vite.config.ts | 62 + VideoAnalysis/appsettings.json | 8 +- VideoAnalysisCore/Common/AppCommon.cs | 9 +- VideoAnalysisCore/Common/AppConfig.cs | 23 + .../Common/AuthenticationSchemes.cs | 13 + VideoAnalysisCore/Common/JwtHelper.cs | 66 + VideoAnalysisCore/Common/QueryRequestBase.cs | 103 + .../Controllers/ApiController.cs | 297 +- VideoAnalysisCore/Controllers/Dto/ApiDto.cs | 8 + .../Controllers/PublicController.cs | 107 + .../Controllers/VideoTaskController.cs | 358 + .../Controllers/_BaseController.cs | 146 + VideoAnalysisCore/Model/VideoTask.cs | 3 +- 241 files changed, 25322 insertions(+), 2374 deletions(-) delete mode 100644 VideoAnalysis/Components/App.razor delete mode 100644 VideoAnalysis/Components/Error.razor delete mode 100644 VideoAnalysis/Components/Layouts/BasicLayout.razor delete mode 100644 VideoAnalysis/Components/Layouts/BasicLayout.razor.cs delete mode 100644 VideoAnalysis/Components/Pages/Dto/TaskShowRoute.cs delete mode 100644 VideoAnalysis/Components/Pages/EvaluationProject.razor delete mode 100644 VideoAnalysis/Components/Pages/EvaluationProject.razor.cs delete mode 100644 VideoAnalysis/Components/Pages/EvaluationProject.razor.css delete mode 100644 VideoAnalysis/Components/Pages/Login.razor delete mode 100644 VideoAnalysis/Components/Pages/Login.razor.cs delete mode 100644 VideoAnalysis/Components/Pages/Login.razor.css delete mode 100644 VideoAnalysis/Components/Pages/NodeSubscriptionPage.razor delete mode 100644 VideoAnalysis/Components/Pages/NodeSubscriptionPage.razor.cs delete mode 100644 VideoAnalysis/Components/Pages/NodeSubscriptionPage.razor.css delete mode 100644 VideoAnalysis/Components/Pages/VideoTaskPage.razor delete mode 100644 VideoAnalysis/Components/Pages/VideoTaskPage.razor.cs delete mode 100644 VideoAnalysis/Components/Pages/VideoTaskPage.razor.css delete mode 100644 VideoAnalysis/Components/Pages/VideoTaskShow.razor delete mode 100644 VideoAnalysis/Components/Pages/VideoTaskShow.razor.cs delete mode 100644 VideoAnalysis/Components/Pages/VideoTaskShow.razor.css delete mode 100644 VideoAnalysis/Components/Resources/I18n.cs delete mode 100644 VideoAnalysis/Components/Resources/I18n.resx delete mode 100644 VideoAnalysis/Components/Resources/I18n.zh-CN.resx delete mode 100644 VideoAnalysis/Components/Routes.razor delete mode 100644 VideoAnalysis/Components/_Imports.razor create mode 100644 VideoAnalysis/Expand/AuthorizeExpand.cs delete mode 100644 VideoAnalysis/Expand/SearchExpand.cs create mode 100644 VideoAnalysis/WebUI/.browserslistrc create mode 100644 VideoAnalysis/WebUI/.dockerignore create mode 100644 VideoAnalysis/WebUI/.editorconfig create mode 100644 VideoAnalysis/WebUI/.env create mode 100644 VideoAnalysis/WebUI/.env.development create mode 100644 VideoAnalysis/WebUI/.env.production create mode 100644 VideoAnalysis/WebUI/.env.staging create mode 100644 VideoAnalysis/WebUI/.lintstagedrc create mode 100644 VideoAnalysis/WebUI/.markdownlint.json create mode 100644 VideoAnalysis/WebUI/.npmrc create mode 100644 VideoAnalysis/WebUI/.nvmrc create mode 100644 VideoAnalysis/WebUI/.prettierrc.js create mode 100644 VideoAnalysis/WebUI/.stylelintignore create mode 100644 VideoAnalysis/WebUI/Dockerfile create mode 100644 VideoAnalysis/WebUI/LICENSE create mode 100644 VideoAnalysis/WebUI/README.en-US.md create mode 100644 VideoAnalysis/WebUI/README.md create mode 100644 VideoAnalysis/WebUI/build/cdn.ts create mode 100644 VideoAnalysis/WebUI/build/compress.ts create mode 100644 VideoAnalysis/WebUI/build/info.ts create mode 100644 VideoAnalysis/WebUI/build/optimize.ts create mode 100644 VideoAnalysis/WebUI/build/plugins.ts create mode 100644 VideoAnalysis/WebUI/build/utils.ts create mode 100644 VideoAnalysis/WebUI/commitlint.config.js create mode 100644 VideoAnalysis/WebUI/eslint.config.js create mode 100644 VideoAnalysis/WebUI/index.html create mode 100644 VideoAnalysis/WebUI/package.json create mode 100644 VideoAnalysis/WebUI/pnpm-lock.yaml create mode 100644 VideoAnalysis/WebUI/postcss.config.js create mode 100644 VideoAnalysis/WebUI/public/favicon.ico create mode 100644 VideoAnalysis/WebUI/public/logo.svg create mode 100644 VideoAnalysis/WebUI/public/platform-config.json create mode 100644 VideoAnalysis/WebUI/src/App.vue create mode 100644 VideoAnalysis/WebUI/src/api/enum.ts create mode 100644 VideoAnalysis/WebUI/src/api/hTable.ts create mode 100644 VideoAnalysis/WebUI/src/api/routes.ts create mode 100644 VideoAnalysis/WebUI/src/api/user.ts create mode 100644 VideoAnalysis/WebUI/src/api/videoTask.ts create mode 100644 VideoAnalysis/WebUI/src/assets/iconfont/iconfont.css create mode 100644 VideoAnalysis/WebUI/src/assets/iconfont/iconfont.js create mode 100644 VideoAnalysis/WebUI/src/assets/iconfont/iconfont.json create mode 100644 VideoAnalysis/WebUI/src/assets/iconfont/iconfont.ttf create mode 100644 VideoAnalysis/WebUI/src/assets/iconfont/iconfont.woff create mode 100644 VideoAnalysis/WebUI/src/assets/iconfont/iconfont.woff2 create mode 100644 VideoAnalysis/WebUI/src/assets/login/avatar.svg create mode 100644 VideoAnalysis/WebUI/src/assets/login/bg.png create mode 100644 VideoAnalysis/WebUI/src/assets/login/illustration.svg create mode 100644 VideoAnalysis/WebUI/src/assets/status/403.svg create mode 100644 VideoAnalysis/WebUI/src/assets/status/404.svg create mode 100644 VideoAnalysis/WebUI/src/assets/status/500.svg create mode 100644 VideoAnalysis/WebUI/src/assets/svg/back_top.svg create mode 100644 VideoAnalysis/WebUI/src/assets/svg/dark.svg create mode 100644 VideoAnalysis/WebUI/src/assets/svg/day.svg create mode 100644 VideoAnalysis/WebUI/src/assets/svg/enter_outlined.svg create mode 100644 VideoAnalysis/WebUI/src/assets/svg/exit_screen.svg create mode 100644 VideoAnalysis/WebUI/src/assets/svg/full_screen.svg create mode 100644 VideoAnalysis/WebUI/src/assets/svg/keyboard_esc.svg create mode 100644 VideoAnalysis/WebUI/src/assets/svg/system.svg create mode 100644 VideoAnalysis/WebUI/src/assets/table-bar/collapse.svg create mode 100644 VideoAnalysis/WebUI/src/assets/table-bar/drag.svg create mode 100644 VideoAnalysis/WebUI/src/assets/table-bar/expand.svg create mode 100644 VideoAnalysis/WebUI/src/assets/table-bar/refresh.svg create mode 100644 VideoAnalysis/WebUI/src/assets/table-bar/settings.svg create mode 100644 VideoAnalysis/WebUI/src/assets/user.jpg create mode 100644 VideoAnalysis/WebUI/src/components/ReAuth/index.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReAuth/src/auth.tsx create mode 100644 VideoAnalysis/WebUI/src/components/ReCol/index.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReDialog/index.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReDialog/index.vue create mode 100644 VideoAnalysis/WebUI/src/components/ReDialog/type.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReIcon/index.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReIcon/src/hooks.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReIcon/src/iconfont.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReIcon/src/iconifyIconOffline.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReIcon/src/iconifyIconOnline.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReIcon/src/offlineIcon.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReIcon/src/types.ts create mode 100644 VideoAnalysis/WebUI/src/components/RePerms/index.ts create mode 100644 VideoAnalysis/WebUI/src/components/RePerms/src/perms.tsx create mode 100644 VideoAnalysis/WebUI/src/components/RePureTableBar/index.ts create mode 100644 VideoAnalysis/WebUI/src/components/RePureTableBar/src/bar.tsx create mode 100644 VideoAnalysis/WebUI/src/components/ReSegmented/index.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReSegmented/src/index.css create mode 100644 VideoAnalysis/WebUI/src/components/ReSegmented/src/index.tsx create mode 100644 VideoAnalysis/WebUI/src/components/ReSegmented/src/type.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReText/index.ts create mode 100644 VideoAnalysis/WebUI/src/components/ReText/src/index.vue create mode 100644 VideoAnalysis/WebUI/src/components/hTable/hTable.ts create mode 100644 VideoAnalysis/WebUI/src/components/hTable/hTableEdit.vue create mode 100644 VideoAnalysis/WebUI/src/components/hTable/index.vue create mode 100644 VideoAnalysis/WebUI/src/config/index.ts create mode 100644 VideoAnalysis/WebUI/src/directives/auth/index.ts create mode 100644 VideoAnalysis/WebUI/src/directives/copy/index.ts create mode 100644 VideoAnalysis/WebUI/src/directives/index.ts create mode 100644 VideoAnalysis/WebUI/src/directives/longpress/index.ts create mode 100644 VideoAnalysis/WebUI/src/directives/optimize/index.ts create mode 100644 VideoAnalysis/WebUI/src/directives/perms/index.ts create mode 100644 VideoAnalysis/WebUI/src/directives/ripple/index.scss create mode 100644 VideoAnalysis/WebUI/src/directives/ripple/index.ts create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-content/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-footer/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-frame/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-navbar/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-notice/components/NoticeItem.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-notice/components/NoticeList.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-notice/data.ts create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-notice/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-panel/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchFooter.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchHistory.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchHistoryItem.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchModal.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchResult.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-search/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-search/types.ts create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-setting/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavHorizontal.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavMix.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavVertical.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarItem.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLogo.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-tag/components/TagChrome.vue create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-tag/index.scss create mode 100644 VideoAnalysis/WebUI/src/layout/components/lay-tag/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/frame.vue create mode 100644 VideoAnalysis/WebUI/src/layout/hooks/useBoolean.ts create mode 100644 VideoAnalysis/WebUI/src/layout/hooks/useDataThemeChange.ts create mode 100644 VideoAnalysis/WebUI/src/layout/hooks/useLayout.ts create mode 100644 VideoAnalysis/WebUI/src/layout/hooks/useMultiFrame.ts create mode 100644 VideoAnalysis/WebUI/src/layout/hooks/useNav.ts create mode 100644 VideoAnalysis/WebUI/src/layout/hooks/useTag.ts create mode 100644 VideoAnalysis/WebUI/src/layout/index.vue create mode 100644 VideoAnalysis/WebUI/src/layout/redirect.vue create mode 100644 VideoAnalysis/WebUI/src/layout/types.ts create mode 100644 VideoAnalysis/WebUI/src/main.ts create mode 100644 VideoAnalysis/WebUI/src/plugins/echarts.ts create mode 100644 VideoAnalysis/WebUI/src/plugins/elementPlus.ts create mode 100644 VideoAnalysis/WebUI/src/router/index.ts create mode 100644 VideoAnalysis/WebUI/src/router/modules/error.ts create mode 100644 VideoAnalysis/WebUI/src/router/modules/home.ts create mode 100644 VideoAnalysis/WebUI/src/router/modules/remaining.ts create mode 100644 VideoAnalysis/WebUI/src/router/utils.ts create mode 100644 VideoAnalysis/WebUI/src/store/index.ts create mode 100644 VideoAnalysis/WebUI/src/store/modules/app.ts create mode 100644 VideoAnalysis/WebUI/src/store/modules/epTheme.ts create mode 100644 VideoAnalysis/WebUI/src/store/modules/multiTags.ts create mode 100644 VideoAnalysis/WebUI/src/store/modules/permission.ts create mode 100644 VideoAnalysis/WebUI/src/store/modules/settings.ts create mode 100644 VideoAnalysis/WebUI/src/store/modules/user.ts create mode 100644 VideoAnalysis/WebUI/src/store/types.ts create mode 100644 VideoAnalysis/WebUI/src/store/utils.ts create mode 100644 VideoAnalysis/WebUI/src/style/dark.scss create mode 100644 VideoAnalysis/WebUI/src/style/element-plus.scss create mode 100644 VideoAnalysis/WebUI/src/style/index.scss create mode 100644 VideoAnalysis/WebUI/src/style/login.css create mode 100644 VideoAnalysis/WebUI/src/style/reset.scss create mode 100644 VideoAnalysis/WebUI/src/style/sidebar.scss create mode 100644 VideoAnalysis/WebUI/src/style/tailwind.css create mode 100644 VideoAnalysis/WebUI/src/style/theme.scss create mode 100644 VideoAnalysis/WebUI/src/style/transition.scss create mode 100644 VideoAnalysis/WebUI/src/utils/auth.ts create mode 100644 VideoAnalysis/WebUI/src/utils/globalPolyfills.ts create mode 100644 VideoAnalysis/WebUI/src/utils/http/index.ts create mode 100644 VideoAnalysis/WebUI/src/utils/http/types.d.ts create mode 100644 VideoAnalysis/WebUI/src/utils/localforage/index.ts create mode 100644 VideoAnalysis/WebUI/src/utils/localforage/types.d.ts create mode 100644 VideoAnalysis/WebUI/src/utils/message.ts create mode 100644 VideoAnalysis/WebUI/src/utils/mitt.ts create mode 100644 VideoAnalysis/WebUI/src/utils/preventDefault.ts create mode 100644 VideoAnalysis/WebUI/src/utils/print.ts create mode 100644 VideoAnalysis/WebUI/src/utils/progress/index.ts create mode 100644 VideoAnalysis/WebUI/src/utils/propTypes.ts create mode 100644 VideoAnalysis/WebUI/src/utils/responsive.ts create mode 100644 VideoAnalysis/WebUI/src/utils/rules.ts create mode 100644 VideoAnalysis/WebUI/src/utils/sso.ts create mode 100644 VideoAnalysis/WebUI/src/utils/tree.ts create mode 100644 VideoAnalysis/WebUI/src/views/error/403.vue create mode 100644 VideoAnalysis/WebUI/src/views/error/404.vue create mode 100644 VideoAnalysis/WebUI/src/views/error/500.vue create mode 100644 VideoAnalysis/WebUI/src/views/welcome/index.vue create mode 100644 VideoAnalysis/WebUI/src/views/welcome/showTask.vue create mode 100644 VideoAnalysis/WebUI/stylelint.config.js create mode 100644 VideoAnalysis/WebUI/tsconfig.json create mode 100644 VideoAnalysis/WebUI/types/directives.d.ts create mode 100644 VideoAnalysis/WebUI/types/global-components.d.ts create mode 100644 VideoAnalysis/WebUI/types/global.d.ts create mode 100644 VideoAnalysis/WebUI/types/index.d.ts create mode 100644 VideoAnalysis/WebUI/types/router.d.ts create mode 100644 VideoAnalysis/WebUI/types/shims-tsx.d.ts create mode 100644 VideoAnalysis/WebUI/types/shims-vue.d.ts create mode 100644 VideoAnalysis/WebUI/vite.config.ts create mode 100644 VideoAnalysisCore/Common/AuthenticationSchemes.cs create mode 100644 VideoAnalysisCore/Common/JwtHelper.cs create mode 100644 VideoAnalysisCore/Common/QueryRequestBase.cs create mode 100644 VideoAnalysisCore/Controllers/PublicController.cs create mode 100644 VideoAnalysisCore/Controllers/VideoTaskController.cs create mode 100644 VideoAnalysisCore/Controllers/_BaseController.cs diff --git a/.gitignore b/.gitignore index 703446e..6bd25cf 100644 --- a/.gitignore +++ b/.gitignore @@ -367,3 +367,4 @@ VideoAnalysis/AICore/_Static/ VideoAnalysisCore/AICore/_Static/ VideoAnalysis/WebUI/node_modules/ VideoAnalysis/WebUI/dist/ +VideoAnalysis/WebUI/.vscode/ diff --git a/VideoAnalysis/Components/App.razor b/VideoAnalysis/Components/App.razor deleted file mode 100644 index 78db69e..0000000 --- a/VideoAnalysis/Components/App.razor +++ /dev/null @@ -1,25 +0,0 @@ - - - -
- - -
- Request ID: @RequestId
-
- 切换到Development环境将显示有关发生的错误的更多详细信息。 -
-- 不应为已部署的应用程序启用开发环境。 - 它可能导致向最终用户显示来自异常的敏感信息。 - 对于本地调试,通过将 ASPNETCORE_ENVIRONMENT 环境变量设置为 Development 来启用 开发 环境 - 并重新启动应用程序。 -
- -@code{ - [CascadingParameter] - private HttpContext? HttpContext { get; set; } - - private string? RequestId { get; set; } - private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - protected override void OnInitialized() => - RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; -} diff --git a/VideoAnalysis/Components/Layouts/BasicLayout.razor b/VideoAnalysis/Components/Layouts/BasicLayout.razor deleted file mode 100644 index b8d5774..0000000 --- a/VideoAnalysis/Components/Layouts/BasicLayout.razor +++ /dev/null @@ -1,36 +0,0 @@ -@namespace VideoAnalysisRazor.Layouts -@using static AntDesign.IconType -@inherits LayoutComponentBase - -将从哪个步骤重试?
- - -E>{Tx+r&IV+)YybXt5haUXyrZML=! zh7aObrHp{F5>0CHq@BMx98P3HftXS+y^t;d_0&%)UyB&tri^wLu~EQ zUAp3!RI$- bblgkb(zQ=^4F4vIEWoxp$-+3f; zBF;AsD#fz>%yj*2lqEW?n~J*5xI;onG7{m(h$LG+%&R5oyS``Lx_ye0-Z98~*P@a( zT%S8XCnz_|MgC}cC^Nad6C2gBdu8W_okFEV$^BfLo+pV8k|J}VNa@9yy2*-q&-G;! z8d`{x=&z;{@U2}_CeF^FWvy8S4`2Dit7NmJ-B=O6qTy}GZvB})AR-czwp!n;I*}rd zPKKTBNcQ+MW#IPTAGI#b1bS`El=Xc)#lJq>xFK-riqMfeiU~qM_xAUXMBU{NW3bq3 ztbo#Zj)K8@|LztY*@CPLEu+&6nimZ<#?7RSS9kuS*6k%2;9Q`VUvvAXFqW+8ck!!j z_AfrwMtR+O-(v@FAEmghA1(>}E>+_5 wKBG=@j^ V@mfEAV zVErbYp;mKDh1!|TihGA#3x5DetM?^DO~0+eD{DLR+Zn5G=U!YH^1sYK;8+)3`BS8J zV8%=N$xx-ZS &5WPdG|(OsOTD%%u2E}KZF5acb8SR)NKw{}dgRsKh;l2j zYN_AYQ`r*7kADj5C%(#_+BhC!k&*^Ozs;C^>{+PSzZ#H#2D^>!l#2H$P-kk~RbbCZ zR=hk+7UR H zb>y{Dj+-g|%aL`5F)8B2oZYr_*7r9zLE&`)1sLNHX3`%2%Y4PJ#7q~phfb`MDvS@x zwmlGs-DB5?NT|J*vM-HE`K3+h?&p(%jIq7Mg7W9>@3mh@iuel*E4so?oy zOovul8=p#BD#N81@SCk=_XV2XV@icyEGqU#F+%uv$^2}%Wj52KTW6tIU_^XgXSH4M zaiXN3jl8n(o@>h2@6H2s9H+1(%)!ToZtJGAAC`xhd%Q3Gr^#T|ZcGMQHFwz8yYOi8 zecEY?#Ps>E)5CpLN!KzsA*xtGC~B6^?ooYFA$K;aJpyjn{g!|}Bb6{a z%?V{jn{_*0E%ZhE)h@a35H|EzP?+U01ECA~+~>qETVD1X!mW#*@)F3q+%JYhK*O 9uT-@I-xsN{ R=idgZ?k_l5P0&-`WSeYMGPMEiBi z*_#pTH66USlFC)F)r{qE#KkRj&FVVZDwOQmLpc?yQtp5ovj|cve&FOdg(_^{c`B}- z!@@|ro00cLIX=MaP&p^jO5@nkH pMkxR+;yLh1zv& z<24(*;=U*z@|X1culIny1>oi*o%*TKxla(2st6Q$zc*11z8hmK#1q29=y}QE73Tw# zY=mU~tv~P}(e^d~3O;X$1RDUX1fhYTk6|$wj9D81umLg98Nq-+A?du4z`uPMtT -MZBN;hzs- zwewc>BW@bjLuW6iG7f)$FA_tNtiwmmc`Td9$AY*m*(bVy&+LSRuT7uS$J!NY@2NcF z6p=G_)7W;=N2&@ES>y-1 S0n1zEhYL(@_y0h81bp5v`i$&u?>CLbsWGcYk&V zwg(?6-51o?{8AO4OAMdkES0_s(VZxDAr8JN{!C5X!X36-Ej1d@LuWhRxZUR r)1$lk?ccClsDD$6ydJL(rSy5oJs)ffH@KXthF*5KK~W*~ 3X9qhQpA1oy4pN5fivqhr-VkY97!yvS zIIyecWI*&8`V)Z yPj>fwXIQ2sN}~}XjY>%jkLTNaNf8YB{ZaQ1-4|I*Kyv3~b1<=gpf5dDC!l>m zF3iW@eEYfj|1b|t56Ogr$9yvTCX>U&;vS*UOyWX9R6~SNdF%8}EI^*-9zQLXMVN%= zl1X3VDh_;!K%;g0+WY6X53q47QUhDM#LU3y8@IU?Fx(nSLPDsQYta*ncvnMuNS}B! zem?%&42+~E6Sq1zKhyAa=35pjf&AsZya}{(LJcyOTjq+( )KvfE??(DNCiz9FPwJg`#HygM1P~NSgOE@(Q71{+rGmgG9U%5y zIwl^!9CtoR)MduGRXipg5su1Z_uDrM{H6 hZNfoKmsJ|*y*V+^mER;BJs&ls zM6%Nk+E=3qHwi_JAV!5To@`)
nb{ zsA+^mD=C|3T7;Z+F`1xgi6nKJPzY9HSdC$1Hk~4!SVm=8ZCwO l1OEeF{+E)-*){1hrT95R!!&qv>0wyEq9ZGIdIX#3`YoyyZM8 z)>U=RLljdj(at>2v`5vE@3E{_I1C+3Iw5>-c9xXQs+;7Oi18?4KTI#^QT4Uft{!hQ zgyLJoJCX0=w~E#owzu)U*!ha_#5Tf+yoybV77ooUoVjHd^&D>4#%D_(X9Y6@*`KE4 z+VFGd3oo48!bg022*Hg=1OGhnkazI5ZSUFa){P%$eUkn`%7Z`nXsApW#1h;`Y+&C- zM-m*gDNU?HW6-53ONe*zC3tl@4tl?1hnt(7P21Uk l_=?DG#m9b_GSNLpP~l=`)MIP6N9DS *(^#>U_Ss2F=#MHiXvnDFaw$N;8#uQx;|TKh(mm zxca!ds+8K4E>l|J3h?@tf&YC{%hwNR>NBmF`^|rH-SVkT(`gh;3G6-XNm{Fo$$tKo zjlYyWUK-^$52>D9HvjQ+fsZD(j7qHeeAJ9MGj;{ ?;I*e@#zFFYIZ6rA`+mbWXs)acuD7-~ik2LzSW7%GeEiKzd3 z#!N#1=ve&;*K!Ue`Mlfr7eQOZvz98MAkPHN2n`D$7=Ajn(rKe0{P|&W=a+Afp&$jQ zz1w!|8Tg;6FnWSxYxegI89g@f_pFs-Ze0AQNYd|h#E*+zTlrID(`sp4Fz(ZUgoUto z!Ap3^y@`){>Hl8{H*mj#j7z{P_;{D!H6HhYD~|D~Ii0)PE@a?bI)=Xkjp~k&RrUw- zDyGDHlf0l`wuW#YWfTUlcZz*TpD2a!cd09l4;$A= $@7 zpb`=30hgt|^dyWWQSjMb5;(k_J6x&i3|qIpekZSzssfTV++Gt86H`gN-A3fJG&3#D zMB2gdN)2OiS GalQW#!QGeo9UbhU=|SDqPviP!T)(9DtJq$BB%eFb^hI)^61ClyJAgedYmXJkIqsb Id!;N zUH7+LP(z`JL*k2Fr)1CsuJL?*JE|&17q}pm=2=Q^d$D#Nb;w+C4JQI3BrUUsr1z{j zg*A-t o>eE^uoNoo^vrwP|L^ yMK>0RIu*}lIU1d93(1#3g?ar_ee*7YMnW&~cDquE8s$#N*sa)dr=eE>0000mD!%Oi literal 0 HcmV?d00001 diff --git a/VideoAnalysis/WebUI/src/assets/login/avatar.svg b/VideoAnalysis/WebUI/src/assets/login/avatar.svg new file mode 100644 index 0000000..a63d2b1 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/login/avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/login/bg.png b/VideoAnalysis/WebUI/src/assets/login/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..8cdd3001fc95b63c1d334ab7d717812f4240839e GIT binary patch literal 17468 zcmd6Oc|gtC-~YLFQ3+XQTw_Z}40TgVX}M$>=B8%CEX1u8p+$>I#fKrxM16-D33bhP zgs-g`OH}G(7#V|_v4l$3ShCg~p(J|V=Y7uS-k#_8%=7oJKW6SZ=Y8JG>%E-qV}t+n zDP1f+upoqVnflShPYLNH5F#II))9IhUODXp|LYSs>GQaNsIa(%pqNlHetuMNsP)u{ zpar3yh6c?~TKZe42O*}m;WIvu``lOU9ugH{A4F;F6C c6qVXohtK!4`D~1@(l^>CbYb{M%VR @upACqMnIGo<>BPxg7_>ZX7RJRzyE`}}BqZ1; zIN3+VEO2mib8~Z0j&>M5dK6HMicN})3rZXn89P|wFflYXBqlsME<7sInsN*Zj*5@- zv;kF_4H3}>wvn+MCqT?0F(}%>(OyYyk_df$|8LTW2m^I&+^3;Neg9X+u``mQLmfU1 zjg5+r2?5WA4W^=^-F;$0gW{rMW<*6Tkzn*+7!?;4yD%!+dh}@fG1hjzK_TIhv}ZU@ z@%43|8W|fG6d4jab)u&YFtHC0pYJ|NIcEH1H&-_&SEZ|?<0PN)%85SX$4^!|I*xXp z;5f!rnmaKnBt9ZEGESO1-#B;tKhCA$5D^WMCx*s^FAJSNIVLK?8m)E@|7TrX|B)Y` zJO7_`8T*fO9Y7fe>f3g{mbxH-=uboFLc R)ZY6%4$5aldOr$ cIko!FaPBh}DZsOK3-fdUwMan0rI|uOgmV>WrSI*5cVNLB46D9kl z8d}m(_uX(#b>V^B&@b1pPGMR~7qQXm$}jym(fvgwuN$VzxSqY#gzL5UY=JB>v@4$# z<0j)O`kTuXtW(>gwxcd-eRxn0PBeOqcWcxh$pDL&Atch5tLUFM5Ud=+WrjXCTgc~3 zYgGu9w%lgBD;cuH_k&|BBnhfZgWC)m*;RyqM(^{9&pqB3W*^}Ux0DUmO1S;~Imua< zH7ifz`Elb}?|Iv}Roan$9dy^7B|VZxkmPO>MnOi5Jl+$w4$51b#OW^l;nCRU) mkf`;s)OHGt62uMJ2KNE0cJ1b3CZ-z@}stJP^I>dP$}} z=-Nd*xQ`8y=k3A-`nTqBpPjGTMZK};lEmZigZ17|94q*=KE_a8swR;}st^ S8B0l$r4$ zy|gO38z&rCC)YH{c>J7S-&17$ ~`wXIk@fwcq=Gvh4R zb|Ghbxr=BtYeU{wxqoDLXLB?ufpT4EC+>^%a&rymH7%~YP&dfdkY|&x$GwRCGC(5y z*cc5&3fK*YO!AwgC`&)^NWb9N136=-@Y^bDzO{oj$lXVy!KR+9rSS;th&J(hB96`8 zv~uoZ*qP8jVX4ukhwet3#E%NQ8f{X3o3}1EDPk6%bLVAuNLsF{<>3s(UEP_DDi7rC z$*s!f39k9vSkzi9#L7G#g^F48Fg3RIX0tS7eKWj^#yGH+7~Nti4lwP)Ia&{ss?mr) z7SOU_ujZI2)AQ Wh>vToNE>okYU(A0 U!V7L_&7#RIrwsKg&QOx~p8cF5<;@6H z3EACO76gcVTK^B6NWlY1(@n =bQ$7_9b~WK*L(Xur2QkTMfAlq+)sTQYkW4%nEKNbjlWm z(3G(%vPP=?4P(}>sNXo7gQq|BscF7J`&R8$x>jxwxzyT)7Vn-)#Yc3V)>U+cy})d~ zUhB2PTN}qodjY+-M5r1%t(*86ja5$Qzk=M3m3A_kSyKSZja))J4b(@!9ON}P@6f(` z$xEuIu~E(AMdro=s-8QFmb)IS7-YiK#UonmC7H_bF&%W lUCT^r67u9^p6 0k4}3z+|Q0GS#0O z#r>c6!z=2Fz@kf>cQ5Cj&71lb@DiJ~dbnJ-AL`OUPQR#9eJ?q(npa%yG6@*f(7YbB za9n>T5nfk6EQ+j-WNfuz0T#@uyl!6o&$b&)H7D+~Vb#IB_l2k$UO2V00%$e|JUPxw z<(Ngb%{yHqdBO1i_wbJBgM&P{7F|)lNRFyr&PBNdbP)?8Jz2|t`@W8_Mm)OpOd|eM z-CPt^y@C_l26V+3TFIdlzI5){_O5T61sf;FtPJa!<4aO(=@`+vm=9gmI psUFF7oxme&IjX|9=3=)8;6Uym)o*g$$_Od%x2Vax%F$=9 zX7il2`;N4pn$5QeWOomtG~vDAFSkjf|01nU(wZmOn2X+3YqH)C<2p_oOxl)|6dmH; zC0izw71ai{!sWWMQLri))G`zOs 6VP$w=wyzQH7gB;WxgNry6A2+`204KGS* 6Y?MI$$y}_tA%U#ePO7^YO#K6fZW@L*lj@yMpww_g#n0us`ilvb25 CYgr(x(5Gx2S-1c_n?sqSVNI&Ch#y_v;vR(wmUb76NC!V_9! zXA)g{+?J1~*^nl~s;N<$M7J4-tVvV3v`%WQO!P{_P>U_;c19Y~CYb1>jYC67w*@OL znhgOoPp%7!@KJY`5Kpxd){UtRG@yJ6+6hmjok>L}Sz==Qv9ddcAvalKKf}<@o ^SwJ%OIbU1 z9odm#2%lIp@%4!~33WQQ)i4%dE+*Z~HMuQaR*YRscH|mIVdfWUS&>o(_xqS!z5*I5 zc@Rfi%EtON*4&p&itZ&FJFlQ!y!n`muWtc+gJN6AjsjT148_O?Bos+!+h{Y{*p;-7 zOHuc+TsOLUJNJ6x1~MnDT} }4kgG;W(R zo~bGRXfDQ|v~WDejTFLr2v1_8_@bX>D?8x@Wm5uB>1h(ZGZOZZ93t^3NX{ pftg(z*mOdc3qC cuzVGp3WAO<;>ur}u6p9{n3@;-k1jXnWLWMt}PclO-;%_ms4IJcRg_$0S5Z(Fdo= zJ&z(_C&jh(?=4F#td~} jd2O_? BM&~zSj}`6elZtQe*ZV$G;1{`%G&34{P 3Vm+9OBLUk6)XAv zF79IzX8EM%zQnY`QI_=+Y# tPpb1`6A(Dp2IfxEFf*ES zh kp+aWSXa^RGcOq63 ?2GfU1gKjW5yL=*7)^;MvTlU5* zMKV86MZzo&O;9(5vAOhA6R>nJpX@(QmNoq56x}$Ubp9X8vi2qQ4Zg+KYvBWPQT@l1 z?(BvYTi=;6qg3&7O`e*YpdA5R3o=A$|F(A(u{?6~un|-^GAP21w6lqJ3|KERrJw|+ zt&~p8y?jYpgQ4*;Ny{}frjoQWLt_d_+hb@ GYG{lnX=fxkuo+J~BaO@l zId@YwiU;|H %P$NlNy~(lyM;g zS2ge;6tm=-hnF{Uq_Fk9)L1z8R8rIZfljKr6}5qt^i`*-7n5fBtGVW3+5P&KesERK zR$O71nOKqg^64Oxc~Z|H=y8x7avB|{tnBO %>$uc4
(c z&!TRpzAm%$UsIb<`EpOU!)-2H@r RH1T}UD`bib zPPs`A{9Z0Y-&JE*m0Ps`r8MQ$4wCls!Gy|ru&v
v-r~xFc7#dM78uRh|1OKPa)$RTXa?Z~dONYNWO4e$ykS`vuF3 z14rogex-bGVw#0=?Zwo47MrS91&+TQxp4Bbkzf39bWZ(6cn4Jus!c6$2>bp`KbJ^T zwvfy(>!Q&)8d|^5R$Fz)$nqRF=GSxZY?1RgA{okl08i!g&BSs^aH(g5kGi)sGd+V? zdX#N{Rh^bKUGm%juyM}jSFHnr3zw9y;{2AuquAWXGmo^m=&QNYi(XzSFKzvMYabye zWnuZvYp?(QlaHJM;3Aq2ez8{P#{pJs8m3(pkyw*cWp!m6hbQfBM`eCQ&RP}#iuHX( z@p%8^b#R>cBgZtBnXx1TdNaKW>V6nk(v4N!3U60o)ga$njmJG*2jy+!`nB8#W*=8R z+xEtb1+L0luF+WDK34biaci>_r{#6#3pJK=kJXh9%KPTk(`^h)T~amac iMi zXKdvbpU4G^=R7T-_T0?b$JHzWo|*ffguQfrA%XQF%pEs~EibyxGKJk*D`w~nfP7lx zPk`cDhL~{6OjPY#qd{o2?j}nALVLNxt=OeTrQxbcqkTWOD)x_@U%?> zV(~23)lppKEfYz{MayaqOwjM<%zK!LWPgMFaPGQSJDCXU*(OSHXZ^U-H4(a#4RQvS zTMt22Uzfx((|R0GWW<;IIAmXItSETEoOTYLGZG6EE{%{Ey~j*R|CU(hyA`a)+8}%a zEav9DnHAl8wN&et`I3$g%j#am`S~O`CWbStHNE8;;a!pqJ4)SvcXCaUm-Bu_%J7a% zmnvIgYFQU}WVvnOH>YzivdZ%USl#I2$N+t~69gv*vyi9&bTxAhZ7))!Eann=g8qrA z3(J4@{APdS-M&2PGP(=0=lY+%_*GZGO#f%VA83}C3fg!a%>8<9VSi>7TwRgZvunH7 zU*27H8fp9;WD2dvlc-K@sq!G~*}INd`g@IM+Kd=POs`I{!hjbh!0ZF1gHo;>SgqeU z6^X53=^{-hBkO zr^T8V= z^-0O_R=-}6YnXgRI 63-U%c`n> zy}PG1b&os(CzXI}RNK5 P~bN&P@uEc>d*QrIoew-aRPj zdmiW9wSgzq)aI6Rf4f`Nu5gQdA8oaSNF`}UOJ){tI@{KoOk*9s%7Q 6*Ljf^u92Ows}F=QS@F~-wkiFn z9Y#ZXHcZTS`vh@O3)y!&4N^WRHw{j4yhb3MFV<^@L&HbW)Kt-+}D%F6N0e{RMY z6+^&yAtlalvP0im7G?t|&AlguCyV;0gkaFC08YCzbq_waj7;%HZT~YAXtspEofDTZ zj_2N9SP+SS350*G>sQ-nTD^Lvp$6D(q20OA{lgcY?>%Et?{7tojU2fB+B1#cB&wN8 z26~X|*Zn3XI5z)@ YP--J5G>6^_=fJ+qRE!n{DUHKV~Yau988tc z_h6};z8>VN1{OwjJnW<2jYUo4N@cVh-*lR0F7Jc$C?QoZXy(%1*_hAbG76^UeV^p_ zFFg*+Z|Fc(6yK+^pZ-!5_TK7L2ZUky5RjPvaZS_e-j_yntj$7<=R!IV&B(%GKR}^< zv;oUr;s9#PPmdxyBtW?;SiKeNp0J0iuQ-x%>G3T6F9$(6LK1gSz3o>lE?*nP#GavA z$K0Kb^+Hc5qdD=l7IU5V&pyrTC+! -^U= z?fmyco$vkZi{|NA26oes*M@`QbMSjf>ja8y^`i;V34;@vT5uI_O2no^+plfZu{GrU z2H;M*^~5rN{%hLE;EmO^;a9m9-STk&s7sWSDa$37H-e6YOr8oj3w4ZK@gA5$=1xb^ znqJh3hZ(pkiqoktD@yI 2+*?E-q- z*!cEUaW}_NRk8EC68LM;bp$KY&qO_QYfOc4P#ZR5gecZkUV8jSj73xRb0R5L&2hDz z>LX3(Wz&tj;A r^d-QTh_VmiB$?vK>JJ;lRVgS=EIU(m7B963p=`Z3Q zt9AV_t&CVZ)Eud(X Uaw2^HbpAcCbM8Ible_ZuT?Z!^ZhOAVbI)gWWd(VZ`20E= zL1Jpx?bkL}I3b+(&QQ@h#|A{@-F%v$!cy20Latxj9~R#?aS`jHwLsW&BdQ$kZrFzT z?tSVBO+e){x0gd`Nf(<>P(Lhr{G%saYqMQ?I%4Tj_M>Mh{kDeYx^h5{&=JDwvCom^ zJzCgKwg`5+gy=tA_w1y(qc5VF51Tc}jCJ)>A2tojJBijVE9+SSUaN&1-CV&^Ksk~k zDU|GV-al+t1`{xnhS!a>x360J1ZQuf!AURnAa{IA)A(DXA}xvn=hIw~v#$Y9ic7X4 zI96S&VxJU=Fo#qjwm#dOOFC1Z< #&rQ zd%8;SCpC|FV{n~J> F)K9t~$~kj$WuP36VW@q{Fbdkqq?(-Fc=w_Aq)4CtBZyr- zrJ%hr*Cx^DZWf=jXsxqe@CH=L$0-%GN~qr1vYQmWD`jAJ+TA1<6KyZut&lB0w9|7D zdSUnLnRqe7T0`+`2Vn%Q52}3=K?I@42gs|@c#X0bYxS1tIORWdt3<|F(GMhq5h#9C zp&9$URy!YOZFY^s7#NCCek-vn-8$KQPj$Ka5i _gq+3 I@_&>?SZ*X->1LY<7?xMsG zAH*y86(fw3bOc%-7bwrcD jP~8UN^yKE%B1bd^A#3YWwVqHCeAP*Dc|21aoSGlI;;$*1oZ6GrwUL z1|fJ=jkaf et~3=G=$gbg&%VIqdbpiNN`BwN6VwaP z_!h+b>E)Ghzkv=&MdO$K-b67FZX?U0G2!J*DKFMe=n GJyJD|0(L2 z*vwistQA*lIv1rU+9UOxxofj7Uj%aE0jD~^#;eVoqjH08jEQo3=UP*+jksVEre6t4 zet_D#U{0l1LY^~M?V;xfa_&+<1yGBd5%jw*lD2y=7tS>dIU5&7*}=0ab^U+`gYrz# zSZ}&zBlk|T>Ho&aN$HoC#iR{jqxa$M1k;6{Tmh5Tk!s*Ys Kb&rQb$wBV+a4XdKb `gg(q! z?S-jy6(i2B3`zabt=fLW2IXPes7|3~KVS4wCsUvVcT^{In{y6*4HV$RE$F?U^j>f_ zzTQ5jM?N^J+(Di8!sQjzu)Mdgc3@~eM!UB>0lP&QApYH`Ioc&?j>?mX+R4LEaL16i zUt|W^2zC;9lcc;Mj!)7!)~2Ar{UK~OU4%A)+dv?Dyfla|kaif4{uz)r=umnw2uK9l zPvC}(M$O}IMwM?vZfYD)y*xqP2@QRY(qC>nx*!q^C3eh|J(WC{wIh-K)B{@>6n %wAO;>;(}b zc;BR>$4;69sdW?;N*2Rsk(g7e5(*h9VRG)_htRsk!y@T>BOiMg5lP`V4_AvIvoyXy z!1NSaQRC_bY!r jHXz3Nm9M_wvxMOR)?k9zr!Wa}dCsCuiBz z-l)vhtZl%DoI}6qhC)5>dXaYArAKurMO-LCQG7L=`!OHaMB)G?4I@eeZ*(xJ1t*DS zNc|!R>Kg|0GIW;Kf$@q&173uBpLNEk VrEhIvv^Ff|#MApstRL60S8w$TvMBqEGe`2%^zreP(_AB909im3DP`!jj? zDf=NJ5!r}H8fJICZ_X+jCNjqN$`HO1St4GVsxVB6AT)SM)l|+YKNSW0=o!)iy2Xmo zK0`+ ~18SZ^HZV~86ZgfTcccH5J zL)pY+_(THN&KLL(Ni@A#xQ#+oZ>&X5sy0Mo*&XHCI1T0Wkm+>j1b%Dk$wKqJS{X$m z2iZTp02z)_AF`mczodqV {EjfB|9X+8seUx$0Smr6R(y|KoSC&F zo2r+?Ku|NV>zFHgqOxLV#%JRKEfg{x`=0z u$-WNTjaH2jX zE=)pL06HGt`JpfU<_f-~>YbU&d!(1=`J(>9p>aHt8Au&eKv0_;dW9s596N&k2 zoJfV3G9lZT5InYIZiaBj%|SF ^tRT5%BisLV z$o)ls4JZI#572URXu2;nNG wgy^2#n0GK!h9M>pR=JFl$MWcNM;dpKW;;<6V?sN^K=4AEE%dk=p}kD z2bXaDuq+|F3rduP0lVeSS*kli<-Z;_{Pf6w72{LetAzfeD%DGzG|dqw9mBY=TT2h4 zgud#)?xNQlzL)dIa1*}8#8+Ko^G4&mF>KxjHV@Y>?T}$KinL+qnMpiyRma#gCngR7 zw%f?2v4wzz>oqoVX%`Luy_gcJrLcki5Sspj(dHgWG~d}#k!)cr!!#5&6esV6mmIKf zLs#lB)kc*2b+6#;FCl~J`xNwUxqz`f1D&Uxfs^S~JM40 rJBj?TBf0{T!h~9{~5iv7w;NXzj?@DKPu6=@IV#5WQtrKrGa_uKuf>2 z02LRpk$RDO@GAcMKy4hoog@BTkl@dqSunym^m2gmOwS{Wn6tk!rPaWnUd0pP12tmM z1$A7MzW{aQbc5T|p~xBZ{)=ckZnoNt<<5>J#VD*&TDEMmuhFFuX4>D3J&UB>7!iAa zMHubiZ3}E4^$OL%OAPX#?!oRqQ$_G-PWJwmLjlJ}4WjbJ*k)&VGG-~gw894*0OBmY ziy?OO4Fl-9qGNtzl7GfeAE#m2@c&NI9uNDfh*wCtk5RJlgD=>D75%foiUptOqYkVB zL$|*f!gp&20{to0!x_WQ9$i3zo`&Z-|9>NZZ6OVt77dXLc4juRPTzd>>*-)J^@Puj zz||JZ|1+exc6ylB@NX>EW9`m*sHC#tV TvHmI*{3r%kUYd0mf z#%R#|*jSjr5`spB^4cQwE6&Kfh2~=TiZixjEMvH4U7&Y+B!*M5+@PZApB2KOyjG4O zhrvL*yQ7jJJ1b9p0Xhpk5R~6sFmN!^DbYkES1&`Tkc9SfO@F>dIc{Mj$LS82acNI- zsU68H{Aiu+Qh 6{f&L2?Ei=Cmna=nhYF=@}Jj44MF>bmFkjX@|s4T8zot)lk z_Cd4S(^C?TK@G0$BN$9?&?pOPH~nKq(O=A$iiYC)Gqkey`X9PL7gL@5{xe;B1QTTO zeJCD>I{bIv!#Ec%3>4uv(^c1r1_<*2{NU{17t<4daKikN-sliaYhyS4^^dc~(#FDW zG=lWTe?Eh8NN4>|0kn^C@<7+~IsdHzB>$|Tk%5sc;|%qBG1dn<{?9^ \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/status/403.svg b/VideoAnalysis/WebUI/src/assets/status/403.svg new file mode 100644 index 0000000..ba3ce29 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/status/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/status/404.svg b/VideoAnalysis/WebUI/src/assets/status/404.svg new file mode 100644 index 0000000..aacb740 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/status/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/status/500.svg b/VideoAnalysis/WebUI/src/assets/status/500.svg new file mode 100644 index 0000000..ea23a37 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/status/500.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/svg/back_top.svg b/VideoAnalysis/WebUI/src/assets/svg/back_top.svg new file mode 100644 index 0000000..f8e6aa0 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/svg/back_top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/svg/dark.svg b/VideoAnalysis/WebUI/src/assets/svg/dark.svg new file mode 100644 index 0000000..b5c4d2d --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/svg/dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/svg/day.svg b/VideoAnalysis/WebUI/src/assets/svg/day.svg new file mode 100644 index 0000000..b760034 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/svg/day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/svg/enter_outlined.svg b/VideoAnalysis/WebUI/src/assets/svg/enter_outlined.svg new file mode 100644 index 0000000..ab4f9b6 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/svg/enter_outlined.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/svg/exit_screen.svg b/VideoAnalysis/WebUI/src/assets/svg/exit_screen.svg new file mode 100644 index 0000000..c431a05 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/svg/exit_screen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/svg/full_screen.svg b/VideoAnalysis/WebUI/src/assets/svg/full_screen.svg new file mode 100644 index 0000000..b7452e4 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/svg/full_screen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/svg/keyboard_esc.svg b/VideoAnalysis/WebUI/src/assets/svg/keyboard_esc.svg new file mode 100644 index 0000000..e128594 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/svg/keyboard_esc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/svg/system.svg b/VideoAnalysis/WebUI/src/assets/svg/system.svg new file mode 100644 index 0000000..9ad39a5 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/svg/system.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/table-bar/collapse.svg b/VideoAnalysis/WebUI/src/assets/table-bar/collapse.svg new file mode 100644 index 0000000..0823ae6 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/table-bar/collapse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/table-bar/drag.svg b/VideoAnalysis/WebUI/src/assets/table-bar/drag.svg new file mode 100644 index 0000000..f477f16 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/table-bar/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/table-bar/expand.svg b/VideoAnalysis/WebUI/src/assets/table-bar/expand.svg new file mode 100644 index 0000000..bb41c35 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/table-bar/expand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/table-bar/refresh.svg b/VideoAnalysis/WebUI/src/assets/table-bar/refresh.svg new file mode 100644 index 0000000..140288c --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/table-bar/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/table-bar/settings.svg b/VideoAnalysis/WebUI/src/assets/table-bar/settings.svg new file mode 100644 index 0000000..4ecd077 --- /dev/null +++ b/VideoAnalysis/WebUI/src/assets/table-bar/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/assets/user.jpg b/VideoAnalysis/WebUI/src/assets/user.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a2973ace3367cf7181b470e2814db5a9c06a4533 GIT binary patch literal 3694 zcmV-!4w3OvNk&Fy4gdgGMM6+kP&go34gdfUJpi2nDxd(M06vjGnMtLiq9G@=4G6Fj z31 %=YJ_J#Mqzynh+5qkiBt9n)6vG`w5 z59Yt%zw-M=|Ci+3>i=N>Ew5u=yB^RVw5UFe3RiK5ppxFI5hZzrgl1&wg-n3c))Y%AQ zztZ0A)fc1!SiK%ISX~$|)-hs;X=zAAAD@Z*gWVgzm)%g1+`$}ptJ-~!RpF(M+D9~| zyzKiw5rIMAM4W$##xF)FI@aAEmGoC*qlTH*Y#7}@iW_duD(>JV*{zEr22N+LV {FP<1qq}&(tt4p3;yxmOQ!w+`1j={ETfnRl zb*POc9Om`L-8@f+Gk(-A2)n%p1tC~&oQ?yQ38vTpU!MQ)ux#BxhN6FS+Ip3+kZ zo)vP~0RH)Kb6xxU_v?P7m=(NjXJ{Qq)1;v4mM+fSM86gKRbTl S-Q*o$`VsB;freYXMC&Fq|O+`H_g4W7j#I156=zk-lyK(W;g4Pr^fJEUsrmG@~M< z8YJKtA)lr1dC%-VDU&Z@=;T7n$wm_N*ypE#R{lf>W{F^Cc+%9AvK*ifnV3I72I=~y zrX$rW(zQAbZygSnMX7Y;U5>deXIh31`1g-ozrKYzNYUp04RlM>U>Ij}<*ZZho!<05 zYmb%3O)I4=oaZb8Dw2p_yb?=pr8GA){5J)#o6zoeg0xoY9UXILJq>qq^Hr+Y5_Yag zId(Fll_vTlz>?58^}uZksfi)kv{%Pol8y;#(Z3Qjz(#GqGs97bk8XnB4BC(unp=DF zwb@#fdhkZHt ?382*X9i4H3( zP%33VLUkL7<7cB&pEAN|2jiP_KpfK{BwK(ZQ;sRM)qUC5H(1&2goq|LP tRZv0 uO}(WVRdjlr->H4e|s$?c(t= z0Y+*WC>B3@x61{)IcDz!K7qehBk6sfXnf(5`f*hK;TvIM#Isf=jLMsYL;sj$x!2Y8 z{cbjG*LdoeU*lwo%hL0ALneG>2>`vmeQ(IQ^#t3emMwCnq#_gAJ@P0XY1%L2vwB1J z^X|s=t%IgV-(Qy%kXc93u`9Dk&2Ls3i4mGtR(DUGY_(Zm{2##iZJ{CzQ@OnEfM{Qo ze#f+W8o~d;onaf71-BcWY!b=oo`-r;Nq_}|8ehGiG5* utgI7MMek4CM1hxkxXrlORW=5OfHu(6?{I)XQg87b!kL2!Z zukGavgW)KJ+F`?&ZR$T>*<|}e=^$Y)*j|G&9bU3GMX6GxjN?uRK FarnJ1Ii3(q-FM*cM~QMOv{tbAPN} BsP9Y?3ec^(|(8D>aYFf8<+-HrcvmwDl-?u-Vp zJ|3KvuNMo~Fj-xr9OYGmPg6Q I~cU=+S4PG9EoZLla zf~s<=?i7=OIk8oX2%pFnZz4v}0d_Yy_WSVi&tWnP*o6YH(vSfzlb$(ZTy2RS$T+)f z(5r!}zZSDj4mr^-J1`2Yp{${_`5%})c >iJ5iW)1!>1mFAFGx z;B;)cd(7#6vu0zB3Qiz&wE9A&QQy8+YwetHC?0#?zR%)|rIuC^P(^NcLGFLLWvrwT ziSba^I7L^ceM_vWH_7yL!}bLW3DNWJjqs&HWJa>c6e!xK_ _eysks1GnQI z=f_v}Bj_px0`6cV_hwtJ5)QfC)>1IDGb$J>Vy9^nQaj04!p nQu2D>_Ozw$GD}C-N_mEcRS!<`}D?FWA;{I4@mjmo4iX`{bVhu`N5_P7$ z$*T<)7`5sHvXa}65d`e%prY8yx~xO-i9`QIz@F?0o2dtu#R1kNto!5Rr&u4`(XmBy zEWzm3Sj90g7xrfDLt|Z F4$qYzc0Sq}*PSHT6JDm}trLU{TeU&6#Fqam6qK9*P z=N#HV3O9LNdF|4Kk}aJkxHdJT#X4I;oHW$da#uo4OO6Srw>$?F!``-RN^mw_Yxw1o zxo`|bmxWItG&jIDM3j3qsV{jks(QdvNM(AEOv@?eVx=4w8)k6rnBFTQh?@{8W=HvQ zk>-?8)CHGVg;; uXr{F^8={SY<_^v;05dcUlv@?V@0}i&WN8? z@E5~EBta9?8mDw9)qrV$3OL(`wg?`sH$8rc1(LIi5)6y=vR_*$r<4r|;t)!_qCNEp zF}2c#-d4enAZYOlHcS0MkH5V_;>cd~ETx8-fJK#EXWSaQg!I}N`C=~yR^_y3!@Duh z(oAswXyBc3&2Q>c31feA8*p;L0a89nQ9)??NeirTT=;C8ExeBxwgMzTBU5Pr#G_C} zGE9$H@qU}U)DgzCagb1$xUof9d-id5Bs6=`!!BN#>C#n?d6;ksZSVCO-%~UvJ_O z7K93jU4giQlw`I7QvtbdUa&p-I;*l@#2YW?%Ku7!?Gt*r8+9vL{`Ga=mN)Kn1Dh}1 zFFK$=E^uzUi^92o+^tUHv9BwVxin&)&?V)_I#1gC?F^CYC}ZI^4rPOaD>`V-8W`mJ zj)FqAei|k^JBZFZDI5_r$N{6To8A>=DE (nmG4eZ z0Oco+JGokWUkHtBtHHGj|L+S#dTpwVov{G}q#pQd^(dy7@qE`=a28_{C^L8vH zbFj1Kh!|RsnqvSfIg*Y=_P5~OclqFwGl`?fv>G@;%KOeL5AjlIPzeTbrk;ru(H1L0 zd;Isk3}O=|1XHS9xWfK3LT8g=)b>X@PjwP=3)n ^4FB>`%!VQ>9+IHe-L z!`KUQU$KxlEY<-h#RJ_gB9-EV)mBh34C%_DsmSnql4?BtvY8Hl^pqD(!_cnWV@K}M zYr8t?Tlh}AngVOKjWx9G3uu{pb {O&yFosG{7Fnp+`9aBx?GId7yL=665}>Y zbn=*9Gkfa|B$iYPJv{zfO!NZx9NZ9*AXlhxm=t>4pfj{%eUw ~!G)t0L4-lj)}T~z zuzBX_ { + if (!slots) return null; + return hasAuth(props.value) ? ( + {slots.default?.()} + ) : null; + }; + } +}); diff --git a/VideoAnalysis/WebUI/src/components/ReCol/index.ts b/VideoAnalysis/WebUI/src/components/ReCol/index.ts new file mode 100644 index 0000000..7a6c937 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReCol/index.ts @@ -0,0 +1,29 @@ +import { ElCol } from "element-plus"; +import { h, defineComponent } from "vue"; + +// 封装element-plus的el-col组件 +export default defineComponent({ + name: "ReCol", + props: { + value: { + type: Number, + default: 24 + } + }, + render() { + const attrs = this.$attrs; + const val = this.value; + return h( + ElCol, + { + xs: val, + sm: val, + md: val, + lg: val, + xl: val, + ...attrs + }, + { default: () => this.$slots.default() } + ); + } +}); diff --git a/VideoAnalysis/WebUI/src/components/ReDialog/index.ts b/VideoAnalysis/WebUI/src/components/ReDialog/index.ts new file mode 100644 index 0000000..b471764 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReDialog/index.ts @@ -0,0 +1,69 @@ +import { ref } from "vue"; +import reDialog from "./index.vue"; +import { useTimeoutFn } from "@vueuse/core"; +import { withInstall } from "@pureadmin/utils"; +import type { + EventType, + ArgsType, + DialogProps, + ButtonProps, + DialogOptions +} from "./type"; + +const dialogStore = ref>([]); + +/** 打开弹框 */ +const addDialog = (options: DialogOptions) => { + const open = () => + dialogStore.value.push(Object.assign(options, { visible: true })); + if (options?.openDelay) { + useTimeoutFn(() => { + open(); + }, options.openDelay); + } else { + open(); + } +}; + +/** 关闭弹框 */ +const closeDialog = (options: DialogOptions, index: number, args?: any) => { + dialogStore.value[index].visible = false; + options.closeCallBack && options.closeCallBack({ options, index, args }); + + const closeDelay = options?.closeDelay ?? 200; + useTimeoutFn(() => { + dialogStore.value.splice(index, 1); + }, closeDelay); +}; + +/** + * @description 更改弹框自身属性值 + * @param value 属性值 + * @param key 属性,默认`title` + * @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`) + */ +const updateDialog = (value: any, key = "title", index = 0) => { + dialogStore.value[index][key] = value; +}; + +/** 关闭所有弹框 */ +const closeAllDialog = () => { + dialogStore.value = []; +}; + +/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载 + * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4 + * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L12 + * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22 + */ +const ReDialog = withInstall(reDialog); + +export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions }; +export { + ReDialog, + dialogStore, + addDialog, + closeDialog, + updateDialog, + closeAllDialog +}; diff --git a/VideoAnalysis/WebUI/src/components/ReDialog/index.vue b/VideoAnalysis/WebUI/src/components/ReDialog/index.vue new file mode 100644 index 0000000..fb3abaf --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReDialog/index.vue @@ -0,0 +1,206 @@ + + + + + + + + diff --git a/VideoAnalysis/WebUI/src/components/ReDialog/type.ts b/VideoAnalysis/WebUI/src/components/ReDialog/type.ts new file mode 100644 index 0000000..7efbe20 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReDialog/type.ts @@ -0,0 +1,275 @@ +import type { CSSProperties, VNode, Component } from "vue"; + +type DoneFn = (cancel?: boolean) => void; +type EventType = + | "open" + | "close" + | "openAutoFocus" + | "closeAutoFocus" + | "fullscreenCallBack"; +type ArgsType = { + /** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */ + command: "cancel" | "sure" | "close"; +}; +type ButtonType = + | "primary" + | "success" + | "warning" + | "danger" + | "info" + | "text"; + +/** https://element-plus.org/zh-CN/component/dialog.html#attributes */ +type DialogProps = { + /** `Dialog` 的显示与隐藏 */ + visible?: boolean; + /** `Dialog` 的标题 */ + title?: string; + /** `Dialog` 的宽度,默认 `50%` */ + width?: string | number; + /** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */ + fullscreen?: boolean; + /** 是否显示全屏操作图标,默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */ + fullscreenIcon?: boolean; + /** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */ + top?: string; + /** 是否需要遮罩层,默认 `true` */ + modal?: boolean; + /** `Dialog` 自身是否插入至 `body` 元素上。嵌套的 `Dialog` 必须指定该属性并赋值为 `true`,默认 `false` */ + appendToBody?: boolean; + /** 是否在 `Dialog` 出现时将 `body` 滚动锁定,默认 `true` */ + lockScroll?: boolean; + /** `Dialog` 的自定义类名 */ + class?: string; + /** `Dialog` 的自定义样式 */ + style?: CSSProperties; + /** `Dialog` 打开的延时时间,单位毫秒,默认 `0` */ + openDelay?: number; + /** `Dialog` 关闭的延时时间,单位毫秒,默认 `0` */ + closeDelay?: number; + /** 是否可以通过点击 `modal` 关闭 `Dialog`,默认 `true` */ + closeOnClickModal?: boolean; + /** 是否可以通过按下 `ESC` 关闭 `Dialog`,默认 `true` */ + closeOnPressEscape?: boolean; + /** 是否显示关闭按钮,默认 `true` */ + showClose?: boolean; + /** 关闭前的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */ + beforeClose?: (done: DoneFn) => void; + /** 为 `Dialog` 启用可拖拽功能,默认 `false` */ + draggable?: boolean; + /** 是否让 `Dialog` 的 `header` 和 `footer` 部分居中排列,默认 `false` */ + center?: boolean; + /** 是否水平垂直对齐对话框,默认 `false` */ + alignCenter?: boolean; + /** 当关闭 `Dialog` 时,销毁其中的元素,默认 `false` */ + destroyOnClose?: boolean; +}; + +//element-plus.org/zh-CN/component/popconfirm.html#attributes +type Popconfirm = { + /** 标题 */ + title?: string; + /** 确定按钮文字 */ + confirmButtonText?: string; + /** 取消按钮文字 */ + cancelButtonText?: string; + /** 确定按钮类型,默认 `primary` */ + confirmButtonType?: ButtonType; + /** 取消按钮类型,默认 `text` */ + cancelButtonType?: ButtonType; + /** 自定义图标,默认 `QuestionFilled` */ + icon?: string | Component; + /** `Icon` 颜色,默认 `#f90` */ + iconColor?: string; + /** 是否隐藏 `Icon`,默认 `false` */ + hideIcon?: boolean; + /** 关闭时的延迟,默认 `200` */ + hideAfter?: number; + /** 是否将 `popover` 的下拉列表插入至 `body` 元素,默认 `true` */ + teleported?: boolean; + /** 当 `popover` 组件长时间不触发且 `persistent` 属性设置为 `false` 时, `popover` 将会被删除,默认 `false` */ + persistent?: boolean; + /** 弹层宽度,最小宽度 `150px`,默认 `150` */ + width?: string | number; +}; + +type BtnClickDialog = { + options?: DialogOptions; + index?: number; +}; +type BtnClickButton = { + btn?: ButtonProps; + index?: number; +}; +/** https://element-plus.org/zh-CN/component/button.html#button-attributes */ +type ButtonProps = { + /** 按钮文字 */ + label: string; + /** 按钮尺寸 */ + size?: "large" | "default" | "small"; + /** 按钮类型 */ + type?: "primary" | "success" | "warning" | "danger" | "info"; + /** 是否为朴素按钮,默认 `false` */ + plain?: boolean; + /** 是否为文字按钮,默认 `false` */ + text?: boolean; + /** 是否显示文字按钮背景颜色,默认 `false` */ + bg?: boolean; + /** 是否为链接按钮,默认 `false` */ + link?: boolean; + /** 是否为圆角按钮,默认 `false` */ + round?: boolean; + /** 是否为圆形按钮,默认 `false` */ + circle?: boolean; + /** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */ + popconfirm?: Popconfirm; + /** 是否为加载中状态,默认 `false` */ + loading?: boolean; + /** 自定义加载中状态图标组件 */ + loadingIcon?: string | Component; + /** 按钮是否为禁用状态,默认 `false` */ + disabled?: boolean; + /** 图标组件 */ + icon?: string | Component; + /** 是否开启原生 `autofocus` 属性,默认 `false` */ + autofocus?: boolean; + /** 原生 `type` 属性,默认 `button` */ + nativeType?: "button" | "submit" | "reset"; + /** 自动在两个中文字符之间插入空格 */ + autoInsertSpace?: boolean; + /** 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 */ + color?: string; + /** `dark` 模式, 意味着自动设置 `color` 为 `dark` 模式的颜色,默认 `false` */ + dark?: boolean; + /** 自定义元素标签 */ + tag?: string | Component; + /** 点击按钮后触发的回调 */ + btnClick?: ({ + dialog, + button + }: { + /** 当前 `Dialog` 信息 */ + dialog: BtnClickDialog; + /** 当前 `button` 信息 */ + button: BtnClickButton; + }) => void; +}; + +interface DialogOptions extends DialogProps { + /** 内容区组件的 `props`,可通过 `defineProps` 接收 */ + props?: any; + /** 是否隐藏 `Dialog` 按钮操作区的内容 */ + hideFooter?: boolean; + /** 确定按钮的 `Popconfirm` 气泡确认框相关配置 */ + popconfirm?: Popconfirm; + /** 点击确定按钮后是否开启 `loading` 加载动画 */ + sureBtnLoading?: boolean; + /** + * @description 自定义对话框标题的内容渲染器 + * @see {@link https://element-plus.org/zh-CN/component/dialog.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%B4%E9%83%A8} + */ + headerRenderer?: ({ + close, + titleId, + titleClass + }: { + close: Function; + titleId: string; + titleClass: string; + }) => VNode | Component; + /** 自定义内容渲染器 */ + contentRenderer?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => VNode | Component; + /** 自定义按钮操作区的内容渲染器,会覆盖`footerButtons`以及默认的 `取消` 和 `确定` 按钮 */ + footerRenderer?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => VNode | Component; + /** 自定义底部按钮操作 */ + footerButtons?: Array+ {{ options?.title }} + { + fullscreen = !fullscreen; + eventsCallBack( + 'fullscreenCallBack', + { ...options, fullscreen }, + index, + true + ); + } + " + > +++ + + + handleClose(options, index, args)" + /> + + + + + + + + + + +{{ btn?.label }} + ++ {{ btn?.label }} + + + + +; + /** `Dialog` 打开后的回调 */ + open?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发) */ + close?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */ + closeCallBack?: ({ + options, + index, + args + }: { + options: DialogOptions; + index: number; + args: any; + }) => void; + /** 点击全屏按钮时的回调 */ + fullscreenCallBack?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** 输入焦点聚焦在 `Dialog` 内容时的回调 */ + openAutoFocus?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** 输入焦点从 `Dialog` 内容失焦时的回调 */ + closeAutoFocus?: ({ + options, + index + }: { + options: DialogOptions; + index: number; + }) => void; + /** 点击底部取消按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */ + beforeCancel?: ( + done: Function, + { + options, + index + }: { + options: DialogOptions; + index: number; + } + ) => void; + /** 点击底部确定按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */ + beforeSure?: ( + done: Function, + { + options, + index, + closeLoading + }: { + options: DialogOptions; + index: number; + /** 关闭确定按钮的 `loading` 加载动画 */ + closeLoading: Function; + } + ) => void; +} + +export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions }; diff --git a/VideoAnalysis/WebUI/src/components/ReIcon/index.ts b/VideoAnalysis/WebUI/src/components/ReIcon/index.ts new file mode 100644 index 0000000..86efe72 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReIcon/index.ts @@ -0,0 +1,12 @@ +import iconifyIconOffline from "./src/iconifyIconOffline"; +import iconifyIconOnline from "./src/iconifyIconOnline"; +import fontIcon from "./src/iconfont"; + +/** 本地图标组件 */ +const IconifyIconOffline = iconifyIconOffline; +/** 在线图标组件 */ +const IconifyIconOnline = iconifyIconOnline; +/** `iconfont`组件 */ +const FontIcon = fontIcon; + +export { IconifyIconOffline, IconifyIconOnline, FontIcon }; diff --git a/VideoAnalysis/WebUI/src/components/ReIcon/src/hooks.ts b/VideoAnalysis/WebUI/src/components/ReIcon/src/hooks.ts new file mode 100644 index 0000000..57ef18d --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReIcon/src/hooks.ts @@ -0,0 +1,63 @@ +import type { iconType } from "./types"; +import { h, defineComponent, type Component } from "vue"; +import { FontIcon, IconifyIconOnline, IconifyIconOffline } from "../index"; + +/** + * 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标 + * @see 点击查看文档图标篇 {@link https://pure-admin.cn/pages/icon/} + * @param icon 必传 图标 + * @param attrs 可选 iconType 属性 + * @returns Component + */ +export function useRenderIcon(icon: any, attrs?: iconType): Component { + // iconfont + const ifReg = /^IF-/; + // typeof icon === "function" 属于SVG + if (ifReg.test(icon)) { + // iconfont + const name = icon.split(ifReg)[1]; + const iconName = name.slice( + 0, + name.indexOf(" ") == -1 ? name.length : name.indexOf(" ") + ); + const iconType = name.slice(name.indexOf(" ") + 1, name.length); + return defineComponent({ + name: "FontIcon", + render() { + return h(FontIcon, { + icon: iconName, + iconType, + ...attrs + }); + } + }); + } else if (typeof icon === "function" || typeof icon?.render === "function") { + // svg + return attrs ? h(icon, { ...attrs }) : icon; + } else if (typeof icon === "object") { + return defineComponent({ + name: "OfflineIcon", + render() { + return h(IconifyIconOffline, { + icon: icon, + ...attrs + }); + } + }); + } else { + // 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之 + return defineComponent({ + name: "Icon", + render() { + if (!icon) return; + const IconifyIcon = icon.includes(":") + ? IconifyIconOnline + : IconifyIconOffline; + return h(IconifyIcon, { + icon, + ...attrs + }); + } + }); + } +} diff --git a/VideoAnalysis/WebUI/src/components/ReIcon/src/iconfont.ts b/VideoAnalysis/WebUI/src/components/ReIcon/src/iconfont.ts new file mode 100644 index 0000000..df60f25 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReIcon/src/iconfont.ts @@ -0,0 +1,47 @@ +import { h, defineComponent } from "vue"; + +// 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code) +export default defineComponent({ + name: "FontIcon", + props: { + icon: { + type: String, + default: "" + } + }, + render() { + const attrs = this.$attrs; + if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") { + return h( + "i", + { + class: "iconfont", + ...attrs + }, + this.icon + ); + } else if ( + Object.keys(attrs).includes("svg") || + attrs?.iconType === "svg" + ) { + return h( + "svg", + { + class: "icon-svg" + }, + { + default: () => [ + h("use", { + "xlink:href": `#${this.icon}` + }) + ] + } + ); + } else { + return h("i", { + class: `iconfont ${this.icon}`, + ...attrs + }); + } + } +}); diff --git a/VideoAnalysis/WebUI/src/components/ReIcon/src/iconifyIconOffline.ts b/VideoAnalysis/WebUI/src/components/ReIcon/src/iconifyIconOffline.ts new file mode 100644 index 0000000..e5782b2 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReIcon/src/iconifyIconOffline.ts @@ -0,0 +1,47 @@ +import { h, defineComponent } from "vue"; +import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline"; + +// Iconify Icon在Vue里本地使用(用于内网环境) +export default defineComponent({ + name: "IconifyIconOffline", + components: { IconifyIcon }, + props: { + icon: { + default: null + } + }, + render() { + if (typeof this.icon === "object") addIcon(this.icon, this.icon); + const attrs = this.$attrs; + if (typeof this.icon === "string") { + return h( + IconifyIcon, + { + icon: this.icon, + "aria-hidden": false, + style: attrs?.style + ? Object.assign(attrs.style, { outline: "none" }) + : { outline: "none" }, + ...attrs + }, + { + default: () => [] + } + ); + } else { + return h( + this.icon, + { + "aria-hidden": false, + style: attrs?.style + ? Object.assign(attrs.style, { outline: "none" }) + : { outline: "none" }, + ...attrs + }, + { + default: () => [] + } + ); + } + } +}); diff --git a/VideoAnalysis/WebUI/src/components/ReIcon/src/iconifyIconOnline.ts b/VideoAnalysis/WebUI/src/components/ReIcon/src/iconifyIconOnline.ts new file mode 100644 index 0000000..8467e07 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReIcon/src/iconifyIconOnline.ts @@ -0,0 +1,31 @@ +import { h, defineComponent } from "vue"; +import { Icon as IconifyIcon } from "@iconify/vue"; + +// Iconify Icon在Vue里在线使用(用于外网环境) +export default defineComponent({ + name: "IconifyIconOnline", + components: { IconifyIcon }, + props: { + icon: { + type: String, + default: "" + } + }, + render() { + const attrs = this.$attrs; + return h( + IconifyIcon, + { + icon: `${this.icon}`, + "aria-hidden": false, + style: attrs?.style + ? Object.assign(attrs.style, { outline: "none" }) + : { outline: "none" }, + ...attrs + }, + { + default: () => [] + } + ); + } +}); diff --git a/VideoAnalysis/WebUI/src/components/ReIcon/src/offlineIcon.ts b/VideoAnalysis/WebUI/src/components/ReIcon/src/offlineIcon.ts new file mode 100644 index 0000000..b820740 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReIcon/src/offlineIcon.ts @@ -0,0 +1,23 @@ +// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载 +import { getSvgInfo } from "@pureadmin/utils"; +import { addIcon } from "@iconify/vue/dist/offline"; + +// https://icon-sets.iconify.design/ep/?keyword=ep +import EpHomeFilled from "~icons/ep/home-filled?raw"; + +// https://icon-sets.iconify.design/ri/?keyword=ri +import RiSearchLine from "~icons/ri/search-line?raw"; +import RiInformationLine from "~icons/ri/information-line?raw"; + +const icons = [ + // Element Plus Icon: https://github.com/element-plus/element-plus-icons + ["ep/home-filled", EpHomeFilled], + // Remix Icon: https://github.com/Remix-Design/RemixIcon + ["ri/search-line", RiSearchLine], + ["ri/information-line", RiInformationLine] +]; + +// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标 +icons.forEach(([name, icon]) => { + addIcon(name as string, getSvgInfo(icon as string)); +}); diff --git a/VideoAnalysis/WebUI/src/components/ReIcon/src/types.ts b/VideoAnalysis/WebUI/src/components/ReIcon/src/types.ts new file mode 100644 index 0000000..000bdc5 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReIcon/src/types.ts @@ -0,0 +1,20 @@ +export interface iconType { + // iconify (https://docs.iconify.design/icon-components/vue/#properties) + inline?: boolean; + width?: string | number; + height?: string | number; + horizontalFlip?: boolean; + verticalFlip?: boolean; + flip?: string; + rotate?: number | string; + color?: string; + horizontalAlign?: boolean; + verticalAlign?: boolean; + align?: string; + onLoad?: Function; + includes?: Function; + // svg 需要什么SVG属性自行添加 + fill?: string; + // all icon + style?: object; +} diff --git a/VideoAnalysis/WebUI/src/components/RePerms/index.ts b/VideoAnalysis/WebUI/src/components/RePerms/index.ts new file mode 100644 index 0000000..3701c3c --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/RePerms/index.ts @@ -0,0 +1,5 @@ +import perms from "./src/perms"; + +const Perms = perms; + +export { Perms }; diff --git a/VideoAnalysis/WebUI/src/components/RePerms/src/perms.tsx b/VideoAnalysis/WebUI/src/components/RePerms/src/perms.tsx new file mode 100644 index 0000000..da01bc1 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/RePerms/src/perms.tsx @@ -0,0 +1,20 @@ +import { defineComponent, Fragment } from "vue"; +import { hasPerms } from "@/utils/auth"; + +export default defineComponent({ + name: "Perms", + props: { + value: { + type: undefined, + default: [] + } + }, + setup(props, { slots }) { + return () => { + if (!slots) return null; + return hasPerms(props.value) ? ( + {slots.default?.()} + ) : null; + }; + } +}); diff --git a/VideoAnalysis/WebUI/src/components/RePureTableBar/index.ts b/VideoAnalysis/WebUI/src/components/RePureTableBar/index.ts new file mode 100644 index 0000000..31b8a16 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/RePureTableBar/index.ts @@ -0,0 +1,5 @@ +import pureTableBar from "./src/bar"; +import { withInstall } from "@pureadmin/utils"; + +/** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */ +export const PureTableBar = withInstall(pureTableBar); diff --git a/VideoAnalysis/WebUI/src/components/RePureTableBar/src/bar.tsx b/VideoAnalysis/WebUI/src/components/RePureTableBar/src/bar.tsx new file mode 100644 index 0000000..1dfadbe --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/RePureTableBar/src/bar.tsx @@ -0,0 +1,393 @@ +import Sortable from "sortablejs"; +import { useEpThemeStoreHook } from "@/store/modules/epTheme"; +import { + type PropType, + ref, + unref, + computed, + nextTick, + defineComponent, + getCurrentInstance +} from "vue"; +import { + delay, + cloneDeep, + isBoolean, + isFunction, + getKeyList +} from "@pureadmin/utils"; + +import Fullscreen from "~icons/ri/fullscreen-fill"; +import ExitFullscreen from "~icons/ri/fullscreen-exit-fill"; +import DragIcon from "@/assets/table-bar/drag.svg?component"; +import ExpandIcon from "@/assets/table-bar/expand.svg?component"; +import RefreshIcon from "@/assets/table-bar/refresh.svg?component"; +import SettingIcon from "@/assets/table-bar/settings.svg?component"; +import CollapseIcon from "@/assets/table-bar/collapse.svg?component"; + +const props = { + /** 头部最左边的标题 */ + title: { + type: String, + default: "列表" + }, + /** 对于树形表格,如果想启用展开和折叠功能,传入当前表格的ref即可 */ + tableRef: { + type: Object as PropType+ }, + /** 需要展示的列 */ + columns: { + type: Array as PropType , + default: () => [] + }, + isExpandAll: { + type: Boolean, + default: true + }, + tableKey: { + type: [String, Number] as PropType , + default: "0" + } +}; + +export default defineComponent({ + name: "PureTableBar", + props, + emits: ["refresh", "fullscreen"], + setup(props, { emit, slots, attrs }) { + const size = ref("default"); + const loading = ref(false); + const checkAll = ref(true); + const isFullscreen = ref(false); + const isIndeterminate = ref(false); + const instance = getCurrentInstance()!; + const isExpandAll = ref(props.isExpandAll); + const filterColumns = cloneDeep(props?.columns).filter(column => + isBoolean(column?.hide) + ? !column.hide + : !(isFunction(column?.hide) && column?.hide()) + ); + let checkColumnList = getKeyList(cloneDeep(props?.columns), "label"); + const checkedColumns = ref(getKeyList(cloneDeep(filterColumns), "label")); + const dynamicColumns = ref(cloneDeep(props?.columns)); + + const getDropdownItemStyle = computed(() => { + return s => { + return { + background: + s === size.value ? useEpThemeStoreHook().epThemeColor : "", + color: s === size.value ? "#fff" : "var(--el-text-color-primary)" + }; + }; + }); + + const iconClass = computed(() => { + return [ + "text-black", + "dark:text-white", + "duration-100", + "hover:text-primary!", + "cursor-pointer", + "outline-hidden" + ]; + }); + + const topClass = computed(() => { + return [ + "flex", + "justify-between", + "pt-[3px]", + "px-[11px]", + "border-b-[1px]", + "border-solid", + "border-[#dcdfe6]", + "dark:border-[#303030]" + ]; + }); + + function onReFresh() { + loading.value = true; + emit("refresh"); + delay(500).then(() => (loading.value = false)); + } + + function onExpand() { + isExpandAll.value = !isExpandAll.value; + toggleRowExpansionAll(props.tableRef.data, isExpandAll.value); + } + + function onFullscreen() { + isFullscreen.value = !isFullscreen.value; + emit("fullscreen", isFullscreen.value); + } + + function toggleRowExpansionAll(data, isExpansion) { + data.forEach(item => { + props.tableRef.toggleRowExpansion(item, isExpansion); + if (item.children !== undefined && item.children !== null) { + toggleRowExpansionAll(item.children, isExpansion); + } + }); + } + + function handleCheckAllChange(val: boolean) { + checkedColumns.value = val ? checkColumnList : []; + isIndeterminate.value = false; + dynamicColumns.value.map(column => + val ? (column.hide = false) : (column.hide = true) + ); + } + + function handleCheckedColumnsChange(value: string[]) { + checkedColumns.value = value; + const checkedCount = value.length; + checkAll.value = checkedCount === checkColumnList.length; + isIndeterminate.value = + checkedCount > 0 && checkedCount < checkColumnList.length; + } + + function handleCheckColumnListChange(val: boolean, label: string) { + dynamicColumns.value.filter(item => item.label === label)[0].hide = !val; + } + + async function onReset() { + checkAll.value = true; + isIndeterminate.value = false; + dynamicColumns.value = cloneDeep(props?.columns); + checkColumnList = []; + checkColumnList = await getKeyList(cloneDeep(props?.columns), "label"); + checkedColumns.value = getKeyList(cloneDeep(filterColumns), "label"); + } + + const dropdown = { + dropdown: () => ( + + + ) + }; + + /** 列展示拖拽排序 */ + const rowDrop = (event: { preventDefault: () => void }) => { + event.preventDefault(); + nextTick(() => { + const wrapper: HTMLElement = ( + instance?.proxy?.$refs[`GroupRef${unref(props.tableKey)}`] as any + ).$el.firstElementChild; + Sortable.create(wrapper, { + animation: 300, + handle: ".drag-btn", + onEnd: ({ newIndex, oldIndex, item }) => { + const targetThElem = item; + const wrapperElem = targetThElem.parentNode as HTMLElement; + const oldColumn = dynamicColumns.value[oldIndex]; + const newColumn = dynamicColumns.value[newIndex]; + if (oldColumn?.fixed || newColumn?.fixed) { + // 当前列存在fixed属性 则不可拖拽 + const oldThElem = wrapperElem.children[oldIndex] as HTMLElement; + if (newIndex > oldIndex) { + wrapperElem.insertBefore(targetThElem, oldThElem); + } else { + wrapperElem.insertBefore( + targetThElem, + oldThElem ? oldThElem.nextElementSibling : oldThElem + ); + } + return; + } + const currentRow = dynamicColumns.value.splice(oldIndex, 1)[0]; + dynamicColumns.value.splice(newIndex, 0, currentRow); + } + }); + }); + }; + + const isFixedColumn = (label: string) => { + return dynamicColumns.value.filter(item => item.label === label)[0].fixed + ? true + : false; + }; + + const rendTippyProps = (content: string) => { + // https://vue-tippy.netlify.app/props + return { + content, + offset: [0, 18], + duration: [300, 0], + followCursor: true, + hideOnClick: "toggle" + }; + }; + + const reference = { + reference: () => ( +(size.value = "large")} + > + 宽松 + +(size.value = "default")} + > + 默认 + +(size.value = "small")} + > + 紧凑 + ++ ) + }; + + return () => ( + <> + ++ > + ); + } +}); diff --git a/VideoAnalysis/WebUI/src/components/ReSegmented/index.ts b/VideoAnalysis/WebUI/src/components/ReSegmented/index.ts new file mode 100644 index 0000000..de4253c --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReSegmented/index.ts @@ -0,0 +1,8 @@ +import reSegmented from "./src/index"; +import { withInstall } from "@pureadmin/utils"; + +/** 分段控制器组件 */ +export const ReSegmented = withInstall(reSegmented); + +export default ReSegmented; +export type { OptionsType } from "./src/type"; diff --git a/VideoAnalysis/WebUI/src/components/ReSegmented/src/index.css b/VideoAnalysis/WebUI/src/components/ReSegmented/src/index.css new file mode 100644 index 0000000..4fe79ef --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReSegmented/src/index.css @@ -0,0 +1,156 @@ +.pure-segmented { + --pure-control-padding-horizontal: 12px; + --pure-control-padding-horizontal-sm: 8px; + --pure-segmented-track-padding: 2px; + --pure-segmented-line-width: 1px; + + --pure-segmented-border-radius-small: 4px; + --pure-segmented-border-radius-base: 6px; + --pure-segmented-border-radius-large: 8px; + + box-sizing: border-box; + display: inline-block; + padding: var(--pure-segmented-track-padding); + font-size: var(--el-font-size-base); + color: rgba(0, 0, 0, 0.65); + background-color: rgb(0 0 0 / 4%); + border-radius: var(--pure-segmented-border-radius-base); +} + +.pure-segmented-block { + display: flex; +} + +.pure-segmented-block .pure-segmented-item { + flex: 1; + min-width: 0; +} + +.pure-segmented-block .pure-segmented-item > .pure-segmented-item-label > span { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/* small */ +.pure-segmented.pure-segmented--small { + border-radius: var(--pure-segmented-border-radius-small); +} +.pure-segmented.pure-segmented--small .pure-segmented-item { + border-radius: var(--el-border-radius-small); +} +.pure-segmented.pure-segmented--small .pure-segmented-item > div { + min-height: calc( + var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2 + ); + line-height: calc( + var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2 + ); + padding: 0 + calc( + var(--pure-control-padding-horizontal-sm) - + var(--pure-segmented-line-width) + ); +} + +/* large */ +.pure-segmented.pure-segmented--large { + border-radius: var(--pure-segmented-border-radius-large); +} +.pure-segmented.pure-segmented--large .pure-segmented-item { + border-radius: calc( + var(--el-border-radius-base) + var(--el-border-radius-small) + ); +} +.pure-segmented.pure-segmented--large .pure-segmented-item > div { + min-height: calc( + var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2 + ); + line-height: calc( + var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2 + ); + padding: 0 + calc( + var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width) + ); + font-size: var(--el-font-size-medium); +} + +/* default */ +.pure-segmented-item { + position: relative; + text-align: center; + cursor: pointer; + border-radius: var(--el-border-radius-base); + transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1); +} +.pure-segmented .pure-segmented-item > div { + min-height: calc( + var(--el-component-size) - var(--pure-segmented-track-padding) * 2 + ); + line-height: calc( + var(--el-component-size) - var(--pure-segmented-track-padding) * 2 + ); + padding: 0 + calc( + var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width) + ); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.pure-segmented-group { + position: relative; + display: flex; + align-items: stretch; + justify-items: flex-start; + width: 100%; +} + +.pure-segmented-item-selected { + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; + display: none; + width: 0; + height: 100%; + padding: 4px 0; + background-color: #fff; + border-radius: 4px; + box-shadow: + 0 2px 8px -2px rgb(0 0 0 / 5%), + 0 1px 4px -1px rgb(0 0 0 / 7%), + 0 0 1px rgb(0 0 0 / 7%); + transition: + transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1), + width 0.5s cubic-bezier(0.645, 0.045, 0.355, 1); + will-change: transform, width; +} + +.pure-segmented-item > input { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + width: 0; + height: 0; + opacity: 0; + pointer-events: none; +} + +.pure-segmented-item-label { + display: flex; + align-items: center; + justify-content: center; +} + +.pure-segmented-item-icon svg { + width: 16px; + height: 16px; +} + +.pure-segmented-item-disabled { + color: rgba(0, 0, 0, 0.25); + cursor: not-allowed; +} diff --git a/VideoAnalysis/WebUI/src/components/ReSegmented/src/index.tsx b/VideoAnalysis/WebUI/src/components/ReSegmented/src/index.tsx new file mode 100644 index 0000000..39580ed --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReSegmented/src/index.tsx @@ -0,0 +1,216 @@ +import "./index.css"; +import type { OptionsType } from "./type"; +import { useRenderIcon } from "@/components/ReIcon/src/hooks"; +import { + useDark, + isNumber, + isFunction, + useResizeObserver +} from "@pureadmin/utils"; +import { + type PropType, + h, + ref, + toRef, + watch, + nextTick, + defineComponent, + getCurrentInstance +} from "vue"; + +const props = { + options: { + type: Array+ {slots?.title ? ( + slots.title() + ) : ( ++ {slots.default({ + size: size.value, + dynamicColumns: dynamicColumns.value + })} +{props.title}
+ )} ++ {slots?.buttons ? ( ++{slots.buttons()}+ ) : null} + {props.tableRef?.size ? ( + <> +onExpand()} + /> + + > + ) : null} + onReFresh()} + /> + + + ++ + + + +++ +handleCheckAllChange(value)} + /> + onReset()}> + 重置 + ++++ +handleCheckedColumnsChange(value)} + > + ++ {checkColumnList.map((item, index) => { + return ( + +++ ); + })} +void; + }) => rowDrop(event)} + /> + + handleCheckColumnListChange(value, item) + } + > + + {item} + + ++ + onFullscreen()} + /> + , + default: () => [] + }, + /** 默认选中,按照第一个索引为 `0` 的模式,可选(`modelValue`只有传`number`类型时才为响应式) */ + modelValue: { + type: undefined, + require: false, + default: "0" + }, + /** 将宽度调整为父元素宽度 */ + block: { + type: Boolean, + default: false + }, + /** 控件尺寸 */ + size: { + type: String as PropType<"small" | "default" | "large"> + }, + /** 是否全局禁用,默认 `false` */ + disabled: { + type: Boolean, + default: false + }, + /** 当内容发生变化时,设置 `resize` 可使其自适应容器位置 */ + resize: { + type: Boolean, + default: false + } +}; + +export default defineComponent({ + name: "ReSegmented", + props, + emits: ["change", "update:modelValue"], + setup(props, { emit }) { + const width = ref(0); + const translateX = ref(0); + const { isDark } = useDark(); + const initStatus = ref(false); + const curMouseActive = ref(-1); + const segmentedItembg = ref(""); + const instance = getCurrentInstance()!; + const curIndex = isNumber(props.modelValue) + ? toRef(props, "modelValue") + : ref(0); + + function handleChange({ option, index }, event: Event) { + if (props.disabled || option.disabled) return; + event.preventDefault(); + isNumber(props.modelValue) + ? emit("update:modelValue", index) + : (curIndex.value = index); + segmentedItembg.value = ""; + emit("change", { index, option }); + } + + function handleMouseenter({ option, index }, event: Event) { + if (props.disabled) return; + event.preventDefault(); + curMouseActive.value = index; + if (option.disabled || curIndex.value === index) { + segmentedItembg.value = ""; + } else { + segmentedItembg.value = isDark.value + ? "#1f1f1f" + : "rgba(0, 0, 0, 0.06)"; + } + } + + function handleMouseleave(_, event: Event) { + if (props.disabled) return; + event.preventDefault(); + curMouseActive.value = -1; + } + + function handleInit(index = curIndex.value) { + nextTick(() => { + const curLabelRef = instance?.proxy?.$refs[`labelRef${index}`] as ElRef; + if (!curLabelRef) return; + width.value = curLabelRef.clientWidth; + translateX.value = curLabelRef.offsetLeft; + initStatus.value = true; + }); + } + + function handleResizeInit() { + useResizeObserver(".pure-segmented", () => { + nextTick(() => { + handleInit(curIndex.value); + }); + }); + } + + (props.block || props.resize) && handleResizeInit(); + + watch( + () => curIndex.value, + index => { + nextTick(() => { + handleInit(index); + }); + }, + { + immediate: true + } + ); + + watch(() => props.size, handleResizeInit, { + immediate: true + }); + + const rendLabel = () => { + return props.options.map((option, index) => { + return ( + + ); + }); + }; + + return () => ( + ++ ); + } +}); diff --git a/VideoAnalysis/WebUI/src/components/ReSegmented/src/type.ts b/VideoAnalysis/WebUI/src/components/ReSegmented/src/type.ts new file mode 100644 index 0000000..6c29889 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReSegmented/src/type.ts @@ -0,0 +1,20 @@ +import type { VNode, Component } from "vue"; +import type { iconType } from "@/components/ReIcon/src/types.ts"; + +export interface OptionsType { + /** 文字 */ + label?: string | (() => VNode | Component); + /** + * @description 图标,采用平台内置的 `useRenderIcon` 函数渲染 + * @see {@link 用法参考 https://pure-admin.cn/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks } + */ + icon?: string | Component; + /** 图标属性、样式配置 */ + iconAttrs?: iconType; + /** 值 */ + value?: any; + /** 是否禁用 */ + disabled?: boolean; + /** `tooltip` 提示 */ + tip?: string; +} diff --git a/VideoAnalysis/WebUI/src/components/ReText/index.ts b/VideoAnalysis/WebUI/src/components/ReText/index.ts new file mode 100644 index 0000000..6213566 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReText/index.ts @@ -0,0 +1,7 @@ +import reText from "./src/index.vue"; +import { withInstall } from "@pureadmin/utils"; + +/** 支持`Tooltip`提示的文本省略组件 */ +export const ReText = withInstall(reText); + +export default ReText; diff --git a/VideoAnalysis/WebUI/src/components/ReText/src/index.vue b/VideoAnalysis/WebUI/src/components/ReText/src/index.vue new file mode 100644 index 0000000..4c4a232 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/ReText/src/index.vue @@ -0,0 +1,69 @@ + + + ++ + {rendLabel()} +++ + diff --git a/VideoAnalysis/WebUI/src/components/hTable/hTable.ts b/VideoAnalysis/WebUI/src/components/hTable/hTable.ts new file mode 100644 index 0000000..00412b4 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/hTable/hTable.ts @@ -0,0 +1,454 @@ +import { Ref } from "vue"; +import { array } from "vue-types"; + +export interface Dialog { + /** 对话框是否可见 */ + visible: boolean; + /** 是否显示关闭按钮 */ + close: boolean; + /** 对话框标题 */ + title: string; + /** 对话框宽度 */ + width: string; + /**自定义弹窗数据 */ + custom: { + /** 自定义对话框高度 */ + height: string; + /** 自定义对话框数据 */ + data: any[]; + /** 自定义组件路径 */ + src?: string; + /** 自定义配置项 */ + custom: Record+ ; + /** 自定义对话框是否可见 */ + visible: boolean; + /** 异步加载组件 */ + component: any; + }; + edit: { + /** 编辑项ID */ + id: number; + /** 编辑对话框标题 */ + title: string; + /** 编辑对话框是否可见 */ + visible: boolean; + row?: any; + tagData?: any; + }; +} + +/** 按钮自定义配置 */ +export interface ButtonCustomConfig { + /** 弹出框标题 */ + title: string; + /** 组件路径 */ + src: string; + /** 弹框宽度 */ + width: string; + /** 弹框高度 */ + height: string; +} + +/** 操作按钮配置 */ +export interface OperationButton { + /** 是否为头部按钮 */ + topBtn: boolean; + /** 按钮权限码 */ + perms?: string; + /** 是否显示 */ + show?: boolean; + /** 按钮文本 */ + label: string; + /** 按钮点击事件 + * @tips btnType 为空时触发 + * @param obj 当前按钮配置对象 + * @param row 当前行数据 + * @param handleReloadPaged 父表单刷新函数 + */ + click?: (obj, row, handleReloadPaged: (reload?: boolean) => void) => void; + /** 按钮类型 */ + btnType?: "add" | "edit" | "del" | "custom"; + /** 按钮样式 */ + btnStyle?: "success" | "info" | "primary" | "danger" | "warning"; + /** 自定义按钮配置 */ + custom?: ButtonCustomConfig; +} + +/** 类型判断枚举 */ +export enum ConditionalType { + Equal, + Like, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + In, + NotIn, + LikeLeft, + LikeRight, + NoEqual, + IsNullOrEmpty, + IsNot, + NoLike, + EqualNull, + InLike, + Range +} + +/** 字段设置项 */ +export class FieldSetting { + + constructor() { + this.datasource = []; + this.mapValue= "value", + this.maplabel= "text" + + } + /**map 时Value的取值的属性 */ + mapValue?: string; + /**map 时label的取值的属性 */ + maplabel?: string; + /** + * 图片地址 获取方式 + * @param value 当前值 + * @param row 当前行值 + * @returns 预期返回有效图片地址url + */ + imgUrl?: (value: any, row: any) => string; + /** 数据源 */ + datasource?: ComboModel[]; +} +///** 表格列配置 */ +//export interface TableEditColumn {} +export interface ComboModel { + value: any; + text: string; +} +/** 新增修改的配置 */ +export class TableColumnEdit { + /** + * + * @param add 字段可以添加 + * @param edit 字段可以修改 + * @param multiple [type=dropdown]是否多选 + * @param editShow 编辑时显示列 + * @param editRows [type=textarea]编辑时的行数 + * @param rules 校验规则 + */ + constructor( + add: boolean = false, + edit: boolean = false, + multiple: boolean = false, + editShow: boolean = true, + editRows: number = 3, + editDefault: any = "", + rules: any | Array = null, + change: () => void = null + ) { + this.add = add; + this.edit = edit; + this.multiple = multiple; + this.editShow = editShow; + this.editRows = editRows; + this.editDefault = editDefault; + this.rules = rules; + this.change = change; + } + /** 是否允许添加 [false]*/ + add?: boolean; + /** 是否允许修改 [false]*/ + edit?: boolean; + /** [type=dropdown]是否多选 */ + multiple?: boolean; + /** 编辑时显示列 */ + editShow?: boolean; + /**校验规则 */ + rules?: any | Array ; + /** [type=textarea]编辑时的行数 */ + editRows?: number; + + /** 新增编辑时的缓存值 */ + valueE?: Array | string | number | boolean | Date; + /** 新增编辑时的默认 */ + editDefault?: Array | string | number | boolean | Date; + + /**编辑时值发生变化 */ + change?: () => void; +} +/** 查询的配置 */ +export class TableColumnSearch { + /** + * + * @param yes 列是否可以查询 + * @param sort 列是否可以排序 + * @param searchType 查询时候的值比较类型 + */ + constructor(yes: boolean = false, + searchType: ConditionalType = ConditionalType.Like,sort: boolean = false){ + this.yes = yes; + this.sort = sort; + this.searchType = searchType; + } + /**可以查询 [false] */ + yes?: boolean; + /** 可以排序 [false]*/ + sort?: boolean; + /** 搜索类型 */ + searchType?: ConditionalType; + /** 查询缓存值 */ + value?: Array | string | number | boolean | Date; +} + +/** 表格列配置 */ +export class TableColumn { + constructor() { + this.type = "string"; + this.show = true; + this.search = new TableColumnSearch(); + this.edit = new TableColumnEdit(); + this.fixed = false; + this.setting = {}; + } + + /** 显示标签 */ + label: string; + fixed?: 'left' | 'right'| boolean; + /** 查询配置 */ + search?: TableColumnSearch; + /** 编辑配置 */ + edit?: TableColumnEdit; + /** Table中展示宽度 [auto]*/ + width?: string; + /** 字段类型 */ + type?: "string" | "dropdown" | "switch" | "img" | "datetime" | "textarea"; + /** 显示列 [不会动态计算]*/ + show?: boolean; + /** 字段设置 */ + setting?: FieldSetting; + /**列值初始化时 如何获取默认取对应列*/ + custom?: (row: any) => string; +} + + + +/** 表格列配置 */ +// export interface TableColumn { +// /** 显示标签 */ +// label: string; + +// /** 是否可搜索 */ +// search: boolean; +// /** 搜索类型 */ +// searchType?: ConditionalType; +// /** 是否允许添加 [false]*/ +// add?: boolean; +// /** 是否允许修改 [false]*/ +// edit?: boolean; +// /** Table中展示宽度 [auto]*/ +// width?: string; +// /** 字段类型 */ +// type?: "string" | "dropdown" | "switch" | "img" | "datetime" | "textarea"; +// /** 是否多选 */ +// multiple?: boolean; +// /** 编辑时显示列 */ +// editShow?: boolean; +// /**校验规则 */ +// rules?: any | Array ; +// /** 显示列 */ +// show?: boolean; +// /** 字段设置 */ +// setting?: FieldSetting; +// /** 修改时的编辑值 */ +// valueE?: Array | string | number | boolean | Date; +// /** 查询值 */ +// value?: Array | string | number | boolean | Date; +// /** textarea编辑时的行数 */ +// editRows?: number; +// /**编辑时值发生变化 */ +// change?: () => void; +// /**列值初始化时 如何获取默认取对应列*/ +// custom?: (row: any) => string; +// } + +/** 分页数据 */ +export interface PageData { + /** 总条数 */ + total: number; +} + +/** 分页数据 */ +export interface ConditionalModel { + /** 字段名称 */ + FieldName: string; + /** 字段查询值 */ + FieldValue: string; + /** 查询方式 */ + ConditionalType?: ConditionalType; + /** C#类型名称 */ + CSharpTypeName?: string; +} + +/** 搜索条件 */ +export class SearchConditions { + /** + * + */ + constructor() { + this.show = true; + this.showPage = true; + this.PageIndex = 0; + this.PageSize = 20; + this.OrderBy = "Id"; + this.OrderByType = 1; + this.defaultConditions = []; + this.Conditions = []; + } + /** 是否显示搜索 */ + show?: boolean; + /** 显示分页器 */ + showPage?:boolean; + /** 当前页码 */ + PageIndex?: number; + /** 每页大小 */ + PageSize?: number; + /** 排序字段 */ + OrderBy?: string; + /**排序顺序 + * @tips 0:升序 1:降序 + * @默认 = 1 + */ + OrderByType?: 0 | 1; + /** 默认查询条件 */ + defaultConditions?: ConditionalModel[]; + /** 查询条件 */ + Conditions?: any[]; +} + +/** 表格配置 */ +export interface TableConfig { + /** 搜索回调函数 */ + searchCallback?: (s: SearchConditions) => void; + /** 新增/修改回调函数 */ + editCallback?: (from: any) => void; + /** 编辑表单初始化回调函数 */ + editInitCallback?: (from: Ref >) => void; + /** 展开行的回调 */ + expandChange?: (row: any, expandedRows: any[]) => void; + /** API地址 */ + apiUrl: string; + /** 是否显示选择列 */ + selectColumn: boolean; + /** 搜索配置 */ + search: SearchConditions; + /** 是否显示操作列 */ + operationColumn: boolean; + /** 操作列是否固定列 */ + operationColumnFixed?: "left" | "right" | boolean; + /** 是否允许展开列 */ + expandColumn?: boolean; + /** 操作按钮配置 */ + operationColumnData: OperationButton[]; + /** 列配置 */ + column: Record ; + /** 表格数据 */ + data: any[]; + /**显示头部操作按钮 */ + operationTop?: boolean; + /** 分页数据 */ + pageData: PageData; + /** 选中行 */ + selectRows: any[]; + /** 是否显示边框 */ + border: boolean; + /**是否显示 */ + show?: boolean; +} +/** 初始化表格数据 */ +export function intTableData(tValue: TableConfig): TableConfig { + if (!tValue.data) tValue.data = []; + if (!tValue.selectRows) tValue.selectRows = []; + if (tValue.border == null) tValue.border = true; + if (tValue.operationColumnFixed == null) tValue.operationColumnFixed = false; + if (tValue.expandColumn == null) tValue.expandColumn = false; + if (!tValue.pageData) tValue.pageData = { total: 0 }; + if (tValue.operationTop === undefined) tValue.operationTop = true; + + //分页查询配置 + tValue.search= { ...new SearchConditions(), ...tValue.search }; + + // 处理 column 的属性 + for (const key in tValue.column) { + tValue.column[key] = { ...new TableColumn(), ...tValue.column[key] }; + const element = tValue.column[key]; + + element.edit = { ...new TableColumnEdit(), ...element.edit }; + element.search = { ...new TableColumnSearch(), ...element.search }; + element.setting = { ...new FieldSetting(), ...element.setting }; + + //已有的情况下以 传入为主 + if (element.custom != undefined) continue; + switch (element.type) { + case "switch": + case "dropdown": { + element.custom = row => { + const value = Array.isArray(row[key]) ? row[key] : [row[key]]; + const res = value.map(item => { + const sc = element.setting.datasource.find( + s => s[element.setting.mapValue] + "" == item + "" + ); + return !sc ? item : sc[element.setting.maplabel]; + }); + return res.join(","); + }; + break; + } + + case "string": + case undefined: { + element.custom = row => row[key]; + break; + } + default: { + element.custom = row => row[key]; + break; + } + } + } + + // 处理 operationColumnData 的属性 + for (const key in tValue.operationColumnData) { + const element = tValue.operationColumnData[key]; + if (element.show === undefined) element.show = true; + } + return tValue; +} + +/** + * 生成 毕业届下拉 + */ +export function gradeComboModel(): ComboModel[] { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + const isAfterAugust = currentMonth >= 9; + const baseYear = isAfterAugust ? currentYear : currentYear - 1; + + const gradeOffsets = [ + { baseGradeName: "初", yearOffset: 0, grade: "初一" }, // 初一 + { baseGradeName: "初", yearOffset: 1, grade: "初二" }, // 初二 + { baseGradeName: "初", yearOffset: 2, grade: "初三" }, // 初三 + { baseGradeName: "高", yearOffset: 0, grade: "高一" }, // 高一 + { baseGradeName: "高", yearOffset: 1, grade: "高二" }, // 高二 + { baseGradeName: "高", yearOffset: 2, grade: "高三" }, // 高三 + ]; + + return gradeOffsets.map( + (item): ComboModel => { + const entranceYear = baseYear - item.yearOffset; + const graduationYear = entranceYear + 3; + let v = `${item.baseGradeName}${graduationYear}届`; + return { text: v + ` [${item.grade}]`, value: v }; + } + ); +} \ No newline at end of file diff --git a/VideoAnalysis/WebUI/src/components/hTable/hTableEdit.vue b/VideoAnalysis/WebUI/src/components/hTable/hTableEdit.vue new file mode 100644 index 0000000..3df60df --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/hTable/hTableEdit.vue @@ -0,0 +1,230 @@ + + + + ++ + diff --git a/VideoAnalysis/WebUI/src/components/hTable/index.vue b/VideoAnalysis/WebUI/src/components/hTable/index.vue new file mode 100644 index 0000000..6afc551 --- /dev/null +++ b/VideoAnalysis/WebUI/src/components/hTable/index.vue @@ -0,0 +1,634 @@ + + + ++ ++ + ++++ +++ ++ +++ +++ +++ + +立即提交 +重置 +++ + + diff --git a/VideoAnalysis/WebUI/src/config/index.ts b/VideoAnalysis/WebUI/src/config/index.ts new file mode 100644 index 0000000..c81d1c4 --- /dev/null +++ b/VideoAnalysis/WebUI/src/config/index.ts @@ -0,0 +1,55 @@ +import axios from "axios"; +import type { App } from "vue"; + +let config: object = {}; +const { VITE_PUBLIC_PATH } = import.meta.env; + +const setConfig = (cfg?: unknown) => { + config = Object.assign(config, cfg); +}; + +const getConfig = (key?: string): PlatformConfigs => { + if (typeof key === "string") { + const arr = key.split("."); + if (arr && arr.length) { + let data = config; + arr.forEach(v => { + if (data && typeof data[v] !== "undefined") { + data = data[v]; + } else { + data = null; + } + }); + return data; + } + } + return config; +}; + +/** 获取项目动态全局配置 */ +export const getPlatformConfig = async (app: App): Promise+ ++ + ++ ++ ++ + ++ + + ++ + + + +查询 + +重置 ++ ++ + + + + +++ + ++ {{ e.label }} ++ + + ++ + + + ++ ++ ++ {{ item.custom(scope.row) }} +++ + ++ {{ item.setting.imgUrl(scope.row[name], scope.row) }} ++ ++ ++ {{ item.custom(scope.row) }} ++++ ++ +++ ++ + => { + app.config.globalProperties.$config = getConfig(); + return axios({ + method: "get", + url: `${VITE_PUBLIC_PATH}platform-config.json` + }) + .then(({ data: config }) => { + let $config = app.config.globalProperties.$config; + // 自动注入系统配置 + if (app && $config && typeof config === "object") { + $config = Object.assign($config, config); + app.config.globalProperties.$config = $config; + // 设置全局配置 + setConfig($config); + } + return $config; + }) + .catch(() => { + throw "请在public文件夹下添加platform-config.json配置文件"; + }); +}; + +/** 本地响应式存储的命名空间 */ +const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace; + +export { getConfig, setConfig, responsiveStorageNameSpace }; diff --git a/VideoAnalysis/WebUI/src/directives/auth/index.ts b/VideoAnalysis/WebUI/src/directives/auth/index.ts new file mode 100644 index 0000000..2fc6490 --- /dev/null +++ b/VideoAnalysis/WebUI/src/directives/auth/index.ts @@ -0,0 +1,15 @@ +import { hasAuth } from "@/router/utils"; +import type { Directive, DirectiveBinding } from "vue"; + +export const auth: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding >) { + const { value } = binding; + if (value) { + !hasAuth(value) && el.parentNode?.removeChild(el); + } else { + throw new Error( + "[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\"" + ); + } + } +}; diff --git a/VideoAnalysis/WebUI/src/directives/copy/index.ts b/VideoAnalysis/WebUI/src/directives/copy/index.ts new file mode 100644 index 0000000..b71fa19 --- /dev/null +++ b/VideoAnalysis/WebUI/src/directives/copy/index.ts @@ -0,0 +1,33 @@ +import { message } from "@/utils/message"; +import { useEventListener } from "@vueuse/core"; +import { copyTextToClipboard } from "@pureadmin/utils"; +import type { Directive, DirectiveBinding } from "vue"; + +export interface CopyEl extends HTMLElement { + copyValue: string; +} + +/** 文本复制指令(默认双击复制) */ +export const copy: Directive = { + mounted(el: CopyEl, binding: DirectiveBinding ) { + const { value } = binding; + if (value) { + el.copyValue = value; + const arg = binding.arg ?? "dblclick"; + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener(el, arg, () => { + const success = copyTextToClipboard(el.copyValue); + success + ? message("复制成功", { type: "success" }) + : message("复制失败", { type: "error" }); + }); + } else { + throw new Error( + '[Directive: copy]: need value! Like v-copy="modelValue"' + ); + } + }, + updated(el: CopyEl, binding: DirectiveBinding) { + el.copyValue = binding.value; + } +}; diff --git a/VideoAnalysis/WebUI/src/directives/index.ts b/VideoAnalysis/WebUI/src/directives/index.ts new file mode 100644 index 0000000..d01fe71 --- /dev/null +++ b/VideoAnalysis/WebUI/src/directives/index.ts @@ -0,0 +1,6 @@ +export * from "./auth"; +export * from "./copy"; +export * from "./longpress"; +export * from "./optimize"; +export * from "./perms"; +export * from "./ripple"; diff --git a/VideoAnalysis/WebUI/src/directives/longpress/index.ts b/VideoAnalysis/WebUI/src/directives/longpress/index.ts new file mode 100644 index 0000000..4eec6a2 --- /dev/null +++ b/VideoAnalysis/WebUI/src/directives/longpress/index.ts @@ -0,0 +1,63 @@ +import { useEventListener } from "@vueuse/core"; +import type { Directive, DirectiveBinding } from "vue"; +import { subBefore, subAfter, isFunction } from "@pureadmin/utils"; + +export const longpress: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding ) { + const cb = binding.value; + if (cb && isFunction(cb)) { + let timer = null; + let interTimer = null; + let num = 500; + let interNum = null; + const isInter = binding?.arg?.includes(":") ?? false; + + if (isInter) { + num = Number(subBefore(binding.arg, ":")); + interNum = Number(subAfter(binding.arg, ":")); + } else if (binding.arg) { + num = Number(binding.arg); + } + + const clear = () => { + if (timer) { + clearTimeout(timer); + timer = null; + } + if (interTimer) { + clearInterval(interTimer); + interTimer = null; + } + }; + + const onDownInter = (ev: PointerEvent) => { + ev.preventDefault(); + if (interTimer === null) { + interTimer = setInterval(() => cb(), interNum); + } + }; + + const onDown = (ev: PointerEvent) => { + clear(); + ev.preventDefault(); + if (timer === null) { + timer = isInter + ? setTimeout(() => { + cb(); + onDownInter(ev); + }, num) + : setTimeout(() => cb(), num); + } + }; + + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener(el, "pointerdown", onDown); + useEventListener(el, "pointerup", clear); + useEventListener(el, "pointerleave", clear); + } else { + throw new Error( + '[Directive: longpress]: need callback and callback must be a function! Like v-longpress="callback"' + ); + } + } +}; diff --git a/VideoAnalysis/WebUI/src/directives/optimize/index.ts b/VideoAnalysis/WebUI/src/directives/optimize/index.ts new file mode 100644 index 0000000..7b92538 --- /dev/null +++ b/VideoAnalysis/WebUI/src/directives/optimize/index.ts @@ -0,0 +1,68 @@ +import { + isArray, + throttle, + debounce, + isObject, + isFunction +} from "@pureadmin/utils"; +import { useEventListener } from "@vueuse/core"; +import type { Directive, DirectiveBinding } from "vue"; + +export interface OptimizeOptions { + /** 事件名 */ + event: string; + /** 事件触发的方法 */ + fn: (...params: any) => any; + /** 是否立即执行 */ + immediate?: boolean; + /** 防抖或节流的延迟时间(防抖默认:`200`毫秒、节流默认:`1000`毫秒) */ + timeout?: number; + /** 传递的参数 */ + params?: any; +} + +/** 防抖(v-optimize或v-optimize:debounce)、节流(v-optimize:throttle)指令 */ +export const optimize: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding ) { + const { value } = binding; + const optimizeType = binding.arg ?? "debounce"; + const type = ["debounce", "throttle"].find(t => t === optimizeType); + if (type) { + if (value && value.event && isFunction(value.fn)) { + let params = value?.params; + if (params) { + if (isArray(params) || isObject(params)) { + params = isObject(params) ? Array.of(params) : params; + } else { + throw new Error( + "[Directive: optimize]: `params` must be an array or object" + ); + } + } + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener( + el, + value.event, + type === "debounce" + ? debounce( + params ? () => value.fn(...params) : value.fn, + value?.timeout ?? 200, + value?.immediate ?? false + ) + : throttle( + params ? () => value.fn(...params) : value.fn, + value?.timeout ?? 1000 + ) + ); + } else { + throw new Error( + "[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function" + ); + } + } else { + throw new Error( + "[Directive: optimize]: only `debounce` and `throttle` are supported" + ); + } + } +}; diff --git a/VideoAnalysis/WebUI/src/directives/perms/index.ts b/VideoAnalysis/WebUI/src/directives/perms/index.ts new file mode 100644 index 0000000..073c918 --- /dev/null +++ b/VideoAnalysis/WebUI/src/directives/perms/index.ts @@ -0,0 +1,15 @@ +import { hasPerms } from "@/utils/auth"; +import type { Directive, DirectiveBinding } from "vue"; + +export const perms: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding >) { + const { value } = binding; + if (value) { + !hasPerms(value) && el.parentNode?.removeChild(el); + } else { + throw new Error( + "[Directive: perms]: need perms! Like v-perms=\"['btn.add','btn.edit']\"" + ); + } + } +}; diff --git a/VideoAnalysis/WebUI/src/directives/ripple/index.scss b/VideoAnalysis/WebUI/src/directives/ripple/index.scss new file mode 100644 index 0000000..061c82c --- /dev/null +++ b/VideoAnalysis/WebUI/src/directives/ripple/index.scss @@ -0,0 +1,48 @@ +/* stylelint-disable-next-line scss/dollar-variable-colon-space-after */ +$ripple-animation-transition-in: + transform 0.4s cubic-bezier(0, 0, 0.2, 1), + opacity 0.2s cubic-bezier(0, 0, 0.2, 1) !default; +$ripple-animation-transition-out: opacity 0.5s cubic-bezier(0, 0, 0.2, 1) !default; +$ripple-animation-visible-opacity: 0.25 !default; + +.v-ripple { + &__container { + position: absolute; + top: 0; + left: 0; + z-index: 0; + width: 100%; + height: 100%; + overflow: hidden; + pointer-events: none; + border-radius: inherit; + contain: strict; + } + + &__animation { + position: absolute; + top: 0; + left: 0; + overflow: hidden; + pointer-events: none; + background: currentcolor; + border-radius: 50%; + opacity: 0; + will-change: transform, opacity; + + &--enter { + opacity: 0; + transition: none; + } + + &--in { + opacity: $ripple-animation-visible-opacity; + transition: $ripple-animation-transition-in; + } + + &--out { + opacity: 0; + transition: $ripple-animation-transition-out; + } + } +} diff --git a/VideoAnalysis/WebUI/src/directives/ripple/index.ts b/VideoAnalysis/WebUI/src/directives/ripple/index.ts new file mode 100644 index 0000000..8aef2d1 --- /dev/null +++ b/VideoAnalysis/WebUI/src/directives/ripple/index.ts @@ -0,0 +1,229 @@ +import "./index.scss"; +import { isObject } from "@pureadmin/utils"; +import type { Directive, DirectiveBinding } from "vue"; + +export interface RippleOptions { + /** 自定义`ripple`颜色,支持`tailwindcss` */ + class?: string; + /** 是否从中心扩散 */ + center?: boolean; + circle?: boolean; +} + +export interface RippleDirectiveBinding + extends Omit { + value?: boolean | { class: string }; + modifiers: { + center?: boolean; + circle?: boolean; + }; +} + +function transform(el: HTMLElement, value: string) { + el.style.transform = value; + el.style.webkitTransform = value; +} + +const calculate = ( + e: PointerEvent, + el: HTMLElement, + value: RippleOptions = {} +) => { + const offset = el.getBoundingClientRect(); + + // 获取点击位置距离 el 的垂直和水平距离 + const localX = e.clientX - offset.left; + const localY = e.clientY - offset.top; + + let radius = 0; + let scale = 0.3; + // 计算点击位置到 el 顶点最远距离,即为圆的最大半径(勾股定理) + if (el._ripple?.circle) { + scale = 0.15; + radius = el.clientWidth / 2; + radius = value.center + ? radius + : radius + Math.sqrt((localX - radius) ** 2 + (localY - radius) ** 2) / 4; + } else { + radius = Math.sqrt(el.clientWidth ** 2 + el.clientHeight ** 2) / 2; + } + + // 中心点坐标 + const centerX = `${(el.clientWidth - radius * 2) / 2}px`; + const centerY = `${(el.clientHeight - radius * 2) / 2}px`; + + // 点击位置坐标 + const x = value.center ? centerX : `${localX - radius}px`; + const y = value.center ? centerY : `${localY - radius}px`; + + return { radius, scale, x, y, centerX, centerY }; +}; + +const ripples = { + show(e: PointerEvent, el: HTMLElement, value: RippleOptions = {}) { + if (!el?._ripple?.enabled) { + return; + } + + // 创建 ripple 元素和 ripple 父元素 + const container = document.createElement("span"); + const animation = document.createElement("span"); + + container.appendChild(animation); + container.className = "v-ripple__container"; + + if (value.class) { + container.className += ` ${value.class}`; + } + + const { radius, scale, x, y, centerX, centerY } = calculate(e, el, value); + + // ripple 圆大小 + const size = `${radius * 2}px`; + + animation.className = "v-ripple__animation"; + animation.style.width = size; + animation.style.height = size; + + el.appendChild(container); + + // 获取目标元素样式表 + const computed = window.getComputedStyle(el); + // 防止 position 被覆盖导致 ripple 位置有问题 + if (computed && computed.position === "static") { + el.style.position = "relative"; + el.dataset.previousPosition = "static"; + } + + animation.classList.add("v-ripple__animation--enter"); + animation.classList.add("v-ripple__animation--visible"); + transform( + animation, + `translate(${x}, ${y}) scale3d(${scale},${scale},${scale})` + ); + animation.dataset.activated = String(performance.now()); + + setTimeout(() => { + animation.classList.remove("v-ripple__animation--enter"); + animation.classList.add("v-ripple__animation--in"); + transform(animation, `translate(${centerX}, ${centerY}) scale3d(1,1,1)`); + }, 0); + }, + + hide(el: HTMLElement | null) { + if (!el?._ripple?.enabled) return; + + const ripples = el.getElementsByClassName("v-ripple__animation"); + + if (ripples.length === 0) return; + const animation = ripples[ripples.length - 1] as HTMLElement; + + if (animation.dataset.isHiding) return; + else animation.dataset.isHiding = "true"; + + const diff = performance.now() - Number(animation.dataset.activated); + const delay = Math.max(250 - diff, 0); + + setTimeout(() => { + animation.classList.remove("v-ripple__animation--in"); + animation.classList.add("v-ripple__animation--out"); + + setTimeout(() => { + const ripples = el.getElementsByClassName("v-ripple__animation"); + if (ripples.length === 1 && el.dataset.previousPosition) { + el.style.position = el.dataset.previousPosition; + delete el.dataset.previousPosition; + } + + if (animation.parentNode?.parentNode === el) + el.removeChild(animation.parentNode); + }, 300); + }, delay); + } +}; + +function isRippleEnabled(value: any): value is true { + return typeof value === "undefined" || !!value; +} + +function rippleShow(e: PointerEvent) { + const value: RippleOptions = {}; + const element = e.currentTarget as HTMLElement | undefined; + + if (!element?._ripple || element._ripple.touched) return; + + value.center = element._ripple.centered; + if (element._ripple.class) { + value.class = element._ripple.class; + } + + ripples.show(e, element, value); +} + +function rippleHide(e: Event) { + const element = e.currentTarget as HTMLElement | null; + if (!element?._ripple) return; + + window.setTimeout(() => { + if (element._ripple) { + element._ripple.touched = false; + } + }); + ripples.hide(element); +} + +function updateRipple( + el: HTMLElement, + binding: RippleDirectiveBinding, + wasEnabled: boolean +) { + const { value, modifiers } = binding; + const enabled = isRippleEnabled(value); + if (!enabled) { + ripples.hide(el); + } + + el._ripple = el._ripple ?? {}; + el._ripple.enabled = enabled; + el._ripple.centered = modifiers.center; + el._ripple.circle = modifiers.circle; + if (isObject(value) && value.class) { + el._ripple.class = value.class; + } + + if (enabled && !wasEnabled) { + el.addEventListener("pointerdown", rippleShow); + el.addEventListener("pointerup", rippleHide); + } else if (!enabled && wasEnabled) { + removeListeners(el); + } +} + +function removeListeners(el: HTMLElement) { + el.removeEventListener("pointerdown", rippleShow); + el.removeEventListener("pointerup", rippleHide); +} + +function mounted(el: HTMLElement, binding: RippleDirectiveBinding) { + updateRipple(el, binding, false); +} + +function unmounted(el: HTMLElement) { + delete el._ripple; + removeListeners(el); +} + +function updated(el: HTMLElement, binding: RippleDirectiveBinding) { + if (binding.value === binding.oldValue) { + return; + } + + const wasEnabled = isRippleEnabled(binding.oldValue); + updateRipple(el, binding, wasEnabled); +} + +export const Ripple: Directive = { + mounted, + unmounted, + updated +}; diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-content/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-content/index.vue new file mode 100644 index 0000000..5c7ceb9 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-content/index.vue @@ -0,0 +1,213 @@ + + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-footer/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-footer/index.vue new file mode 100644 index 0000000..b265daf --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-footer/index.vue @@ -0,0 +1,31 @@ + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-frame/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-frame/index.vue new file mode 100644 index 0000000..b2bb9d5 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-frame/index.vue @@ -0,0 +1,79 @@ + + + ++ + + + ++ + + ++ ++ ++ +++ ++ ++ + + ++ ++ ++ ++ + + ++ ++ ++ diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-navbar/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-navbar/index.vue new file mode 100644 index 0000000..787c5b1 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-navbar/index.vue @@ -0,0 +1,128 @@ + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-notice/components/NoticeItem.vue b/VideoAnalysis/WebUI/src/layout/components/lay-notice/components/NoticeItem.vue new file mode 100644 index 0000000..4608e6f --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-notice/components/NoticeItem.vue @@ -0,0 +1,177 @@ + + + ++ ++ + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-notice/components/NoticeList.vue b/VideoAnalysis/WebUI/src/layout/components/lay-notice/components/NoticeList.vue new file mode 100644 index 0000000..8617345 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-notice/components/NoticeList.vue @@ -0,0 +1,23 @@ + + + ++ ++++ ++ ++ {{ noticeItem.title }} +++ {{ noticeItem?.extra }} + ++ ++ {{ noticeItem.description }} +++ {{ noticeItem.datetime }} +++++ + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-notice/data.ts b/VideoAnalysis/WebUI/src/layout/components/lay-notice/data.ts new file mode 100644 index 0000000..5a07f4d --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-notice/data.ts @@ -0,0 +1,97 @@ +export interface ListItem { + avatar: string; + title: string; + datetime: string; + type: string; + description: string; + status?: "primary" | "success" | "warning" | "info" | "danger"; + extra?: string; +} + +export interface TabItem { + key: string; + name: string; + list: ListItem[]; + emptyText: string; +} + +export const noticesData: TabItem[] = [ + { + key: "1", + name: "通知", + list: [], + emptyText: "暂无通知" + }, + { + key: "2", + name: "消息", + list: [ + { + avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile1.svg", + title: "小铭 评论了你", + description: "诚在于心,信在于行,诚信在于心行合一。", + datetime: "今天", + type: "2" + }, + { + avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile2.svg", + title: "李白 回复了你", + description: "长风破浪会有时,直挂云帆济沧海。", + datetime: "昨天", + type: "2" + }, + { + avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile5.svg", + title: "标题", + description: + "请将鼠标移动到此处,以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2,超过2行的描述内容将被省略并且可以通过tooltip查看完整内容", + datetime: "时间", + type: "2" + } + ], + emptyText: "暂无消息" + }, + { + key: "3", + name: "待办", + list: [ + { + avatar: "", + title: "第三方紧急代码变更", + description: + "小林提交于 2024-05-10,需在 2024-05-11 前完成代码变更任务", + datetime: "", + extra: "马上到期", + status: "danger", + type: "3" + }, + { + avatar: "", + title: "版本发布", + description: "指派小铭于 2024-06-18 前完成更新并发布", + datetime: "", + extra: "已耗时 8 天", + status: "warning", + type: "3" + }, + { + avatar: "", + title: "新功能开发", + description: "开发多租户管理", + datetime: "", + extra: "进行中", + type: "3" + }, + { + avatar: "", + title: "任务名称", + description: "任务需要在 2030-10-30 10:00 前启动", + datetime: "", + extra: "未开始", + status: "info", + type: "3" + } + ], + emptyText: "暂无待办" + } +]; diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-notice/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-notice/index.vue new file mode 100644 index 0000000..d250e20 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-notice/index.vue @@ -0,0 +1,91 @@ + + + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-panel/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-panel/index.vue new file mode 100644 index 0000000..b2d5bbe --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-panel/index.vue @@ -0,0 +1,145 @@ + + + ++ + + + ++ + ++ ++ + + + + + ++ ++++ + ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchFooter.vue b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchFooter.vue new file mode 100644 index 0000000..ebac0e7 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchFooter.vue @@ -0,0 +1,61 @@ + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchHistory.vue b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchHistory.vue new file mode 100644 index 0000000..dd5875a --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchHistory.vue @@ -0,0 +1,198 @@ + + + +++++系统配置
+ ++ + + + ++ +++ 清空缓存 + ++ ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchHistoryItem.vue b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchHistoryItem.vue new file mode 100644 index 0000000..b94ccde --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchHistoryItem.vue @@ -0,0 +1,52 @@ + + + +搜索历史+++ + ++ + {{ `收藏${collectList.length > 1 ? "(可拖拽排序)" : ""}` }} ++++ ++++ + + {{ item.meta?.title }} + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchModal.vue b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchModal.vue new file mode 100644 index 0000000..af778c0 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchModal.vue @@ -0,0 +1,334 @@ + + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchResult.vue b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchResult.vue new file mode 100644 index 0000000..1dc7841 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-search/components/SearchResult.vue @@ -0,0 +1,113 @@ + + + ++ + ++ + ++ ++ ++ + + + + ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-search/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-search/index.vue new file mode 100644 index 0000000..b9bf15c --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-search/index.vue @@ -0,0 +1,21 @@ + + + ++++ + {{ item.meta?.title }} + + + + ++ diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-search/types.ts b/VideoAnalysis/WebUI/src/layout/components/lay-search/types.ts new file mode 100644 index 0000000..a39adbd --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-search/types.ts @@ -0,0 +1,20 @@ +interface optionsItem { + path: string; + type: "history" | "collect"; + meta: { + icon?: string; + title?: string; + }; +} + +interface dragItem { + oldIndex: number; + newIndex: number; +} + +interface Props { + value: string; + options: Array+ ; +} + +export type { optionsItem, dragItem, Props }; diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-setting/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-setting/index.vue new file mode 100644 index 0000000..2294667 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-setting/index.vue @@ -0,0 +1,631 @@ + + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavHorizontal.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavHorizontal.vue new file mode 100644 index 0000000..695c942 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavHorizontal.vue @@ -0,0 +1,123 @@ + + + +++整体风格
+{ + theme.index === 1 && theme.index !== 2 + ? (dataTheme = true) + : (dataTheme = false); + overallStyle = theme.option.theme; + dataThemeChange(theme.option.theme); + theme.index === 2 && watchSystemThemeChange(); + } + " + /> + + 主题色
++
+ +- +
++ ++ 导航模式
++
+ + +- + + +
+- + + +
+- + + +
+页宽
++ setStretch(value)" + /> + + + +页签风格
++ + 界面显示
++
+- + 灰色模式 +
++ - + 色弱模式 +
++ - + 隐藏标签页 +
++ - + 隐藏页脚 +
++ - + Logo +
++ - + 页签持久化 +
++ ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavMix.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavMix.vue new file mode 100644 index 0000000..d8e4977 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavMix.vue @@ -0,0 +1,143 @@ + + + ++++ {{ title }} +
+ ++ + +++ + + + + + + + + + ++ + ++ ++ 退出系统 + ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavVertical.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavVertical.vue new file mode 100644 index 0000000..0e9fa12 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/NavVertical.vue @@ -0,0 +1,137 @@ + + + ++ ++ + ++++ + + {{ route.meta.title }} + ++ ++ + +++ + + + + + + + + + ++ + ++ ++ 退出系统 + ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue new file mode 100644 index 0000000..c73d5b9 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarBreadCrumb.vue @@ -0,0 +1,120 @@ + + + ++ + ++ ++ + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue new file mode 100644 index 0000000..945a37a --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarCenterCollapse.vue @@ -0,0 +1,70 @@ + + + ++ ++ + {{ item.meta.title }} + + +++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue new file mode 100644 index 0000000..7cad16e --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue @@ -0,0 +1,20 @@ + + + ++ ++ diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue new file mode 100644 index 0000000..4d38bd0 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarFullScreen.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarItem.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarItem.vue new file mode 100644 index 0000000..351790f --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarItem.vue @@ -0,0 +1,231 @@ + + + ++ + ++ + ++ {{ onlyOneChild.meta.title }} + + + +++ ++ {{ onlyOneChild.meta.title }} + ++ + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue new file mode 100644 index 0000000..50e5125 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue @@ -0,0 +1,69 @@ + + + ++ {{ item.meta.title }} + ++ + + + ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue new file mode 100644 index 0000000..8911c12 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLinkItem.vue @@ -0,0 +1,32 @@ + + + ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLogo.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLogo.vue new file mode 100644 index 0000000..ccbc7ab --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarLogo.vue @@ -0,0 +1,72 @@ + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue new file mode 100644 index 0000000..350df33 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-tag/components/TagChrome.vue b/VideoAnalysis/WebUI/src/layout/components/lay-tag/components/TagChrome.vue new file mode 100644 index 0000000..137365b --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-tag/components/TagChrome.vue @@ -0,0 +1,33 @@ + + + diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-tag/index.scss b/VideoAnalysis/WebUI/src/layout/components/lay-tag/index.scss new file mode 100644 index 0000000..e399680 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-tag/index.scss @@ -0,0 +1,371 @@ +@keyframes schedule-in-width { + from { + width: 0; + } + + to { + width: 100%; + } +} + +@keyframes schedule-out-width { + from { + width: 100%; + } + + to { + width: 0; + } +} + +.tags-view { + position: relative; + display: flex; + align-items: center; + width: 100%; + font-size: 14px; + color: var(--el-text-color-primary); + background: #fff; + box-shadow: 0 0 1px #888; + + .scroll-item { + position: relative; + display: inline-block; + height: 34px; + padding-left: 6px; + line-height: 34px; + cursor: pointer; + transition: all 0.4s; + + &:not(:first-child) { + padding-right: 24px; + } + + &.chrome-item { + padding-right: 0; + padding-left: 0; + margin-right: -18px; + box-shadow: none; + } + + .el-icon-close { + position: absolute; + top: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + color: var(--el-color-primary); + cursor: pointer; + border-radius: 4px; + transform: translate(0, -50%); + transition: + background-color 0.12s, + color 0.12s; + + &:hover { + color: rgb(0 0 0 / 88%) !important; + background-color: rgb(0 0 0 / 6%); + } + } + } + + .tag-title { + padding: 0 4px; + color: var(--el-text-color-primary); + text-decoration: none; + } + + .scroll-container { + position: relative; + flex: 1; + overflow: hidden; + white-space: nowrap; + + &.chrome-scroll-container { + padding-top: 4px; + + .fixed-tag { + padding: 0 !important; + } + } + + .tab { + position: relative; + float: left; + overflow: visible; + white-space: nowrap; + list-style: none; + + .scroll-item { + transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); + + &:nth-child(1) { + padding: 0 12px; + } + + &.chrome-item { + &:nth-child(1) { + padding: 0; + } + } + } + + .fixed-tag { + padding: 0 12px; + } + } + } + + /* 右键菜单 */ + .contextmenu { + position: absolute; + padding: 5px 0; + margin: 0; + font-size: 13px; + font-weight: normal; + color: var(--el-text-color-primary); + white-space: nowrap; + outline: 0; + list-style-type: none; + background: #fff; + border-radius: 4px; + box-shadow: 0 2px 8px rgb(0 0 0 / 15%); + + li { + display: flex; + align-items: center; + width: 100%; + padding: 7px 12px; + margin: 0; + cursor: pointer; + + &:hover { + color: var(--el-color-primary); + } + + svg { + display: block; + margin-right: 0.5em; + } + } + } +} + +.el-dropdown-menu { + li { + display: flex; + align-items: center; + width: 100%; + margin: 0; + cursor: pointer; + + svg { + display: block; + margin-right: 0.5em; + } + } +} + +.el-dropdown-menu__item:not(.is-disabled):hover { + color: #606266; + background: #f0f0f0; +} + +:deep(.el-dropdown-menu__item) i { + margin-right: 10px; +} + +:deep(.el-dropdown-menu__item--divided) { + margin: 1px 0; +} + +.el-dropdown-menu__item--divided::before { + margin: 0; +} + +.el-dropdown-menu__item.is-disabled { + cursor: not-allowed; +} + +.scroll-item.is-active { + position: relative; + color: #fff; + box-shadow: 0 0 0.7px #888; + + .chrome-tab { + z-index: 10; + } + + .chrome-tab__bg { + color: var(--el-color-primary-light-9) !important; + } + + .tag-title { + color: var(--el-color-primary) !important; + } + + .chrome-close-btn { + color: var(--el-color-primary); + + &:hover { + background-color: var(--el-color-primary); + } + } + + .chrome-tab-divider { + opacity: 0; + } +} + +.arrow-left, +.arrow-right, +.arrow-down { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 34px; + color: var(--el-text-color-primary); + + svg { + width: 20px; + height: 20px; + } +} + +.arrow-left { + box-shadow: 5px 0 5px -6px #ccc; + + &:hover { + cursor: w-resize; + } +} + +.arrow-right { + border-right: 0.5px solid #ccc; + box-shadow: -5px 0 5px -6px #ccc; + + &:hover { + cursor: e-resize; + } +} + +/* 卡片模式下鼠标移入显示蓝色边框 */ +.card-in { + color: var(--el-color-primary); + + .tag-title { + color: var(--el-color-primary); + } +} + +/* 卡片模式下鼠标移出隐藏蓝色边框 */ +.card-out { + color: #666; + border: none; + + .tag-title { + color: #666; + } +} + +/* 灵动模式 */ +.schedule-active { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--el-color-primary); +} + +/* 灵动模式下鼠标移入显示蓝色进度条 */ +.schedule-in { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--el-color-primary); + animation: schedule-in-width 200ms ease-in; +} + +/* 灵动模式下鼠标移出隐藏蓝色进度条 */ +.schedule-out { + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: var(--el-color-primary); + animation: schedule-out-width 200ms ease-in; +} + +/* 谷歌风格的页签 */ +.chrome-tab { + position: relative; + display: inline-flex; + gap: 16px; + align-items: center; + justify-content: center; + padding: 0 24px; + white-space: nowrap; + cursor: pointer; + + .tag-title { + padding: 0; + } + + .chrome-tab-divider { + position: absolute; + right: 7px; + width: 1px; + height: 14px; + background-color: #2b2d2f; + } + + &:hover { + z-index: 10; + + .chrome-tab__bg { + color: #dee1e6; + } + + .tag-title { + color: #1f1f1f; + } + + .chrome-tab-divider { + opacity: 0; + } + } + + .chrome-tab__bg { + position: absolute; + top: 0; + left: 0; + z-index: -10; + width: 100%; + height: 100%; + color: transparent; + pointer-events: none; + } + + .chrome-close-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: #666; + border-radius: 50%; + + &:hover { + color: white; + background-color: #b1b3b8; + } + } +} diff --git a/VideoAnalysis/WebUI/src/layout/components/lay-tag/index.vue b/VideoAnalysis/WebUI/src/layout/components/lay-tag/index.vue new file mode 100644 index 0000000..a21e389 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/components/lay-tag/index.vue @@ -0,0 +1,684 @@ + + + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/frame.vue b/VideoAnalysis/WebUI/src/layout/frame.vue new file mode 100644 index 0000000..a6549f7 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/frame.vue @@ -0,0 +1,91 @@ + + + ++ + ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/hooks/useBoolean.ts b/VideoAnalysis/WebUI/src/layout/hooks/useBoolean.ts new file mode 100644 index 0000000..1d14031 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/hooks/useBoolean.ts @@ -0,0 +1,26 @@ +import { ref } from "vue"; + +export function useBoolean(initValue = false) { + const bool = ref(initValue); + + function setBool(value: boolean) { + bool.value = value; + } + function setTrue() { + setBool(true); + } + function setFalse() { + setBool(false); + } + function toggle() { + setBool(!bool.value); + } + + return { + bool, + setBool, + setTrue, + setFalse, + toggle + }; +} diff --git a/VideoAnalysis/WebUI/src/layout/hooks/useDataThemeChange.ts b/VideoAnalysis/WebUI/src/layout/hooks/useDataThemeChange.ts new file mode 100644 index 0000000..4b08dff --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/hooks/useDataThemeChange.ts @@ -0,0 +1,138 @@ +import { ref } from "vue"; +import { getConfig } from "@/config"; +import { useLayout } from "./useLayout"; +import { removeToken } from "@/utils/auth"; +import { routerArrays } from "@/layout/types"; +import { router, resetRouter } from "@/router"; +import type { themeColorsType } from "../types"; +import { useAppStoreHook } from "@/store/modules/app"; +import { useEpThemeStoreHook } from "@/store/modules/epTheme"; +import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; +import { darken, lighten, useGlobal, storageLocal } from "@pureadmin/utils"; + +export function useDataThemeChange() { + const { layoutTheme, layout } = useLayout(); + const themeColors = ref>([ + /* 亮白色 */ + { color: "#ffffff", themeColor: "light" }, + /* 道奇蓝 */ + { color: "#1b2a47", themeColor: "default" }, + /* 深紫罗兰色 */ + { color: "#722ed1", themeColor: "saucePurple" }, + /* 深粉色 */ + { color: "#eb2f96", themeColor: "pink" }, + /* 猩红色 */ + { color: "#f5222d", themeColor: "dusk" }, + /* 橙红色 */ + { color: "#fa541c", themeColor: "volcano" }, + /* 绿宝石 */ + { color: "#13c2c2", themeColor: "mingQing" }, + /* 酸橙绿 */ + { color: "#52c41a", themeColor: "auroraGreen" } + ]); + + const { $storage } = useGlobal (); + const dataTheme = ref ($storage?.layout?.darkMode); + const overallStyle = ref ($storage?.layout?.overallStyle); + const body = document.documentElement as HTMLElement; + + function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) { + const targetEl = target || document.body; + let { className } = targetEl; + className = className.replace(clsName, "").trim(); + targetEl.className = flag ? `${className} ${clsName}` : className; + } + + /** 设置导航主题色 */ + function setLayoutThemeColor( + theme = getConfig().Theme ?? "light", + isClick = true + ) { + layoutTheme.value.theme = theme; + document.documentElement.setAttribute("data-theme", theme); + // 如果非isClick,保留之前的themeColor + const storageThemeColor = $storage.layout.themeColor; + $storage.layout = { + layout: layout.value, + theme, + darkMode: dataTheme.value, + sidebarStatus: $storage.layout?.sidebarStatus, + epThemeColor: $storage.layout?.epThemeColor, + themeColor: isClick ? theme : storageThemeColor, + overallStyle: overallStyle.value + }; + + if (theme === "default" || theme === "light") { + setEpThemeColor(getConfig().EpThemeColor); + } else { + const colors = themeColors.value.find(v => v.themeColor === theme); + setEpThemeColor(colors.color); + } + } + + function setPropertyPrimary(mode: string, i: number, color: string) { + document.documentElement.style.setProperty( + `--el-color-primary-${mode}-${i}`, + dataTheme.value ? darken(color, i / 10) : lighten(color, i / 10) + ); + } + + /** 设置 `element-plus` 主题色 */ + const setEpThemeColor = (color: string) => { + useEpThemeStoreHook().setEpThemeColor(color); + document.documentElement.style.setProperty("--el-color-primary", color); + for (let i = 1; i <= 2; i++) { + setPropertyPrimary("dark", i, color); + } + for (let i = 1; i <= 9; i++) { + setPropertyPrimary("light", i, color); + } + }; + + /** 浅色、深色整体风格切换 */ + function dataThemeChange(overall?: string) { + overallStyle.value = overall; + if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) { + setLayoutThemeColor("default", false); + } else { + setLayoutThemeColor(useEpThemeStoreHook().epTheme, false); + } + + if (dataTheme.value) { + document.documentElement.classList.add("dark"); + } else { + if ($storage.layout.themeColor === "light") { + setLayoutThemeColor("light", false); + } + document.documentElement.classList.remove("dark"); + } + } + + /** 清空缓存并返回登录页 */ + function onReset() { + removeToken(); + storageLocal().clear(); + const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig(); + useAppStoreHook().setLayout(Layout); + setEpThemeColor(EpThemeColor); + useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache); + toggleClass(Grey, "html-grey", document.querySelector("html")); + toggleClass(Weak, "html-weakness", document.querySelector("html")); + router.push("/login"); + useMultiTagsStoreHook().handleTags("equal", [...routerArrays]); + resetRouter(); + } + + return { + body, + dataTheme, + overallStyle, + layoutTheme, + themeColors, + onReset, + toggleClass, + dataThemeChange, + setEpThemeColor, + setLayoutThemeColor + }; +} diff --git a/VideoAnalysis/WebUI/src/layout/hooks/useLayout.ts b/VideoAnalysis/WebUI/src/layout/hooks/useLayout.ts new file mode 100644 index 0000000..a45ea4f --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/hooks/useLayout.ts @@ -0,0 +1,58 @@ +import { computed } from "vue"; +import { routerArrays } from "../types"; +import { useGlobal } from "@pureadmin/utils"; +import { useMultiTagsStore } from "@/store/modules/multiTags"; + +export function useLayout() { + const { $storage, $config } = useGlobal (); + + const initStorage = () => { + /** 路由 */ + if ( + useMultiTagsStore().multiTagsCache && + (!$storage.tags || $storage.tags.length === 0) + ) { + $storage.tags = routerArrays; + } + /** 导航 */ + if (!$storage.layout) { + $storage.layout = { + layout: $config?.Layout ?? "vertical", + theme: $config?.Theme ?? "light", + darkMode: $config?.DarkMode ?? false, + sidebarStatus: $config?.SidebarStatus ?? true, + epThemeColor: $config?.EpThemeColor ?? "#409EFF", + themeColor: $config?.Theme ?? "light", + overallStyle: $config?.OverallStyle ?? "light" + }; + } + /** 灰色模式、色弱模式、隐藏标签页 */ + if (!$storage.configure) { + $storage.configure = { + grey: $config?.Grey ?? false, + weak: $config?.Weak ?? false, + hideTabs: $config?.HideTabs ?? false, + hideFooter: $config.HideFooter ?? true, + showLogo: $config?.ShowLogo ?? true, + showModel: $config?.ShowModel ?? "smart", + multiTagsCache: $config?.MultiTagsCache ?? false, + stretch: $config?.Stretch ?? false + }; + } + }; + + /** 清空缓存后从platform-config.json读取默认配置并赋值到storage中 */ + const layout = computed(() => { + return $storage?.layout.layout; + }); + + const layoutTheme = computed(() => { + return $storage.layout; + }); + + return { + layout, + layoutTheme, + initStorage + }; +} diff --git a/VideoAnalysis/WebUI/src/layout/hooks/useMultiFrame.ts b/VideoAnalysis/WebUI/src/layout/hooks/useMultiFrame.ts new file mode 100644 index 0000000..73a779d --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/hooks/useMultiFrame.ts @@ -0,0 +1,25 @@ +const MAP = new Map(); + +export const useMultiFrame = () => { + function setMap(path, Comp) { + MAP.set(path, Comp); + } + + function getMap(path?) { + if (path) { + return MAP.get(path); + } + return [...MAP.entries()]; + } + + function delMap(path) { + MAP.delete(path); + } + + return { + setMap, + getMap, + delMap, + MAP + }; +}; diff --git a/VideoAnalysis/WebUI/src/layout/hooks/useNav.ts b/VideoAnalysis/WebUI/src/layout/hooks/useNav.ts new file mode 100644 index 0000000..2f3e741 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/hooks/useNav.ts @@ -0,0 +1,157 @@ +import { storeToRefs } from "pinia"; +import { getConfig } from "@/config"; +import { emitter } from "@/utils/mitt"; +import Avatar from "@/assets/user.jpg"; +import { getTopMenu } from "@/router/utils"; +import { useFullscreen } from "@vueuse/core"; +import type { routeMetaType } from "../types"; +import { useRouter, useRoute } from "vue-router"; +import { router, remainingPaths } from "@/router"; +import { computed, type CSSProperties } from "vue"; +import { useAppStoreHook } from "@/store/modules/app"; +import { useUserStoreHook } from "@/store/modules/user"; +import { useGlobal, isAllEmpty } from "@pureadmin/utils"; +import { usePermissionStoreHook } from "@/store/modules/permission"; +import ExitFullscreen from "~icons/ri/fullscreen-exit-fill"; +import Fullscreen from "~icons/ri/fullscreen-fill"; + +const errorInfo = + "The current routing configuration is incorrect, please check the configuration"; + +export function useNav() { + const route = useRoute(); + const pureApp = useAppStoreHook(); + const routers = useRouter().options.routes; + const { isFullscreen, toggle } = useFullscreen(); + const { wholeMenus } = storeToRefs(usePermissionStoreHook()); + /** 平台`layout`中所有`el-tooltip`的`effect`配置,默认`light` */ + const tooltipEffect = getConfig()?.TooltipEffect ?? "light"; + + const getDivStyle = computed((): CSSProperties => { + return { + width: "100%", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + overflow: "hidden" + }; + }); + + /** 头像(如果头像为空则使用 src/assets/user.jpg ) */ + const userAvatar = computed(() => { + return isAllEmpty(useUserStoreHook()?.avatar) + ? Avatar + : useUserStoreHook()?.avatar; + }); + + /** 昵称(如果昵称为空则显示用户名) */ + const userName = computed(() => { + return isAllEmpty(useUserStoreHook()?.nickName) + ? useUserStoreHook()?.userName + : useUserStoreHook()?.nickName; + }); + + const avatarsStyle = computed(() => { + return userName.value ? { marginRight: "10px" } : ""; + }); + + const isCollapse = computed(() => { + return !pureApp.getSidebarStatus; + }); + + const device = computed(() => { + return pureApp.getDevice; + }); + + const { $storage, $config } = useGlobal (); + const layout = computed(() => { + return $storage?.layout?.layout; + }); + + const title = computed(() => { + return $config.Title; + }); + + /** 动态title */ + function changeTitle(meta: routeMetaType) { + const Title = getConfig().Title; + if (Title) document.title = `${meta.title} | ${Title}`; + else document.title = meta.title; + } + + /** 退出登录 */ + function logout() { + useUserStoreHook().logOut(); + } + + function backTopMenu() { + router.push(getTopMenu()?.path); + } + + function onPanel() { + emitter.emit("openPanel"); + } + + function toggleSideBar() { + pureApp.toggleSideBar(); + } + + function handleResize(menuRef) { + menuRef?.handleResize(); + } + + function resolvePath(route) { + if (!route.children) return console.error(errorInfo); + const httpReg = /^http(s?):\/\//; + const routeChildPath = route.children[0]?.path; + if (httpReg.test(routeChildPath)) { + return route.path + "/" + routeChildPath; + } else { + return routeChildPath; + } + } + + function menuSelect(indexPath: string) { + if (wholeMenus.value.length === 0 || isRemaining(indexPath)) return; + emitter.emit("changLayoutRoute", indexPath); + } + + /** 判断路径是否参与菜单 */ + function isRemaining(path: string) { + return remainingPaths.includes(path); + } + + /** 获取`logo` */ + function getLogo() { + return new URL("/logo.svg", import.meta.url).href; + } + + return { + route, + title, + device, + layout, + logout, + routers, + $storage, + isFullscreen, + Fullscreen, + ExitFullscreen, + toggle, + backTopMenu, + onPanel, + getDivStyle, + changeTitle, + toggleSideBar, + menuSelect, + handleResize, + resolvePath, + getLogo, + isCollapse, + pureApp, + userName, + userAvatar, + avatarsStyle, + tooltipEffect + }; +} diff --git a/VideoAnalysis/WebUI/src/layout/hooks/useTag.ts b/VideoAnalysis/WebUI/src/layout/hooks/useTag.ts new file mode 100644 index 0000000..1c2d254 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/hooks/useTag.ts @@ -0,0 +1,245 @@ +import { + ref, + unref, + computed, + reactive, + onMounted, + type CSSProperties, + getCurrentInstance +} from "vue"; +import type { tagsViewsType } from "../types"; +import { useRoute, useRouter } from "vue-router"; +import { responsiveStorageNameSpace } from "@/config"; +import { useSettingStoreHook } from "@/store/modules/settings"; +import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; +import { + isEqual, + isBoolean, + storageLocal, + toggleClass, + hasClass +} from "@pureadmin/utils"; + +import Fullscreen from "~icons/ri/fullscreen-fill"; +import CloseAllTags from "~icons/ri/subtract-line"; +import CloseOtherTags from "~icons/ri/text-spacing"; +import CloseRightTags from "~icons/ri/text-direction-l"; +import CloseLeftTags from "~icons/ri/text-direction-r"; +import RefreshRight from "~icons/ep/refresh-right"; +import Close from "~icons/ep/close"; + +export function useTags() { + const route = useRoute(); + const router = useRouter(); + const instance = getCurrentInstance(); + const pureSetting = useSettingStoreHook(); + + const buttonTop = ref(0); + const buttonLeft = ref(0); + const translateX = ref(0); + const visible = ref(false); + const activeIndex = ref(-1); + // 当前右键选中的路由信息 + const currentSelect = ref({}); + const isScrolling = ref(false); + + /** 显示模式,默认灵动模式 */ + const showModel = ref( + storageLocal().getItem ( + `${responsiveStorageNameSpace()}configure` + )?.showModel || "smart" + ); + /** 是否隐藏标签页,默认显示 */ + const showTags = + ref( + storageLocal().getItem ( + `${responsiveStorageNameSpace()}configure` + ).hideTabs + ) ?? ref("false"); + const multiTags: any = computed(() => { + return useMultiTagsStoreHook().multiTags; + }); + + const tagsViews = reactive >([ + { + icon: RefreshRight, + text: "重新加载", + divided: false, + disabled: false, + show: true + }, + { + icon: Close, + text: "关闭当前标签页", + divided: false, + disabled: multiTags.value.length > 1 ? false : true, + show: true + }, + { + icon: CloseLeftTags, + text: "关闭左侧标签页", + divided: true, + disabled: multiTags.value.length > 1 ? false : true, + show: true + }, + { + icon: CloseRightTags, + text: "关闭右侧标签页", + divided: false, + disabled: multiTags.value.length > 1 ? false : true, + show: true + }, + { + icon: CloseOtherTags, + text: "关闭其他标签页", + divided: true, + disabled: multiTags.value.length > 2 ? false : true, + show: true + }, + { + icon: CloseAllTags, + text: "关闭全部标签页", + divided: false, + disabled: multiTags.value.length > 1 ? false : true, + show: true + }, + { + icon: Fullscreen, + text: "内容区全屏", + divided: true, + disabled: false, + show: true + } + ]); + + function conditionHandle(item, previous, next) { + if (isBoolean(route?.meta?.showLink) && route?.meta?.showLink === false) { + if (Object.keys(route.query).length > 0) { + return isEqual(route.query, item.query) ? previous : next; + } else { + return isEqual(route.params, item.params) ? previous : next; + } + } else { + return route.path === item.path ? previous : next; + } + } + + const isFixedTag = computed(() => { + return item => { + return isBoolean(item?.meta?.fixedTag) && item?.meta?.fixedTag === true; + }; + }); + + const iconIsActive = computed(() => { + return (item, index) => { + if (index === 0) return; + return conditionHandle(item, true, false); + }; + }); + + const linkIsActive = computed(() => { + return item => { + return conditionHandle(item, "is-active", ""); + }; + }); + + const scheduleIsActive = computed(() => { + return item => { + return conditionHandle(item, "schedule-active", ""); + }; + }); + + const getTabStyle = computed((): CSSProperties => { + return { + transform: `translateX(${translateX.value}px)`, + transition: isScrolling.value ? "none" : "transform 0.5s ease-in-out" + }; + }); + + const getContextMenuStyle = computed((): CSSProperties => { + return { left: buttonLeft.value + "px", top: buttonTop.value + "px" }; + }); + + const closeMenu = () => { + visible.value = false; + }; + + /** 鼠标移入添加激活样式 */ + function onMouseenter(index) { + if (index) activeIndex.value = index; + if (unref(showModel) === "smart") { + if (hasClass(instance.refs["schedule" + index][0], "schedule-active")) + return; + toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]); + toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]); + } else { + if (hasClass(instance.refs["dynamic" + index][0], "is-active")) return; + toggleClass(true, "card-in", instance.refs["dynamic" + index][0]); + toggleClass(false, "card-out", instance.refs["dynamic" + index][0]); + } + } + + /** 鼠标移出恢复默认样式 */ + function onMouseleave(index) { + activeIndex.value = -1; + if (unref(showModel) === "smart") { + if (hasClass(instance.refs["schedule" + index][0], "schedule-active")) + return; + toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]); + toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]); + } else { + if (hasClass(instance.refs["dynamic" + index][0], "is-active")) return; + toggleClass(false, "card-in", instance.refs["dynamic" + index][0]); + toggleClass(true, "card-out", instance.refs["dynamic" + index][0]); + } + } + + function onContentFullScreen() { + pureSetting.hiddenSideBar + ? pureSetting.changeSetting({ key: "hiddenSideBar", value: false }) + : pureSetting.changeSetting({ key: "hiddenSideBar", value: true }); + } + + onMounted(() => { + if (!showModel.value) { + const configure = storageLocal().getItem ( + `${responsiveStorageNameSpace()}configure` + ); + configure.showModel = "card"; + storageLocal().setItem( + `${responsiveStorageNameSpace()}configure`, + configure + ); + } + }); + + return { + Close, + route, + router, + visible, + showTags, + instance, + multiTags, + showModel, + tagsViews, + buttonTop, + buttonLeft, + translateX, + isFixedTag, + pureSetting, + activeIndex, + getTabStyle, + isScrolling, + iconIsActive, + linkIsActive, + currentSelect, + scheduleIsActive, + getContextMenuStyle, + closeMenu, + onMounted, + onMouseenter, + onMouseleave, + onContentFullScreen + }; +} diff --git a/VideoAnalysis/WebUI/src/layout/index.vue b/VideoAnalysis/WebUI/src/layout/index.vue new file mode 100644 index 0000000..c559d9e --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/index.vue @@ -0,0 +1,235 @@ + + + + + ++ + + diff --git a/VideoAnalysis/WebUI/src/layout/redirect.vue b/VideoAnalysis/WebUI/src/layout/redirect.vue new file mode 100644 index 0000000..6e16339 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/redirect.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/VideoAnalysis/WebUI/src/layout/types.ts b/VideoAnalysis/WebUI/src/layout/types.ts new file mode 100644 index 0000000..32da0c1 --- /dev/null +++ b/VideoAnalysis/WebUI/src/layout/types.ts @@ -0,0 +1,92 @@ +import type { FunctionalComponent } from "vue"; +const { VITE_HIDE_HOME } = import.meta.env; + +export const routerArrays: Array+ ++ ++++ + + + ++ ++ + + + + = + VITE_HIDE_HOME === "false" + ? [ + { + path: "/welcome", + meta: { + title: "首页", + icon: "ep/home-filled" + } + } + ] + : []; + +export type routeMetaType = { + title?: string; + icon?: string | FunctionalComponent; + showLink?: boolean; + savedPosition?: boolean; + auths?: Array ; +}; + +export type RouteConfigs = { + path?: string; + query?: object; + params?: object; + meta?: routeMetaType; + children?: RouteConfigs[]; + name?: string; +}; + +export type multiTagsType = { + tags: Array ; +}; + +export type tagsViewsType = { + icon: string | FunctionalComponent; + text: string; + divided: boolean; + disabled: boolean; + show: boolean; +}; + +export interface setType { + sidebar: { + opened: boolean; + withoutAnimation: boolean; + isClickCollapse: boolean; + }; + device: string; + fixedHeader: boolean; + classes: { + hideSidebar: boolean; + openSidebar: boolean; + withoutAnimation: boolean; + mobile: boolean; + }; + hideTabs: boolean; +} + +export type menuType = { + id?: number; + name?: string; + path?: string; + noShowingChildren?: boolean; + children?: menuType[]; + value: unknown; + meta?: { + icon?: string; + title?: string; + rank?: number; + showParent?: boolean; + extraIcon?: string; + }; + showTooltip?: boolean; + parentId?: number; + pathList?: number[]; + redirect?: string; +}; + +export type themeColorsType = { + color: string; + themeColor: string; +}; + +export interface scrollbarDomType extends HTMLElement { + wrap?: { + offsetWidth: number; + }; +} diff --git a/VideoAnalysis/WebUI/src/main.ts b/VideoAnalysis/WebUI/src/main.ts new file mode 100644 index 0000000..d603e32 --- /dev/null +++ b/VideoAnalysis/WebUI/src/main.ts @@ -0,0 +1,64 @@ +import App from "./App.vue"; +import router from "./router"; +import { setupStore } from "@/store"; +import { getPlatformConfig } from "./config"; +import { MotionPlugin } from "@vueuse/motion"; +// import { useEcharts } from "@/plugins/echarts"; +import { createApp, type Directive } from "vue"; +import { useElementPlus } from "@/plugins/elementPlus"; +import { injectResponsiveStorage } from "@/utils/responsive"; + +import Table from "@pureadmin/table"; +// import PureDescriptions from "@pureadmin/descriptions"; + +// 引入重置样式 +import "./style/reset.scss"; +// 导入公共样式 +import "./style/index.scss"; +// 一定要在main.ts中导入tailwind.css,防止vite每次hmr都会请求src/style/index.scss整体css文件导致热更新慢的问题 +import "./style/tailwind.css"; +import "element-plus/dist/index.css"; +// 导入字体图标 +import "./assets/iconfont/iconfont.js"; +import "./assets/iconfont/iconfont.css"; + +const app = createApp(App); + +// 自定义指令 +import * as directives from "@/directives"; +Object.keys(directives).forEach(key => { + app.directive(key, (directives as { [key: string]: Directive })[key]); +}); + +// 全局注册@iconify/vue图标库 +import { + IconifyIconOffline, + IconifyIconOnline, + FontIcon +} from "./components/ReIcon"; +app.component("IconifyIconOffline", IconifyIconOffline); +app.component("IconifyIconOnline", IconifyIconOnline); +app.component("FontIcon", FontIcon); + +// 全局注册按钮级别权限组件 +import { Auth } from "@/components/ReAuth"; +import { Perms } from "@/components/RePerms"; +app.component("Auth", Auth); +app.component("Perms", Perms); + +// 全局注册vue-tippy +import "tippy.js/dist/tippy.css"; +import "tippy.js/themes/light.css"; +import VueTippy from "vue-tippy"; +app.use(VueTippy); + +getPlatformConfig(app).then(async config => { + setupStore(app); + app.use(router); + await router.isReady(); + injectResponsiveStorage(app, config); + app.use(MotionPlugin).use(useElementPlus).use(Table); + // .use(PureDescriptions) + // .use(useEcharts); + app.mount("#app"); +}); diff --git a/VideoAnalysis/WebUI/src/plugins/echarts.ts b/VideoAnalysis/WebUI/src/plugins/echarts.ts new file mode 100644 index 0000000..cb62d96 --- /dev/null +++ b/VideoAnalysis/WebUI/src/plugins/echarts.ts @@ -0,0 +1,44 @@ +import type { App } from "vue"; +import * as echarts from "echarts/core"; +import { PieChart, BarChart, LineChart } from "echarts/charts"; +import { CanvasRenderer, SVGRenderer } from "echarts/renderers"; +import { + GridComponent, + TitleComponent, + PolarComponent, + LegendComponent, + GraphicComponent, + ToolboxComponent, + TooltipComponent, + DataZoomComponent, + VisualMapComponent +} from "echarts/components"; + +const { use } = echarts; + +use([ + PieChart, + BarChart, + LineChart, + CanvasRenderer, + SVGRenderer, + GridComponent, + TitleComponent, + PolarComponent, + LegendComponent, + GraphicComponent, + ToolboxComponent, + TooltipComponent, + DataZoomComponent, + VisualMapComponent +]); + +/** + * @description 按需引入echarts,具体看 https://echarts.apache.org/handbook/zh/basics/import/#%E5%9C%A8-typescript-%E4%B8%AD%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5 + * @see 温馨提示:必须将 `$echarts` 添加到全局 `globalProperties` ,具体看 https://pure-admin-utils.netlify.app/hooks/useECharts/useECharts#%E4%BD%BF%E7%94%A8%E5%89%8D%E6%8F%90 + */ +export function useEcharts(app: App) { + app.config.globalProperties.$echarts = echarts; +} + +export default echarts; diff --git a/VideoAnalysis/WebUI/src/plugins/elementPlus.ts b/VideoAnalysis/WebUI/src/plugins/elementPlus.ts new file mode 100644 index 0000000..8363187 --- /dev/null +++ b/VideoAnalysis/WebUI/src/plugins/elementPlus.ts @@ -0,0 +1,248 @@ +// 按需引入element-plus(该方法稳定且明确。当然也支持:https://element-plus.org/zh-CN/guide/quickstart.html#%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5) +import type { App, Component } from "vue"; +import { + /** + * 为了方便演示平台将 element-plus 导出的所有组件引入,实际使用中如果你没用到哪个组件,将其注释掉就行 + * 导出来源:https://github.com/element-plus/element-plus/blob/dev/packages/element-plus/component.ts#L111-L211 + * */ + ElAffix, + ElAlert, + ElAutocomplete, + ElAutoResizer, + ElAvatar, + ElAnchor, + ElAnchorLink, + ElBacktop, + ElBadge, + ElBreadcrumb, + ElBreadcrumbItem, + ElButton, + ElButtonGroup, + ElCalendar, + ElCard, + ElCarousel, + ElCarouselItem, + ElCascader, + ElCascaderPanel, + ElCheckTag, + ElCheckbox, + ElCheckboxButton, + ElCheckboxGroup, + ElCol, + ElCollapse, + ElCollapseItem, + ElCollapseTransition, + ElColorPicker, + ElConfigProvider, + ElContainer, + ElAside, + ElFooter, + ElHeader, + ElMain, + ElDatePicker, + ElDescriptions, + ElDescriptionsItem, + ElDialog, + ElDivider, + ElDrawer, + ElDropdown, + ElDropdownItem, + ElDropdownMenu, + ElEmpty, + ElForm, + ElFormItem, + ElIcon, + ElImage, + ElImageViewer, + ElInput, + ElInputNumber, + ElLink, + ElMenu, + ElMenuItem, + ElMenuItemGroup, + ElSubMenu, + ElPageHeader, + ElPagination, + ElPopconfirm, + ElPopover, + ElPopper, + ElProgress, + ElRadio, + ElRadioButton, + ElRadioGroup, + ElRate, + ElResult, + ElRow, + ElScrollbar, + ElSelect, + ElOption, + ElOptionGroup, + ElSelectV2, + ElSkeleton, + ElSkeletonItem, + ElSlider, + ElSpace, + ElStatistic, + ElCountdown, + ElSteps, + ElStep, + ElSwitch, + ElTable, + ElTableColumn, + ElTableV2, + ElTabs, + ElTabPane, + ElTag, + ElText, + ElTimePicker, + ElTimeSelect, + ElTimeline, + ElTimelineItem, + ElTooltip, + ElTransfer, + ElTree, + ElTreeSelect, + ElTreeV2, + ElUpload, + ElWatermark, + ElTour, + ElTourStep, + ElSegmented, + /** + * 为了方便演示平台将 element-plus 导出的所有插件引入,实际使用中如果你没用到哪个插件,将其注释掉就行 + * 导出来源:https://github.com/element-plus/element-plus/blob/dev/packages/element-plus/plugin.ts#L11-L16 + * */ + ElLoading, // v-loading 指令 + ElInfiniteScroll, // v-infinite-scroll 指令 + ElPopoverDirective, // v-popover 指令 + ElMessage, // $message 全局属性对象globalProperties + ElMessageBox, // $msgbox、$alert、$confirm、$prompt 全局属性对象globalProperties + ElNotification // $notify 全局属性对象globalProperties +} from "element-plus"; + +const components = [ + ElAffix, + ElAlert, + ElAutocomplete, + ElAutoResizer, + ElAvatar, + ElAnchor, + ElAnchorLink, + ElBacktop, + ElBadge, + ElBreadcrumb, + ElBreadcrumbItem, + ElButton, + ElButtonGroup, + ElCalendar, + ElCard, + ElCarousel, + ElCarouselItem, + ElCascader, + ElCascaderPanel, + ElCheckTag, + ElCheckbox, + ElCheckboxButton, + ElCheckboxGroup, + ElCol, + ElCollapse, + ElCollapseItem, + ElCollapseTransition, + ElColorPicker, + ElConfigProvider, + ElContainer, + ElAside, + ElFooter, + ElHeader, + ElMain, + ElDatePicker, + ElDescriptions, + ElDescriptionsItem, + ElDialog, + ElDivider, + ElDrawer, + ElDropdown, + ElDropdownItem, + ElDropdownMenu, + ElEmpty, + ElForm, + ElFormItem, + ElIcon, + ElImage, + ElImageViewer, + ElInput, + ElInputNumber, + ElLink, + ElMenu, + ElMenuItem, + ElMenuItemGroup, + ElSubMenu, + ElPageHeader, + ElPagination, + ElPopconfirm, + ElPopover, + ElPopper, + ElProgress, + ElRadio, + ElRadioButton, + ElRadioGroup, + ElRate, + ElResult, + ElRow, + ElScrollbar, + ElSelect, + ElOption, + ElOptionGroup, + ElSelectV2, + ElSkeleton, + ElSkeletonItem, + ElSlider, + ElSpace, + ElStatistic, + ElCountdown, + ElSteps, + ElStep, + ElSwitch, + ElTable, + ElTableColumn, + ElTableV2, + ElTabs, + ElTabPane, + ElTag, + ElText, + ElTimePicker, + ElTimeSelect, + ElTimeline, + ElTimelineItem, + ElTooltip, + ElTransfer, + ElTree, + ElTreeSelect, + ElTreeV2, + ElUpload, + ElWatermark, + ElTour, + ElTourStep, + ElSegmented +]; + +const plugins = [ + ElLoading, + ElInfiniteScroll, + ElPopoverDirective, + ElMessage, + ElMessageBox, + ElNotification +]; + +/** 按需引入`element-plus` */ +export function useElementPlus(app: App) { + // 全局注册组件 + components.forEach((component: Component) => { + app.component(component.name, component); + }); + // 全局注册插件 + plugins.forEach(plugin => { + app.use(plugin); + }); +} diff --git a/VideoAnalysis/WebUI/src/router/index.ts b/VideoAnalysis/WebUI/src/router/index.ts new file mode 100644 index 0000000..0fd3dcb --- /dev/null +++ b/VideoAnalysis/WebUI/src/router/index.ts @@ -0,0 +1,215 @@ +// import "@/utils/sso"; +import Cookies from "js-cookie"; +import { getConfig } from "@/config"; +import NProgress from "@/utils/progress"; +import { buildHierarchyTree } from "@/utils/tree"; +import remainingRouter from "./modules/remaining"; +import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; +import { usePermissionStoreHook } from "@/store/modules/permission"; +import { + isUrl, + openLink, + cloneDeep, + isAllEmpty, + storageLocal +} from "@pureadmin/utils"; +import { + ascending, + getTopMenu, + initRouter, + isOneOfArray, + getHistoryMode, + findRouteByPath, + handleAliveRoute, + formatTwoStageRoutes, + formatFlatteningRoutes +} from "./utils"; +import { + type Router, + type RouteRecordRaw, + type RouteComponent, + createRouter +} from "vue-router"; +import { + type DataInfo, + userKey, + removeToken, + multipleTabsKey +} from "@/utils/auth"; + +/** 自动导入全部静态路由,无需再手动引入!匹配 src/router/modules 目录(任何嵌套级别)中具有 .ts 扩展名的所有文件,除了 remaining.ts 文件 + * 如何匹配所有文件请看:https://github.com/mrmlnc/fast-glob#basic-syntax + * 如何排除文件请看:https://cn.vitejs.dev/guide/features.html#negative-patterns + */ +const modules: Record = import.meta.glob( + ["./modules/**/*.ts", "!./modules/**/remaining.ts"], + { + eager: true + } +); + +/** 原始静态路由(未做任何处理) */ +const routes = []; + +Object.keys(modules).forEach(key => { + routes.push(modules[key].default); +}); + +/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */ +export const constantRoutes: Array = formatTwoStageRoutes( + formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity)))) +); + +/** 初始的静态路由,用于退出登录时重置路由 */ +const initConstantRoutes: Array = cloneDeep(constantRoutes); + +/** 用于渲染菜单,保持原始层级 */ +export const constantMenus: Array = ascending( + routes.flat(Infinity) +).concat(...remainingRouter); + +/** 不参与菜单的路由 */ +export const remainingPaths = Object.keys(remainingRouter).map(v => { + return remainingRouter[v].path; +}); + +/** 创建路由实例 */ +export const router: Router = createRouter({ + history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY), + routes: constantRoutes.concat(...(remainingRouter as any)), + strict: true, + scrollBehavior(to, from, savedPosition) { + return new Promise(resolve => { + if (savedPosition) { + return savedPosition; + } else { + if (from.meta.saveSrollTop) { + const top: number = + document.documentElement.scrollTop || document.body.scrollTop; + resolve({ left: 0, top }); + } + } + }); + } +}); + +/** 重置路由 */ +export function resetRouter() { + router.clearRoutes(); + for (const route of initConstantRoutes.concat(...(remainingRouter as any))) { + router.addRoute(route); + } + router.options.routes = formatTwoStageRoutes( + formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity)))) + ); + usePermissionStoreHook().clearAllCachePage(); +} + +/** 路由白名单 */ +const whiteList = ["/login"]; + +const { VITE_HIDE_HOME } = import.meta.env; + +router.beforeEach((to: ToRouteType, _from, next) => { + if (to.meta?.keepAlive) { + handleAliveRoute(to, "add"); + // 页面整体刷新和点击标签页刷新 + if (_from.name === undefined || _from.name === "Redirect") { + handleAliveRoute(to); + } + } + const userInfo = storageLocal().getItem >(userKey); + NProgress.start(); + const externalLink = isUrl(to?.name as string); + if (!externalLink) { + to.matched.some(item => { + if (!item.meta.title) return ""; + const Title = getConfig().Title; + if (Title) document.title = `${item.meta.title} | ${Title}`; + else document.title = item.meta.title as string; + }); + } + /** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */ + function toCorrectRoute() { + whiteList.includes(to.fullPath) ? next(_from.fullPath) : next(); + } + if (Cookies.get(multipleTabsKey) && userInfo) { + // 无权限跳转403页面 + if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) { + next({ path: "/error/403" }); + } + // 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面 + if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") { + next({ path: "/error/404" }); + } + if (_from?.name) { + // name为超链接 + if (externalLink) { + openLink(to?.name as string); + NProgress.done(); + } else { + toCorrectRoute(); + } + } else { + // 刷新 + if ( + usePermissionStoreHook().wholeMenus.length === 0 && + to.path !== "/login" + ) { + initRouter().then((router: Router) => { + if (!useMultiTagsStoreHook().getMultiTagsCache) { + const { path } = to; + const route = findRouteByPath( + path, + router.options.routes[0].children + ); + getTopMenu(true); + // query、params模式路由传参数的标签页不在此处处理 + if (route && route.meta?.title) { + if ( + isAllEmpty(route.parentId) && + route.meta?.backstage && + route.children + ) { + // 此处为动态顶级路由(目录) + const { path, name, meta } = route.children[0]; + useMultiTagsStoreHook().handleTags("push", { + path, + name, + meta + }); + } else { + const { path, name, meta } = route; + useMultiTagsStoreHook().handleTags("push", { + path, + name, + meta + }); + } + } + } + // 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次) + if (isAllEmpty(to.name)) router.push(to.fullPath); + }); + } + toCorrectRoute(); + } + } else { + if (to.path !== "/login") { + if (whiteList.indexOf(to.path) !== -1) { + next(); + } else { + removeToken(); + next({ path: "/login" }); + } + } else { + next(); + } + } +}); + +router.afterEach(() => { + NProgress.done(); +}); + +export default router; diff --git a/VideoAnalysis/WebUI/src/router/modules/error.ts b/VideoAnalysis/WebUI/src/router/modules/error.ts new file mode 100644 index 0000000..ced8248 --- /dev/null +++ b/VideoAnalysis/WebUI/src/router/modules/error.ts @@ -0,0 +1,37 @@ +export default { + path: "/error", + redirect: "/error/403", + meta: { + icon: "ri/information-line", + // showLink: false, + title: "异常页面", + rank: 9, + showLink: false + }, + children: [ + { + path: "/error/403", + name: "403", + component: () => import("@/views/error/403.vue"), + meta: { + title: "403" + } + }, + { + path: "/error/404", + name: "404", + component: () => import("@/views/error/404.vue"), + meta: { + title: "404" + } + }, + { + path: "/error/500", + name: "500", + component: () => import("@/views/error/500.vue"), + meta: { + title: "500" + } + } + ] +} satisfies RouteConfigsTable; diff --git a/VideoAnalysis/WebUI/src/router/modules/home.ts b/VideoAnalysis/WebUI/src/router/modules/home.ts new file mode 100644 index 0000000..f67d017 --- /dev/null +++ b/VideoAnalysis/WebUI/src/router/modules/home.ts @@ -0,0 +1,34 @@ +const { VITE_HIDE_HOME } = import.meta.env; +const Layout = () => import("@/layout/index.vue"); + +export default { + path: "/", + name: "Home", + component: Layout, + redirect: "/welcome", + meta: { + icon: "ep/home-filled", + title: "首页", + rank: 0 + }, + children: [ + { + path: "/welcome", + name: "Welcome", + component: () => import("@/views/welcome/index.vue"), + meta: { + title: "任务列表", + showLink: VITE_HIDE_HOME === "true" ? false : true + } + }, + { + path: "/welcome/showTask", + name: "ShowTask", + component: () => import("@/views/welcome/showTask.vue"), + meta: { + title: "进行中任务", + showLink: VITE_HIDE_HOME === "true" ? false : true + } + } + ] +} satisfies RouteConfigsTable; diff --git a/VideoAnalysis/WebUI/src/router/modules/remaining.ts b/VideoAnalysis/WebUI/src/router/modules/remaining.ts new file mode 100644 index 0000000..a1390dc --- /dev/null +++ b/VideoAnalysis/WebUI/src/router/modules/remaining.ts @@ -0,0 +1,30 @@ +const Layout = () => import("@/layout/index.vue"); + +export default [ + { + path: "/login", + name: "Login", + component: () => import("@/views/login/index.vue"), + meta: { + title: "登录", + showLink: false, + rank: 101 + } + }, + { + path: "/redirect", + component: Layout, + meta: { + title: "加载中...", + showLink: false, + rank: 102 + }, + children: [ + { + path: "/redirect/:path(.*)", + name: "Redirect", + component: () => import("@/layout/redirect.vue") + } + ] + } +] satisfies Array ; diff --git a/VideoAnalysis/WebUI/src/router/utils.ts b/VideoAnalysis/WebUI/src/router/utils.ts new file mode 100644 index 0000000..2010a40 --- /dev/null +++ b/VideoAnalysis/WebUI/src/router/utils.ts @@ -0,0 +1,409 @@ +import { + type RouterHistory, + type RouteRecordRaw, + type RouteComponent, + createWebHistory, + createWebHashHistory +} from "vue-router"; +import { router } from "./index"; +import { isProxy, toRaw } from "vue"; +import { useTimeoutFn } from "@vueuse/core"; +import { + isString, + cloneDeep, + isAllEmpty, + intersection, + storageLocal, + isIncludeAllChildren +} from "@pureadmin/utils"; +import { getConfig } from "@/config"; +import { buildHierarchyTree } from "@/utils/tree"; +import { userKey, type DataInfo } from "@/utils/auth"; +import { type menuType, routerArrays } from "@/layout/types"; +import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; +import { usePermissionStoreHook } from "@/store/modules/permission"; +const IFrame = () => import("@/layout/frame.vue"); +// https://cn.vitejs.dev/guide/features.html#glob-import +const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}"); + +// 动态路由 +import { getAsyncRoutes } from "@/api/routes"; +function handRank(routeInfo: any) { + const { name, path, parentId, meta } = routeInfo; + return isAllEmpty(parentId) + ? isAllEmpty(meta?.rank) || + (meta?.rank === 0 && name !== "Home" && path !== "/") + ? true + : false + : false; +} + +/** 按照路由中meta下的rank等级升序来排序路由 */ +function ascending(arr: any[]) { + arr.forEach((v, index) => { + // 当rank不存在时,根据顺序自动创建,首页路由永远在第一位 + if (handRank(v)) v.meta.rank = index + 2; + }); + return arr.sort( + (a: { meta: { rank: number } }, b: { meta: { rank: number } }) => { + return a?.meta.rank - b?.meta.rank; + } + ); +} + +/** 过滤meta中showLink为false的菜单 */ +function filterTree(data: RouteComponent[]) { + const newTree = cloneDeep(data).filter( + (v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false + ); + newTree.forEach( + (v: { children }) => v.children && (v.children = filterTree(v.children)) + ); + return newTree; +} + +/** 过滤children长度为0的的目录,当目录下没有菜单时,会过滤此目录,目录没有赋予roles权限,当目录下只要有一个菜单有显示权限,那么此目录就会显示 */ +function filterChildrenTree(data: RouteComponent[]) { + const newTree = cloneDeep(data).filter((v: any) => v?.children?.length !== 0); + newTree.forEach( + (v: { children }) => v.children && (v.children = filterTree(v.children)) + ); + return newTree; +} + +/** 判断两个数组彼此是否存在相同值 */ +function isOneOfArray(a: Array , b: Array ) { + return Array.isArray(a) && Array.isArray(b) + ? intersection(a, b).length > 0 + ? true + : false + : true; +} + +/** 从localStorage里取出当前登录用户的角色roles,过滤无权限的菜单 */ +function filterNoPermissionTree(data: RouteComponent[]) { + const currentRoles = + storageLocal().getItem >(userKey)?.roles ?? []; + const newTree = cloneDeep(data).filter((v: any) => + isOneOfArray(v.meta?.roles, currentRoles) + ); + newTree.forEach( + (v: any) => v.children && (v.children = filterNoPermissionTree(v.children)) + ); + return filterChildrenTree(newTree); +} + +/** 通过指定 `key` 获取父级路径集合,默认 `key` 为 `path` */ +function getParentPaths(value: string, routes: RouteRecordRaw[], key = "path") { + // 深度遍历查找 + function dfs(routes: RouteRecordRaw[], value: string, parents: string[]) { + for (let i = 0; i < routes.length; i++) { + const item = routes[i]; + // 返回父级path + if (item[key] === value) return parents; + // children不存在或为空则不递归 + if (!item.children || !item.children.length) continue; + // 往下查找时将当前path入栈 + parents.push(item.path); + + if (dfs(item.children, value, parents).length) return parents; + // 深度遍历查找未找到时当前path 出栈 + parents.pop(); + } + // 未找到时返回空数组 + return []; + } + + return dfs(routes, value, []); +} + +/** 查找对应 `path` 的路由信息 */ +function findRouteByPath(path: string, routes: RouteRecordRaw[]) { + let res = routes.find((item: { path: string }) => item.path == path); + if (res) { + return isProxy(res) ? toRaw(res) : res; + } else { + for (let i = 0; i < routes.length; i++) { + if ( + routes[i].children instanceof Array && + routes[i].children.length > 0 + ) { + res = findRouteByPath(path, routes[i].children); + if (res) { + return isProxy(res) ? toRaw(res) : res; + } + } + } + return null; + } +} + +function addPathMatch() { + if (!router.hasRoute("pathMatch")) { + router.addRoute({ + path: "/:pathMatch(.*)", + name: "pathMatch", + redirect: "/error/404" + }); + } +} + +/** 处理动态路由(后端返回的路由) */ +function handleAsyncRoutes(routeList) { + if (routeList == null ||routeList.length === 0) { + usePermissionStoreHook().handleWholeMenus(routeList); + } else { + formatFlatteningRoutes(addAsyncRoutes(routeList)).map( + (v: RouteRecordRaw) => { + // 防止重复添加路由 + if ( + router.options.routes[0].children.findIndex( + value => value.path === v.path + ) !== -1 + ) { + return; + } else { + // 切记将路由push到routes后还需要使用addRoute,这样路由才能正常跳转 + router.options.routes[0].children.push(v); + // 最终路由进行升序 + ascending(router.options.routes[0].children); + if (!router.hasRoute(v?.name)) router.addRoute(v); + const flattenRouters: any = router + .getRoutes() + .find(n => n.path === "/"); + // 保持router.options.routes[0].children与path为"/"的children一致,防止数据不一致导致异常 + flattenRouters.children = router.options.routes[0].children; + router.addRoute(flattenRouters); + } + } + ); + usePermissionStoreHook().handleWholeMenus(routeList); + } + if (!useMultiTagsStoreHook().getMultiTagsCache) { + useMultiTagsStoreHook().handleTags("equal", [ + ...routerArrays, + ...usePermissionStoreHook().flatteningRoutes.filter( + v => v?.meta?.fixedTag + ) + ]); + } + addPathMatch(); +} + +/** 初始化路由(`new Promise` 写法防止在异步请求中造成无限循环)*/ +function initRouter() { + if (getConfig()?.CachingAsyncRoutes) { + // 开启动态路由缓存本地localStorage + const key = "async-routes"; + const asyncRouteList = storageLocal().getItem(key) as any; + if (asyncRouteList && asyncRouteList?.length > 0) { + return new Promise(resolve => { + handleAsyncRoutes(asyncRouteList); + resolve(router); + }); + } else { + return new Promise(resolve => { + getAsyncRoutes().then(({ data }) => { + handleAsyncRoutes(cloneDeep(data)); + storageLocal().setItem(key, data); + resolve(router); + }); + }); + } + } else { + return new Promise(resolve => { + getAsyncRoutes().then(({ data }) => { + handleAsyncRoutes(cloneDeep(data)); + resolve(router); + }); + }); + } +} + +/** + * 将多级嵌套路由处理成一维数组 + * @param routesList 传入路由 + * @returns 返回处理后的一维路由 + */ +function formatFlatteningRoutes(routesList: RouteRecordRaw[]) { + if (routesList.length === 0) return routesList; + let hierarchyList = buildHierarchyTree(routesList); + for (let i = 0; i < hierarchyList.length; i++) { + if (hierarchyList[i].children) { + hierarchyList = hierarchyList + .slice(0, i + 1) + .concat(hierarchyList[i].children, hierarchyList.slice(i + 1)); + } + } + return hierarchyList; +} + +/** + * 一维数组处理成多级嵌套数组(三级及以上的路由全部拍成二级,keep-alive 只支持到二级缓存) + * https://github.com/pure-admin/vue-pure-admin/issues/67 + * @param routesList 处理后的一维路由菜单数组 + * @returns 返回将一维数组重新处理成规定路由的格式 + */ +function formatTwoStageRoutes(routesList: RouteRecordRaw[]) { + if (routesList.length === 0) return routesList; + const newRoutesList: RouteRecordRaw[] = []; + routesList.forEach((v: RouteRecordRaw) => { + if (v.path === "/") { + newRoutesList.push({ + component: v.component, + name: v.name, + path: v.path, + redirect: v.redirect, + meta: v.meta, + children: [] + }); + } else { + newRoutesList[0]?.children.push({ ...v }); + } + }); + return newRoutesList; +} + +/** 处理缓存路由(添加、删除、刷新) */ +function handleAliveRoute({ name }: ToRouteType, mode?: string) { + switch (mode) { + case "add": + usePermissionStoreHook().cacheOperate({ + mode: "add", + name + }); + break; + case "delete": + usePermissionStoreHook().cacheOperate({ + mode: "delete", + name + }); + break; + case "refresh": + usePermissionStoreHook().cacheOperate({ + mode: "refresh", + name + }); + break; + default: + usePermissionStoreHook().cacheOperate({ + mode: "delete", + name + }); + useTimeoutFn(() => { + usePermissionStoreHook().cacheOperate({ + mode: "add", + name + }); + }, 100); + } +} + +/** 过滤后端传来的动态路由 重新生成规范路由 */ +function addAsyncRoutes(arrRoutes: Array ) { + if (!arrRoutes || !arrRoutes.length) return; + const modulesRoutesKeys = Object.keys(modulesRoutes); + arrRoutes.forEach((v: RouteRecordRaw) => { + // 将backstage属性加入meta,标识此路由为后端返回路由 + v.meta.backstage = true; + // 父级的redirect属性取值:如果子级存在且父级的redirect属性不存在,默认取第一个子级的path;如果子级存在且父级的redirect属性存在,取存在的redirect属性,会覆盖默认值 + if (v?.children && v.children.length && !v.redirect) + v.redirect = v.children[0].path; + // 父级的name属性取值:如果子级存在且父级的name属性不存在,默认取第一个子级的name;如果子级存在且父级的name属性存在,取存在的name属性,会覆盖默认值(注意:测试中发现父级的name不能和子级name重复,如果重复会造成重定向无效(跳转404),所以这里给父级的name起名的时候后面会自动加上`Parent`,避免重复) + if (v?.children && v.children.length && !v.name) + v.name = (v.children[0].name as string) + "Parent"; + if (v.meta?.frameSrc) { + v.component = IFrame; + } else { + // 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会跟path保持一致) + const index = v?.component + ? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any)) + : modulesRoutesKeys.findIndex(ev => ev.includes(v.path)); + v.component = modulesRoutes[modulesRoutesKeys[index]]; + } + if (v?.children && v.children.length) { + addAsyncRoutes(v.children); + } + }); + return arrRoutes; +} + +/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */ +function getHistoryMode(routerHistory): RouterHistory { + // len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1 + const historyMode = routerHistory.split(","); + const leftMode = historyMode[0]; + const rightMode = historyMode[1]; + // no param + if (historyMode.length === 1) { + if (leftMode === "hash") { + return createWebHashHistory(""); + } else if (leftMode === "h5") { + return createWebHistory(""); + } + } //has param + else if (historyMode.length === 2) { + if (leftMode === "hash") { + return createWebHashHistory(rightMode); + } else if (leftMode === "h5") { + return createWebHistory(rightMode); + } + } +} + +/** 获取当前页面按钮级别的权限 */ +function getAuths(): Array { + return router.currentRoute.value.meta.auths as Array ; +} + +/** 是否有按钮级别的权限(根据路由`meta`中的`auths`字段进行判断)*/ +function hasAuth(value: string | Array ): boolean { + if (!value) return false; + /** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */ + const metaAuths = getAuths(); + if (!metaAuths) return false; + const isAuths = isString(value) + ? metaAuths.includes(value) + : isIncludeAllChildren(value, metaAuths); + return isAuths ? true : false; +} + +function handleTopMenu(route) { + if (route?.children && route.children.length > 1) { + if (route.redirect) { + return route.children.filter(cur => cur.path === route.redirect)[0]; + } else { + return route.children[0]; + } + } else { + return route; + } +} + +/** 获取所有菜单中的第一个菜单(顶级菜单)*/ +function getTopMenu(tag = false): menuType { + const topMenu = handleTopMenu( + usePermissionStoreHook().wholeMenus[0]?.children[0] + ); + tag && useMultiTagsStoreHook().handleTags("push", topMenu); + return topMenu; +} + +export { + hasAuth, + getAuths, + ascending, + filterTree, + initRouter, + getTopMenu, + addPathMatch, + isOneOfArray, + getHistoryMode, + addAsyncRoutes, + getParentPaths, + findRouteByPath, + handleAliveRoute, + formatTwoStageRoutes, + formatFlatteningRoutes, + filterNoPermissionTree +}; diff --git a/VideoAnalysis/WebUI/src/store/index.ts b/VideoAnalysis/WebUI/src/store/index.ts new file mode 100644 index 0000000..a8dc752 --- /dev/null +++ b/VideoAnalysis/WebUI/src/store/index.ts @@ -0,0 +1,9 @@ +import type { App } from "vue"; +import { createPinia } from "pinia"; +const store = createPinia(); + +export function setupStore(app: App ) { + app.use(store); +} + +export { store }; diff --git a/VideoAnalysis/WebUI/src/store/modules/app.ts b/VideoAnalysis/WebUI/src/store/modules/app.ts new file mode 100644 index 0000000..1f5a9a1 --- /dev/null +++ b/VideoAnalysis/WebUI/src/store/modules/app.ts @@ -0,0 +1,85 @@ +import { defineStore } from "pinia"; +import { + type appType, + store, + getConfig, + storageLocal, + deviceDetection, + responsiveStorageNameSpace +} from "../utils"; + +export const useAppStore = defineStore("pure-app", { + state: (): appType => ({ + sidebar: { + opened: + storageLocal().getItem ( + `${responsiveStorageNameSpace()}layout` + )?.sidebarStatus ?? getConfig().SidebarStatus, + withoutAnimation: false, + isClickCollapse: false + }, + // 这里的layout用于监听容器拖拉后恢复对应的导航模式 + layout: + storageLocal().getItem ( + `${responsiveStorageNameSpace()}layout` + )?.layout ?? getConfig().Layout, + device: deviceDetection() ? "mobile" : "desktop", + // 浏览器窗口的可视区域大小 + viewportSize: { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight + } + }), + getters: { + getSidebarStatus(state) { + return state.sidebar.opened; + }, + getDevice(state) { + return state.device; + }, + getViewportWidth(state) { + return state.viewportSize.width; + }, + getViewportHeight(state) { + return state.viewportSize.height; + } + }, + actions: { + TOGGLE_SIDEBAR(opened?: boolean, resize?: string) { + const layout = storageLocal().getItem ( + `${responsiveStorageNameSpace()}layout` + ); + if (opened && resize) { + this.sidebar.withoutAnimation = true; + this.sidebar.opened = true; + layout.sidebarStatus = true; + } else if (!opened && resize) { + this.sidebar.withoutAnimation = true; + this.sidebar.opened = false; + layout.sidebarStatus = false; + } else if (!opened && !resize) { + this.sidebar.withoutAnimation = false; + this.sidebar.opened = !this.sidebar.opened; + this.sidebar.isClickCollapse = !this.sidebar.opened; + layout.sidebarStatus = this.sidebar.opened; + } + storageLocal().setItem(`${responsiveStorageNameSpace()}layout`, layout); + }, + async toggleSideBar(opened?: boolean, resize?: string) { + await this.TOGGLE_SIDEBAR(opened, resize); + }, + toggleDevice(device: string) { + this.device = device; + }, + setLayout(layout) { + this.layout = layout; + }, + setViewportSize(size) { + this.viewportSize = size; + } + } +}); + +export function useAppStoreHook() { + return useAppStore(store); +} diff --git a/VideoAnalysis/WebUI/src/store/modules/epTheme.ts b/VideoAnalysis/WebUI/src/store/modules/epTheme.ts new file mode 100644 index 0000000..e6f62d2 --- /dev/null +++ b/VideoAnalysis/WebUI/src/store/modules/epTheme.ts @@ -0,0 +1,49 @@ +import { defineStore } from "pinia"; +import { + store, + getConfig, + storageLocal, + responsiveStorageNameSpace +} from "../utils"; + +export const useEpThemeStore = defineStore("pure-epTheme", { + state: () => ({ + epThemeColor: + storageLocal().getItem ( + `${responsiveStorageNameSpace()}layout` + )?.epThemeColor ?? getConfig().EpThemeColor, + epTheme: + storageLocal().getItem