林小满 · {{currentRole}}切换角色 {{role.name}} 退出登录
From 40546bfd0134d321d97840905dc9cf8326ba795c Mon Sep 17 00:00:00 2001 From: Vicky <10212746+haifeng-jiang@user.noreply.gitee.com> Date: Tue, 13 Oct 2026 13:58:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=90=AD=E5=BB=BA=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 11 - .env.base | 20 + .env.dev | 38 + .env.pro | 38 + .env.test | 38 + .eslintignore | 8 + .eslintrc.js | 70 + .gitignore | 13 +- .husky/commit-msg | 4 + .husky/lintstagedrc.js | 9 + .husky/pre-commit | 8 + .prettierignore | 10 + .stylelintignore | 6 + .vscode/extensions.json | 3 + .vscode/settings.json | 19 + README.md | 74 +- commitlint.config.js | 28 + index.html | 134 +- mock/_createProductionServer.ts | 18 + mock/role/index.mock.ts | 129 ++ mock/user/index.mock.ts | 90 + package-lock.json | 1565 ----------------- package.json | 125 +- postcss.config.js | 5 + prettier.config.js | 19 + public/favicon.ico | Bin 0 -> 4286 bytes public/logo.png | Bin 0 -> 9193 bytes src/App.vue | 50 +- src/api/login/index.ts | 24 + src/api/login/types.ts | 12 + src/assets/imgs/avatar.jpg | Bin 0 -> 6264 bytes src/assets/imgs/logo.png | Bin 0 -> 9193 bytes src/assets/svgs/403.svg | 1 + src/assets/svgs/404.svg | 1 + src/assets/svgs/500.svg | 1 + src/axios/config.ts | 52 + src/axios/index.ts | 41 + src/axios/service.ts | 74 + src/axios/types/index.ts | 31 + src/components/Backtop/index.ts | 3 + src/components/Backtop/src/Backtop.vue | 15 + src/components/Breadcrumb/index.ts | 3 + src/components/Breadcrumb/src/Breadcrumb.vue | 126 ++ src/components/Breadcrumb/src/helper.ts | 31 + src/components/Button/index.ts | 3 + src/components/Button/src/Button.vue | 121 ++ src/components/Collapse/index.ts | 3 + src/components/Collapse/src/Collapse.vue | 34 + src/components/ConfigGlobal/index.ts | 5 + .../ConfigGlobal/src/ConfigGlobal.vue | 62 + .../ConfigGlobal/src/types/index.ts | 5 + src/components/ContentDetailWrap/index.ts | 3 + .../src/ContentDetailWrap.vue | 25 + src/components/ContentWrap/index.ts | 3 + .../ContentWrap/src/ContentWrap.vue | 33 + src/components/ContextMenu/index.ts | 12 + .../ContextMenu/src/ContextMenu.vue | 72 + src/components/ContextMenu/src/types/index.ts | 7 + src/components/Descriptions/index.ts | 5 + .../Descriptions/src/Descriptions.vue | 161 ++ .../Descriptions/src/types/index.ts | 15 + src/components/Dialog/index.ts | 3 + src/components/Dialog/src/Dialog.vue | 136 ++ src/components/Editor/index.ts | 8 + src/components/Editor/src/Editor.vue | 137 ++ src/components/Error/index.ts | 3 + src/components/Error/src/Error.vue | 58 + src/components/Form/index.ts | 48 + src/components/Form/src/Form.vue | 420 +++++ .../Form/src/components/useRenderCheckbox.tsx | 32 + .../Form/src/components/useRenderRadio.tsx | 32 + .../Form/src/components/useRenderSelect.tsx | 58 + .../Form/src/helper/componentMap.ts | 53 + src/components/Form/src/helper/index.ts | 162 ++ src/components/Form/src/types/index.ts | 665 +++++++ src/components/Icon/index.ts | 5 + src/components/Icon/src/Icon.vue | 74 + src/components/Icon/src/types/index.ts | 6 + src/components/InputPassword/index.ts | 3 + .../InputPassword/src/InputPassword.vue | 142 ++ src/components/LocaleDropdown/index.ts | 5 + .../LocaleDropdown/src/LocaleDropdown.vue | 52 + .../LocaleDropdown/src/types/index.ts | 10 + src/components/Logo/index.ts | 3 + src/components/Logo/src/Logo.vue | 86 + src/components/Menu/index.ts | 3 + src/components/Menu/src/Menu.vue | 260 +++ .../Menu/src/components/useRenderMenuItem.tsx | 50 + .../src/components/useRenderMenuTitle.tsx | 27 + src/components/Menu/src/helper.ts | 54 + src/components/Screenfull/index.ts | 3 + src/components/Screenfull/src/Screenfull.vue | 30 + src/components/Search/index.ts | 15 + src/components/Search/src/Search.vue | 269 +++ .../Search/src/components/ActionButton.vue | 59 + src/components/Search/src/types/index.ts | 16 + src/components/Setting/index.ts | 3 + src/components/Setting/src/Setting.vue | 257 +++ .../src/components/ColorRadioPicker.vue | 65 + .../src/components/InterfaceDisplay.vue | 215 +++ .../src/components/LayoutRadioPicker.vue | 171 ++ src/components/SizeDropdown/index.ts | 3 + .../SizeDropdown/src/SizeDropdown.vue | 39 + src/components/TabMenu/index.ts | 3 + src/components/TabMenu/src/TabMenu.vue | 246 +++ src/components/TabMenu/src/helper.ts | 52 + src/components/Table/index.ts | 21 + src/components/Table/src/Table.vue | 588 +++++++ .../Table/src/components/ColumnSetting.vue | 166 ++ .../Table/src/components/TableActions.vue | 105 ++ src/components/Table/src/helper/index.ts | 8 + src/components/Table/src/types/index.ts | 97 + src/components/TagsView/index.ts | 3 + src/components/TagsView/src/TagsView.vue | 585 ++++++ src/components/TagsView/src/helper.ts | 21 + src/components/ThemeSwitch/index.ts | 3 + .../ThemeSwitch/src/ThemeSwitch.vue | 50 + src/components/UserInfo/index.ts | 3 + src/components/UserInfo/src/UserInfo.vue | 84 + .../UserInfo/src/components/LockDialog.vue | 101 ++ .../UserInfo/src/components/LockPage.vue | 272 +++ src/components/VideoPlayer/index.ts | 27 + .../VideoPlayer/src/VideoPlayer.vue | 59 + src/components/VideoPlayerViewer/index.ts | 3 + .../src/VideoPlayerViewer.vue | 49 + src/components/index.ts | 8 + src/constants/index.ts | 34 + src/dark-theme.css | 85 - src/data/customMenus.js | 17 +- src/data/dramas.js | 60 +- src/data/episodes.js | 40 +- src/data/roles.js | 40 +- src/drama-management.css | 252 --- src/hooks/event/useEmitt.ts | 23 + src/hooks/event/useScrollTo.ts | 62 + src/hooks/web/useConfigGlobal.ts | 10 + src/hooks/web/useCrudSchemas.ts | 158 ++ src/hooks/web/useDesign.ts | 18 + src/hooks/web/useForm.ts | 130 ++ src/hooks/web/useI18n.ts | 52 + src/hooks/web/useIcon.ts | 7 + src/hooks/web/useLocale.ts | 35 + src/hooks/web/useNProgress.ts | 34 + src/hooks/web/useNow.ts | 60 + src/hooks/web/usePageLoading.ts | 18 + src/hooks/web/useSearch.ts | 91 + src/hooks/web/useStorage.ts | 31 + src/hooks/web/useTable.ts | 193 ++ src/hooks/web/useTagsView.ts | 63 + src/hooks/web/useTimeAgo.ts | 50 + src/hooks/web/useTitle.ts | 25 + src/hooks/web/useValidator.ts | 64 + src/hooks/web/useWatermark.ts | 55 + src/layout/Layout.vue | 84 + src/layout/components/AppView.vue | 26 + src/layout/components/ToolHeader.vue | 82 + src/layout/components/useRenderLayout.tsx | 306 ++++ src/layouts/AdminLayout.vue | 94 - src/locales/en.ts | 536 ++++++ src/locales/zh-CN.ts | 530 ++++++ src/main.js | 16 - src/main.ts | 54 + src/navigation.css | 138 -- src/permission.ts | 64 + src/permissions/permissions.js | 17 +- src/plugins/animate.css/index.ts | 1 + src/plugins/echarts/index.ts | 41 + src/plugins/elementPlus/index.ts | 13 + src/plugins/svgIcon/index.ts | 1 + src/plugins/unocss/index.ts | 1 + src/plugins/vueI18n/helper.ts | 3 + src/plugins/vueI18n/index.ts | 42 + src/router/index.js | 47 - src/router/index.ts | 217 +++ src/store/index.ts | 13 + src/store/modules/app.ts | 338 ++++ src/store/modules/locale.ts | 59 + src/store/modules/lock.ts | 48 + src/store/modules/permission.ts | 87 + src/store/modules/tagsView.ts | 163 ++ src/store/modules/user.ts | 102 ++ src/stores/auth.js | 4 +- src/stores/rbac.js | 35 +- src/stores/theme.js | 25 - src/style.css | 1062 ----------- src/styles/business/dark.css | 130 ++ src/styles/business/drama-management.css | 349 ++++ src/styles/business/index.css | 5 + src/styles/business/style.css | 1369 ++++++++++++++ src/styles/business/theme.css | 395 +++++ src/styles/business/typography.css | 11 + src/styles/index.less | 7 + src/styles/var.css | 61 + src/styles/variables.module.less | 10 + src/theme.css | 231 --- src/typography.css | 9 - src/utils/color.ts | 172 ++ src/utils/dateUtil.ts | 17 + src/utils/domUtils.ts | 289 +++ src/utils/index.ts | 139 ++ src/utils/is.ts | 114 ++ src/utils/propTypes.ts | 24 + src/utils/routerHelper.ts | 197 +++ src/utils/status.js | 2 +- src/utils/tree.ts | 207 +++ src/utils/tsxHelper.ts | 16 + src/views/Error/404.vue | 17 + src/views/Login/Login.vue | 274 +++ src/views/Login/components/LoginForm.vue | 251 +++ src/views/Login/components/RegisterForm.vue | 170 ++ src/views/Login/components/index.ts | 4 + src/views/Redirect/Redirect.vue | 30 + src/views/common/DashboardView.vue | 71 +- src/views/common/LoginView.vue | 73 - src/views/components/PageHeading.vue | 4 +- .../contentManagement/DramaManagementView.vue | 168 +- .../EpisodeManagementView.vue | 122 +- src/views/permission/CustomMenuView.vue | 9 +- src/views/permission/MenuManagementView.vue | 29 +- src/views/permission/RoleCategoriesView.vue | 25 +- src/views/permission/RoleManagementView.vue | 26 +- .../composables/useMenuManagement.js | 148 +- .../composables/useRoleCategories.js | 39 +- .../composables/useRolePermissionEditor.js | 69 +- src/views/systemManagement/SettingsView.vue | 17 +- src/views/userManagement/AdminUsersView.vue | 15 +- .../composables/useAdminUsers.js | 54 +- src/views/userManagement/data/adminUsers.js | 65 +- stylelint.config.js | 231 +++ tsconfig.json | 36 + tsconfig.schema.json | 22 + types/components.d.ts | 8 + types/env.d.ts | 34 + types/global.d.ts | 66 + types/router.d.ts | 73 + uno.config.ts | 150 ++ vite.config.js | 12 - vite.config.mts | 165 ++ 238 files changed, 18160 insertions(+), 3837 deletions(-) delete mode 100644 .editorconfig create mode 100644 .env.base create mode 100644 .env.dev create mode 100644 .env.pro create mode 100644 .env.test create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100755 .husky/commit-msg create mode 100644 .husky/lintstagedrc.js create mode 100755 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .stylelintignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 commitlint.config.js create mode 100644 mock/_createProductionServer.ts create mode 100644 mock/role/index.mock.ts create mode 100644 mock/user/index.mock.ts delete mode 100644 package-lock.json create mode 100644 postcss.config.js create mode 100644 prettier.config.js create mode 100644 public/favicon.ico create mode 100644 public/logo.png create mode 100644 src/api/login/index.ts create mode 100644 src/api/login/types.ts create mode 100644 src/assets/imgs/avatar.jpg create mode 100644 src/assets/imgs/logo.png create mode 100644 src/assets/svgs/403.svg create mode 100644 src/assets/svgs/404.svg create mode 100644 src/assets/svgs/500.svg create mode 100644 src/axios/config.ts create mode 100644 src/axios/index.ts create mode 100644 src/axios/service.ts create mode 100644 src/axios/types/index.ts create mode 100644 src/components/Backtop/index.ts create mode 100644 src/components/Backtop/src/Backtop.vue create mode 100644 src/components/Breadcrumb/index.ts create mode 100644 src/components/Breadcrumb/src/Breadcrumb.vue create mode 100644 src/components/Breadcrumb/src/helper.ts create mode 100644 src/components/Button/index.ts create mode 100644 src/components/Button/src/Button.vue create mode 100644 src/components/Collapse/index.ts create mode 100644 src/components/Collapse/src/Collapse.vue create mode 100644 src/components/ConfigGlobal/index.ts create mode 100644 src/components/ConfigGlobal/src/ConfigGlobal.vue create mode 100644 src/components/ConfigGlobal/src/types/index.ts create mode 100644 src/components/ContentDetailWrap/index.ts create mode 100644 src/components/ContentDetailWrap/src/ContentDetailWrap.vue create mode 100644 src/components/ContentWrap/index.ts create mode 100644 src/components/ContentWrap/src/ContentWrap.vue create mode 100644 src/components/ContextMenu/index.ts create mode 100644 src/components/ContextMenu/src/ContextMenu.vue create mode 100644 src/components/ContextMenu/src/types/index.ts create mode 100644 src/components/Descriptions/index.ts create mode 100644 src/components/Descriptions/src/Descriptions.vue create mode 100644 src/components/Descriptions/src/types/index.ts create mode 100644 src/components/Dialog/index.ts create mode 100644 src/components/Dialog/src/Dialog.vue create mode 100644 src/components/Editor/index.ts create mode 100644 src/components/Editor/src/Editor.vue create mode 100644 src/components/Error/index.ts create mode 100644 src/components/Error/src/Error.vue create mode 100644 src/components/Form/index.ts create mode 100644 src/components/Form/src/Form.vue create mode 100644 src/components/Form/src/components/useRenderCheckbox.tsx create mode 100644 src/components/Form/src/components/useRenderRadio.tsx create mode 100644 src/components/Form/src/components/useRenderSelect.tsx create mode 100644 src/components/Form/src/helper/componentMap.ts create mode 100644 src/components/Form/src/helper/index.ts create mode 100644 src/components/Form/src/types/index.ts create mode 100644 src/components/Icon/index.ts create mode 100644 src/components/Icon/src/Icon.vue create mode 100644 src/components/Icon/src/types/index.ts create mode 100644 src/components/InputPassword/index.ts create mode 100644 src/components/InputPassword/src/InputPassword.vue create mode 100644 src/components/LocaleDropdown/index.ts create mode 100644 src/components/LocaleDropdown/src/LocaleDropdown.vue create mode 100644 src/components/LocaleDropdown/src/types/index.ts create mode 100644 src/components/Logo/index.ts create mode 100644 src/components/Logo/src/Logo.vue create mode 100644 src/components/Menu/index.ts create mode 100644 src/components/Menu/src/Menu.vue create mode 100644 src/components/Menu/src/components/useRenderMenuItem.tsx create mode 100644 src/components/Menu/src/components/useRenderMenuTitle.tsx create mode 100644 src/components/Menu/src/helper.ts create mode 100644 src/components/Screenfull/index.ts create mode 100644 src/components/Screenfull/src/Screenfull.vue create mode 100644 src/components/Search/index.ts create mode 100644 src/components/Search/src/Search.vue create mode 100644 src/components/Search/src/components/ActionButton.vue create mode 100644 src/components/Search/src/types/index.ts create mode 100644 src/components/Setting/index.ts create mode 100644 src/components/Setting/src/Setting.vue create mode 100644 src/components/Setting/src/components/ColorRadioPicker.vue create mode 100644 src/components/Setting/src/components/InterfaceDisplay.vue create mode 100644 src/components/Setting/src/components/LayoutRadioPicker.vue create mode 100644 src/components/SizeDropdown/index.ts create mode 100644 src/components/SizeDropdown/src/SizeDropdown.vue create mode 100644 src/components/TabMenu/index.ts create mode 100644 src/components/TabMenu/src/TabMenu.vue create mode 100644 src/components/TabMenu/src/helper.ts create mode 100644 src/components/Table/index.ts create mode 100644 src/components/Table/src/Table.vue create mode 100644 src/components/Table/src/components/ColumnSetting.vue create mode 100644 src/components/Table/src/components/TableActions.vue create mode 100644 src/components/Table/src/helper/index.ts create mode 100644 src/components/Table/src/types/index.ts create mode 100644 src/components/TagsView/index.ts create mode 100644 src/components/TagsView/src/TagsView.vue create mode 100644 src/components/TagsView/src/helper.ts create mode 100644 src/components/ThemeSwitch/index.ts create mode 100644 src/components/ThemeSwitch/src/ThemeSwitch.vue create mode 100644 src/components/UserInfo/index.ts create mode 100644 src/components/UserInfo/src/UserInfo.vue create mode 100644 src/components/UserInfo/src/components/LockDialog.vue create mode 100644 src/components/UserInfo/src/components/LockPage.vue create mode 100644 src/components/VideoPlayer/index.ts create mode 100644 src/components/VideoPlayer/src/VideoPlayer.vue create mode 100644 src/components/VideoPlayerViewer/index.ts create mode 100644 src/components/VideoPlayerViewer/src/VideoPlayerViewer.vue create mode 100644 src/components/index.ts create mode 100644 src/constants/index.ts delete mode 100644 src/dark-theme.css delete mode 100644 src/drama-management.css create mode 100644 src/hooks/event/useEmitt.ts create mode 100644 src/hooks/event/useScrollTo.ts create mode 100644 src/hooks/web/useConfigGlobal.ts create mode 100644 src/hooks/web/useCrudSchemas.ts create mode 100644 src/hooks/web/useDesign.ts create mode 100644 src/hooks/web/useForm.ts create mode 100644 src/hooks/web/useI18n.ts create mode 100644 src/hooks/web/useIcon.ts create mode 100644 src/hooks/web/useLocale.ts create mode 100644 src/hooks/web/useNProgress.ts create mode 100644 src/hooks/web/useNow.ts create mode 100644 src/hooks/web/usePageLoading.ts create mode 100644 src/hooks/web/useSearch.ts create mode 100644 src/hooks/web/useStorage.ts create mode 100644 src/hooks/web/useTable.ts create mode 100644 src/hooks/web/useTagsView.ts create mode 100644 src/hooks/web/useTimeAgo.ts create mode 100644 src/hooks/web/useTitle.ts create mode 100644 src/hooks/web/useValidator.ts create mode 100644 src/hooks/web/useWatermark.ts create mode 100644 src/layout/Layout.vue create mode 100644 src/layout/components/AppView.vue create mode 100644 src/layout/components/ToolHeader.vue create mode 100644 src/layout/components/useRenderLayout.tsx delete mode 100644 src/layouts/AdminLayout.vue create mode 100644 src/locales/en.ts create mode 100644 src/locales/zh-CN.ts delete mode 100644 src/main.js create mode 100644 src/main.ts delete mode 100644 src/navigation.css create mode 100644 src/permission.ts create mode 100644 src/plugins/animate.css/index.ts create mode 100644 src/plugins/echarts/index.ts create mode 100644 src/plugins/elementPlus/index.ts create mode 100644 src/plugins/svgIcon/index.ts create mode 100644 src/plugins/unocss/index.ts create mode 100644 src/plugins/vueI18n/helper.ts create mode 100644 src/plugins/vueI18n/index.ts delete mode 100644 src/router/index.js create mode 100644 src/router/index.ts create mode 100644 src/store/index.ts create mode 100644 src/store/modules/app.ts create mode 100644 src/store/modules/locale.ts create mode 100644 src/store/modules/lock.ts create mode 100644 src/store/modules/permission.ts create mode 100644 src/store/modules/tagsView.ts create mode 100644 src/store/modules/user.ts delete mode 100644 src/stores/theme.js delete mode 100644 src/style.css create mode 100644 src/styles/business/dark.css create mode 100644 src/styles/business/drama-management.css create mode 100644 src/styles/business/index.css create mode 100644 src/styles/business/style.css create mode 100644 src/styles/business/theme.css create mode 100644 src/styles/business/typography.css create mode 100644 src/styles/index.less create mode 100644 src/styles/var.css create mode 100644 src/styles/variables.module.less delete mode 100644 src/theme.css delete mode 100644 src/typography.css create mode 100644 src/utils/color.ts create mode 100644 src/utils/dateUtil.ts create mode 100644 src/utils/domUtils.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/is.ts create mode 100644 src/utils/propTypes.ts create mode 100644 src/utils/routerHelper.ts create mode 100644 src/utils/tree.ts create mode 100644 src/utils/tsxHelper.ts create mode 100644 src/views/Error/404.vue create mode 100644 src/views/Login/Login.vue create mode 100644 src/views/Login/components/LoginForm.vue create mode 100644 src/views/Login/components/RegisterForm.vue create mode 100644 src/views/Login/components/index.ts create mode 100644 src/views/Redirect/Redirect.vue delete mode 100644 src/views/common/LoginView.vue create mode 100644 stylelint.config.js create mode 100644 tsconfig.json create mode 100644 tsconfig.schema.json create mode 100644 types/components.d.ts create mode 100644 types/env.d.ts create mode 100644 types/global.d.ts create mode 100644 types/router.d.ts create mode 100644 uno.config.ts delete mode 100644 vite.config.js create mode 100644 vite.config.mts diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 192c070..0000000 --- a/.editorconfig +++ /dev/null @@ -1,11 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 2 - -[*.md] -trim_trailing_whitespace = false diff --git a/.env.base b/.env.base new file mode 100644 index 0000000..17a0c79 --- /dev/null +++ b/.env.base @@ -0,0 +1,20 @@ +# 环境 +VITE_NODE_ENV=development + +# 接口前缀 +VITE_API_BASE_PATH= + +# 打包路径 +VITE_BASE_PATH=/ + +# 标题 +VITE_APP_TITLE=ReelShort + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=true + +# 是否开启mock +VITE_USE_MOCK=true + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=true diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..a4d6b3a --- /dev/null +++ b/.env.dev @@ -0,0 +1,38 @@ +# 环境 +VITE_NODE_ENV=production + +# 接口前缀 +VITE_API_BASE_PATH= + +# 打包路径 +VITE_BASE_PATH=/dist-dev/ + +# 是否删除debugger +VITE_DROP_DEBUGGER=false + +# 是否删除console.log +VITE_DROP_CONSOLE=false + +# 是否sourcemap +VITE_SOURCEMAP=true + +# 输出路径 +VITE_OUT_DIR=dist-dev + +# 标题 +VITE_APP_TITLE=ElementAdmin + +# 是否包分析 +VITE_USE_BUNDLE_ANALYZER=false + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=false + +# 是否开启mock +VITE_USE_MOCK=true + +# 是否切割css +VITE_USE_CSS_SPLIT=true + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=true diff --git a/.env.pro b/.env.pro new file mode 100644 index 0000000..d3d823f --- /dev/null +++ b/.env.pro @@ -0,0 +1,38 @@ +# 环境 +VITE_NODE_ENV=production + +# 接口前缀 +VITE_API_BASE_PATH= + +# 打包路径 +VITE_BASE_PATH=/ + +# 是否删除debugger +VITE_DROP_DEBUGGER=true + +# 是否删除console.log +VITE_DROP_CONSOLE=true + +# 是否sourcemap +VITE_SOURCEMAP=false + +# 输出路径 +VITE_OUT_DIR=dist-pro + +# 标题 +VITE_APP_TITLE=ElementAdmin + +# 是否包分析 +VITE_USE_BUNDLE_ANALYZER=true + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=false + +# 是否开启mock +VITE_USE_MOCK=true + +# 是否切割css +VITE_USE_CSS_SPLIT=true + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=true diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..b53f347 --- /dev/null +++ b/.env.test @@ -0,0 +1,38 @@ +# 环境 +VITE_NODE_ENV=production + +# 接口前缀 +VITE_API_BASE_PATH= + +# 打包路径 +VITE_BASE_PATH=/dist-test/ + +# 是否删除debugger +VITE_DROP_DEBUGGER=false + +# 是否删除console.log +VITE_DROP_CONSOLE=false + +# 是否sourcemap +VITE_SOURCEMAP=true + +# 输出路径 +VITE_OUT_DIR=dist-test + +# 标题 +VITE_APP_TITLE=ElementAdmin + +# 是否包分析 +VITE_USE_BUNDLE_ANALYZER=false + +# 是否全量引入element-plus样式 +VITE_USE_ALL_ELEMENT_PLUS_STYLE=false + +# 是否开启mock +VITE_USE_MOCK=true + +# 是否切割css +VITE_USE_CSS_SPLIT=false + +# 是否使用在线图标 +VITE_USE_ONLINE_ICON=true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1e85c0f --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +/build/ +/config/ +/dist/ +/*.js +/test/unit/coverage/ +/node_modules/* +/dist* +/src/main.ts diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..4cad823 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,70 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config') +module.exports = defineConfig({ + root: true, + env: { + browser: true, + node: true, + es6: true + }, + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + ecmaVersion: 2020, + sourceType: 'module', + jsxPragma: 'React', + ecmaFeatures: { + jsx: true + } + }, + extends: [ + 'plugin:vue/vue3-recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + 'plugin:prettier/recommended' + ], + rules: { + 'vue/no-setup-props-destructure': 'off', + 'vue/script-setup-uses-vars': 'error', + 'vue/no-reserved-component-names': 'off', + '@typescript-eslint/ban-ts-ignore': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-empty-function': 'off', + 'vue/custom-event-name-casing': 'off', + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'no-unused-vars': 'off', + 'space-before-function-paren': 'off', + + 'vue/attributes-order': 'off', + 'vue/one-component-per-file': 'off', + 'vue/html-closing-bracket-newline': 'off', + 'vue/max-attributes-per-line': 'off', + 'vue/multiline-html-element-content-newline': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/attribute-hyphenation': 'off', + 'vue/require-default-prop': 'off', + 'vue/require-explicit-emits': 'off', + 'vue/html-self-closing': [ + 'error', + { + html: { + void: 'always', + normal: 'never', + component: 'always' + }, + svg: 'always', + math: 'always' + } + ], + 'vue/multi-word-component-names': 'off', + 'vue/no-v-html': 'off' + } +}) diff --git a/.gitignore b/.gitignore index b3c85c8..c4af407 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ -node_modules/ -dist/ +node_modules .DS_Store -.env -.env.* -!.env.example +dist +dist-ssr +*.local +/dist* +*-lock.* +pnpm-debug +stats.html diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..10cb551 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +pnpm commitlint --edit "$1" diff --git a/.husky/lintstagedrc.js b/.husky/lintstagedrc.js new file mode 100644 index 0000000..02afbb2 --- /dev/null +++ b/.husky/lintstagedrc.js @@ -0,0 +1,9 @@ +module.exports = { + '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'], + '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'], + 'package.json': ['prettier --write'], + '*.vue': ['prettier --write', 'stylelint --fix'], + '*.{scss,less,styl,css,html}': ['stylelint --fix', 'prettier --write'], + '*.md': ['prettier --write'], + '*.hbs': ['prettier --write'] +} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..a6a2318 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,8 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +[ -n "$CI" ] && exit 0 + +# Format and submit code according to lintstagedrc.js configuration +npm run ts:check +npm run lint:lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..15cf071 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +/node_modules/** +/dist/ +/dist* +/public/* +/docs/* +/vite.config.ts +/src/types/env.d.ts +/docs/**/* +/plop/**/* +CHANGELOG diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000..aa605b4 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,6 @@ +/dist/* +/public/* +public/* +/dist* +/src/types/env.d.ts +/docs/**/* diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..62a1b22 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["vue.volar", "lokalise.i18n-ally"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ad7ff14 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "prettier.enable": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "[vue]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "i18n-ally.localesPaths": ["src/locales"], + "i18n-ally.keystyle": "nested", + "i18n-ally.sortKeys": true, + "i18n-ally.namespace": false, + "i18n-ally.enabledParsers": ["ts"], + "i18n-ally.sourceLanguage": "en", + "i18n-ally.displayLanguage": "zh-CN", + "i18n-ally.enabledFrameworks": ["vue", "react"], + "god.tsconfig": "./tsconfig.json" +} diff --git a/README.md b/README.md index 7139d18..a8b2f63 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,60 @@ -# ReelShort Studio Console +# ReelShort 内容管理后台 -Frontend-only short-drama content management console built with Vue 3, Vite, Vue Router, and Element Plus. +短剧内容管理后台。界面和工程结构使用 [vue-element-plus-admin](https://element-plus-admin-doc.cn/) 的 `main` 分支,在上面接入了短剧、剧集、角色、菜单和管理员页面。 -## Development +## 环境 + +- Node.js 18 或更高版本 +- pnpm 8 或更高版本 + +## 开始 ```sh -npm install -npm run dev +pnpm install +pnpm dev ``` -## Production build +开发服务地址是 [http://localhost:3005](http://localhost:3005)。同一局域网的设备可以使用终端里的 Network 地址打开。 -```sh -npm run build -npm run preview -``` +演示账号: -## Demo access +| 账号 | 密码 | +| ------- | ---------- | +| `admin` | `admin123` | +| `test` | `test` | -- Username: `admin` -- Password: `admin123` +`admin` 拥有全部菜单。`test` 只用于对照框架自带的权限示例。 -Data and RBAC changes are local demo state. No backend service is connected. +## 命令 -## Source layout +| 命令 | 作用 | +| ----------------- | ------------ | +| `pnpm dev` | 启动开发服务 | +| `pnpm build:dev` | 构建开发包 | +| `pnpm build:test` | 构建测试包 | +| `pnpm build:pro` | 构建生产包 | +| `pnpm serve:pro` | 预览生产包 | -- `src/views/common/` contains shared route pages such as login and dashboard. -- `src/views/contentManagement/` contains short-drama and episode pages. -- `src/views/permission/` contains role, menu, and role-category pages plus their composables. -- `src/views/userManagement/` contains administrator pages and their page data/composables. -- `src/views/systemManagement/` contains system settings pages. -- `src/views/components/` contains reusable page-level UI components. -- `src/data/` contains shared domain records used across views. -- `src/layouts/` contains the authenticated application shell. -- `src/router/` contains routes and access guards. -- `src/stores/` contains auth, RBAC, and theme state. -- `src/permissions/` contains permission definitions and role defaults. +## 页面 + +登录后菜单包括: + +- 工作台 +- 内容管理:短剧管理、剧集管理 +- 后台管理:角色权限配置、管理员用户列表、菜单管理、角色分类、系统设置 + +这些页面的数据仍是前端演示数据,没有接后端。登录和菜单由框架的 mock 接口提供。 + +## 目录 + +框架原有代码保持 [vue-element-plus-admin](https://github.com/kailong321200875/vue-element-plus-admin/tree/mini) 的结构。本项目新增的业务代码在: + +- `src/views/common/DashboardView.vue` +- `src/views/contentManagement/` +- `src/views/permission/` +- `src/views/userManagement/` +- `src/views/systemManagement/` +- `src/data/`、`src/permissions/`、`src/stores/` +- `src/styles/business/` + +路由写在 `src/router/index.ts` 的 `asyncRouterMap`。 diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..78d145d --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,28 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + [ + 'feat', // 新功能(feature) + 'fix', // 修补bug + 'docs', // 文档(documentation) + 'style', // 格式、样式(不影响代码运行的变动) + 'refactor', // 重构(即不是新增功能,也不是修改BUG的代码) + 'perf', // 优化相关,比如提升性能、体验 + 'test', // 添加测试 + 'ci', // 持续集成修改 + 'chore', // 构建过程或辅助工具的变动 + 'revert', // 回滚到上一个版本 + 'workflow', // 工作流改进 + 'mod', // 不确定分类的修改 + 'wip', // 开发中 + 'types', // 类型修改 + 'release' // 版本发布 + ] + ], + 'subject-full-stop': [0, 'never'], + 'subject-case': [0, 'never'] + } +} diff --git a/index.html b/index.html index 6d42b7f..60f5b87 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,133 @@ - -
+ 1I1>71e{dFL_8$L8>!SV6_|+{nL|xI|rrLlSfjLJ4vr?#bx-ADLXnY zgilc@@O6GI!CNlI4s3moX%|TAUTsLKDDDg-)F{@VF+QmbF@Gg0^hGGjNuP~Jbxdt+ zd__@chTr1@HQmc4p09 MYbO zez$-+ME&*W8Y&h_W&^6eP b80sWE(HDAh^ zi4~Lruc+lyf@zd+`#r0mg>i9E1YaGvm^(YcAKZ}u!yy2)i(^i{M ->%1R<$YCVFrlXzF W1YXmrAb z)KGK^FX!mv^4#>o<|N&HW%_2uUyNKAuMMnSznr=&F${X=!u$>Fe=V55CBE}-!H_!> zMv2A14qNysIh$cP4!ekQ+J6kBzZGs_q2k1)JRjdyJZfZsyvu>XDZ}BE5g+WSz)jH! z{;YURbaD4rY^gw4mRqpF4Skk_XHWHujU|&Mcx!hSuHl)9T@*@G0!lNbZELl9Eukai zbh-8=6Z2KyBTXZ1-}`fraU%Ux)$%yF+9gRskBo9GIqpgGF<5AS>jg8XM;hx+BK&r! z E;rJg*Y;rabcZV?9Sw-Hcu8E@-LpjYEWy&GXL~VkR^a(MJgtdG {ojJ?4>5}t-j+P^+(<;A4L=tRZB~w^*Ys`;%i8B)xY+a}hH{7crjc+B=^6+Oo zH*Y&-{z2Uk)}URh-CaU3r=g>zo-K#=y=+}n#QG_*Ta{ap?J75@#D9$knHK|BvqUI^ z_46Fh)wAfUx!fAAsP2QLMmc}Qr)mFM_MdRY_n8)#DArG~RSr>3R@QnToH%m3_+J|q BLq7lj literal 0 HcmV?d00001 diff --git a/src/assets/imgs/logo.png b/src/assets/imgs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b76c71b6ba9a0f73203fbf745fe93d240a3c7802 GIT binary patch literal 9193 zcmX9^2Ut@}(*{vMApwHYi XtvS)pOc)|-<~rT N`R?%w*ibFMc6?%(G~SRCdlq@OxuY$Fj>+gNgU7NpA{UkLOQhYancbKN2Oxx)dD zJoe;Ot`9Q|KS}$V9h(Yu^dpB%O}ljSN6eLYP~SdR)I2YGf-Q67RymG6eW{Nnr;{O# ze&Gl-e?$+++!6gOAcvrtBl?*`00UqSG +XwgVGxY2;{M={g286`Wwa?J8&-jD%$6%ZhOJZy%0r(kPNf&y< zjLmyYO?ym@d#5@BObvTX^?N6Xex}+z@}pknZ`(}Yb|E#pN7_9Q% V9mjADb{Z) z*0E;o;1p`vGPk$no7pn9H{jnk;5BUNTkL6@>kq5f9%9&2ajWuGtMZlX$r~I=8!K{^ z9Es~3iEEq*YirVVoQd-s32V!;UpV8}IO111<5rg*eCCW>;f!5bl=;LNv%DZv&K14P z6}2=kUB(r;$n{}yPO5}EVu3q+e&&ABqF5Do*c{LMxoKD-Z|E!!AR#kTlKH&vrY9sm z^1hwo4F+WLcF+WG(B!yS&h5a7+X3UFqM3ZCF@FCsKL1fZzY%`lVFBb2KXRBKIW)mv z%I`DOc0HkuHi2!r-w_B`LvLdP6N+opbPUWKTu>ni8AT->BP&}+H*aKcXm~ (uay?<|=3TV0mzK#0Db)~!?))=Ccb%=dj5WW>H6!imxA>d>qiVN( z2}Mt!SJCbjcC-*x*ivXhqzbM1m1%xKLFq>j o^n_mRh9`&~a_aCc Zc2v;XfKuU)Rbj(0fd zytDtt%ifF1V$ocCVq$W_Y|<>=>F21EVZ4x5+`5o%pRO*cNDjkjqq2d+Z8+f^ot&cn zOV3Hy;^A!TjxFZpW8hWnKbX8QiSBQ!Cwx}ao<^-FE4|KH2&c7sf#0kdwgVFul9^P5#1c_#ls%nE2OHg zZ#1uHvSedcHg+>!^U*#`q0Ew);5FV{_<}lSWFYbh&1d3X?UHVgS%zI3D6O)FYqfXT zuGgcZDIUN6Wpm7!J0EE|pXu?+og+H(q69ulZ$ otFE7V9SIoI+Tj10Uy1H-$~r zYj4D(q%6}b2bMTqc^Hk>n2&8r41vKDDk*U$zTvjMV>u?i2E-~SmTfocqEYs}K`|Lb z#jfx601Zu%`4m{5<78evWFAGtDt2I7EXrdv8D4p4su$<`<`)bPQ^5UNuIzhDHYO5z zR(e{JHOz5$jKK<7BlPdf;FL5Dm&~>8OygKD_Xxtmjo;HA!_-#v&eQ_K!e^=alHB-f z`T7>VE4y^m7Q1*YJyOGZR?0dro?c+- %BLA7lQr%>)hIifwtG}`xB!pAc28r+t-tkdk4oO5@i*Rsdqd2Z=_$oeSeQDW-WT{dV 8fPMPnXj0Fo=H#vmVK@vw* zM)xic|4NR^>qoTp^@U8cFhLHwd^Fm}R{c>jYK*(<3*6HkJYUdV{DgnJU0q*CV4+h> z#OQtKb9bQ`Yz%cQk#B`~t0mh<(bd1Xa9oe83qva>yklD2cID-k=WsSI-68s*0YOwO zI=EHl)#vg#7*!=&P<-_VmZh@9KXy1bb=CrK<15NrCwo}a#nWR7p}-ay-N_h8GoBSi ze#h)OGH>aFTbRQ;YOq2(f)Fnp%Rja($xYE3Bpt)fR_BE6o5J?$>(mDIr@eH?pL~vc zR>SbaGfvdhOMUA4fV5PjNwbmGLGeYy`iENg8~maa%ChOqEIPgKV#4OvxCDzFbHz z?I3}Cdy*4&-;vQ?F5GC__TJ5cWpqaL{aVzQ%cG9eK5DXzZmk#LTA%7ed`*dZIqHJb zVSuV6Nq46DJVS=JJ3LK3#Gl4c-Q%v2vU2UelKh+wy3ewCN$Z#oc|WLhzO;>*r>MwJ zfka#Rj!oT@;!&JT^VAWkHDTv$kwI-S(f0jGGuT<5_Iw4?vuI4nKs#<7(|EH%yCK(A z;t|-I;( @|dMmQPZn0{ZRF6Pk3y_ z^h+H;(G>;LHr7{$-UU&xnxE$4Xuj=VS<57@9Im8Fkx{xgHOa3s6K6~w)LAJ!GSTA^ zH}-{k$BDkYs?>X) x}U>i8xd_F6Ew!6ipJr zBi|I95Q>&**Z(PdZ1%L?$<^=|)U~`))^m@0ChU4Rn$T{jj(E_U@{1tSRHzRpN%#nD ze>~|D=0@%hsS-tIg(J^rtZD1zQfpOs?g+(^B5VQJZl!ZauN|K?)!Dikjze8Lai$q| z%dFc5U(k4R>own+o(19RMkl2gM!pDC`oHiVcV_ChuPS-wj246{$5#UXI`yz_OsGdu zH;zWhN2nF%PdPWTc7`-_E)dOQ-WGTHI#Z){AukV;f15Oiy}bKMKepF(K#bc+sV)n_ z=hCfrI-b4R<$SNcBxP2S6piuI5K4MFk^(Y%f@Ie^mxOx8`Qyc?7_x+p+2+&dEe|l) zcqami=<4f}Ft%Ina G%-J&D3sCX*E^jH+hn;5P6W4E_=T=^8Hfa&~ zeIBgKl>NBQ%IxU+uj;Pv@Gr!*oZiECw62&ohDWgmovAfrX#%`u@LcY-T&*3|kbNa~ zDY^z(^;m oq!Pzd zwn(Fw zEia`W{?|Q#_fE;F5*n4(BvB$|n+)2@YgU`W^P@j}9i3wSVTw!~{_mK9*RX#cI>zp`G~oKihga92dB|#qUOt+>10 nv?-(f)P!Gt)Vq-)!CJASZQCT4K+&f7oCWBlGkEPF)yNvoXziXXn9R+7@+66a1_u zl169t&$4<9(L3pP?{c9&SSLBmKYR0jP%L@SI -0GD*v6eUKUA%Xqbi0viV}HNCg0v3Vl&I9G81zGPrv3Z;^2w!)P%Vw1 zzLNM42O-!kYr7kfQnR;3;kOQ+Jpa= `EYQ>fJLjs9cYIc72={##-ozb)d8 z)yM?yzE+9Tao9kb+n!7@`r8e_NT-5=_`TDmPF*e@|NBIGS=v4Wv5U``Ai}BaX?>yV z25jT=u6mu)56~AUfAinUYyFUKEfTnW55zhBtN^naZ EI;t eO5k{n!G4DP!u8OQvY|bvaWQn#JnSj{8 zp*mB#+_c|ws=&&oyFG|#6)qq|Tj^&6S&=WUF;E_{zj); G!=P&+kLI?Ym~D1 zi#2I2nr*KxbM#l(?~3R#o;cUtzU!m76bys4!ZAIoTYxwObmO0s*!6x2vHOI#ZJMj8 zH5XmZ0lDAbye!9p`mcWaqq`;}
&ztJA$EjYa#3lv$^32iv+r0}-3un|5caA_Svp*N|a;xlZXIaZGk1wSgqto+M zn_J?G$X^E11FX6hZo#yU0=A=4s9i!U@zPqTsZS|!m$N-fE zx&i^YIc}7ma(5FdY)~pAn+-+#u)3=DGd*cw`O(es1&SCK%7AI|7wZQo#H`i~^W7>O zGwaOle7AemIq8LVPc}hJX9a|(t#0ImloQCkDw^3u=Q!N^J$=U%eN|wW-)(N`W7gtH zUB(<(AU&5YZMxW9hq%9i%%rN5h-)o;S6;ilwg~dN!<#AtaeoITzj;4@{edu2_Ya5E zuf|F#y2gWA8qtjA!lx%>`2neAh)G{IUoYpIuUYIR+_DaHgtx7QSTEgsq&xrGZC4v+ zGr2N`RRcUL$h;vu?Ic_IgRDoCUXwq=%|Dt)h}4u+t6s>bA!@_xXxQFZO)!mfPSkZ* zb&ucv{P-hQreCoBtpBJyw3fz7_PD;{(lXUaE+1Mvl+#6BS9LKIx1-7K%8(|BbQ9Tv z9BFy%ps%|y3VEV;pI|N?&xlVgboR00q7=nn1fCH_DB^YWJ}L$P^q$&@xuZ7S2gVwT zwqY`NtnK(;Fo>;`K4%O9JLa&wjP0K9jAUiiR}|lQ)l`6f#nyQy{h&T)J@&&Qb?bAi zSV{L{eqOhOPlsh8gc)V!?rr+u ={)>xuus zHD{>&e<_H+@YiY9yzAi{8~L4j%kj`B#bzROzQV*eI9+#UKWvEGU<^{FWbOM7G~!V5 zdL}}7CWr1!>Y%;?6iqp-({XkvQ~!27lj9Ny94&E|7r6?rx4|i_WgX1U2wij~z8`Mc z^ND4VoWce>sE_iC{MSx7rKAY^FeT_N^4d2VzuBf&q{fJDhiJpgEbhAYO(>6+@ YHqFlb@1CZGIjYnUw~C1o3i$~Y ze^Wcjtq9~Qk7qI++SbrK#g`0eqnGs+IeyH-@j3dqECv62$3jHpt@48r=`)hRgd>G!GW@p4lkjSMa^msYG{sJ0<9*rq7cbb)$U)V0KeN;H1!{$EPN$D8t$xO z9*$&LMJR3>U{s$oq-4YF?`flOgF-SkBM?JRXz~4{>(b#Ns0w?Jf)%HZY(v!lbh^49 zt$~M~Z_M||JR-F8taKM!5%QGuaVh(+?X)o+9ZIb0AYz##sleOG93^fOtP~yF_b)HA zpsrp>W8{LZRZ6z?#JV#BJ|KSVq`m^FI>P2q>du@01)G~vZ9W3o82HADSX3s7!x5e% zP;eeR8Pw%8z2$H;K18lm5{vD7pu<^kcuerp@<1#9=i%_89)7!wkWgFkVwr?7GFo8* z`>vD>%Zb5TD$Xp@l*dX|JuIYT?%1qc{th@lCM=fJV*P+l>5w{KjzgEm!q5JjE{2GR zrT#_km{;iC?w8Yu!22u{JSV!_{#Fxw?ok7876W~g9@M9Ire{ Y%J3l;Y!KjG%S|7_4iL?oKqi~)(S>URO oX9n86)G<%DvaBc1qEe>N^B&Sa4}!j%i^I3JrG~g~7fbY@ z5KStNjqSD%TJfdNC3E@=BzCUMss}vhO&u?XX(8UUF0#i^#}84jHsTLQR&e6F#(hx2 zkI=A9gGP-p+)&9EY<)}IUI{h++Nthyp!6vH$>tzz!s{Y=<{}*^A$T#(u>>|Z^+QDV z4TSwSmL#|2M`bF*w&)=AdzEi!G5@UF;d4hC1`=zBy329FX(lXvwZ<26SO(;OMF#Q} z8m8k$-WCyO$q4{h5&YeS*j?p1Z-tS=#evh`ZE8XP@k_JqI+Ub-n-&vt*%jP0xd2ky zvH+WR$(Ig`nv#!L-p&o2twPO7EzOaLQeBd_n8JQk2<(31V;kg6X|`iPbsQHkL$zUQ zGopr))6vDN`tF0ef`FLVVQszK7`r?$PG;btoXY8in9u~SM`cj2w&4#k+uYj&4r+u| z%mZVfhZ!IPE7u0l@U}MWEJ;!P6H4Q;g&n_1?gwf*dtfda51gmT7St72pe&F6Rwn8} zJKyuboDJ|vrcg1JDH)fd1<9F#(17cOohu&$L0oI>+wKsWTC@K2F;9BfwvNI(q9A9~ z;vm+q)X%Ah=j>c<894Z%PT}7nyxb~l9HLo(C)6_wq{QxVz |Lt;J4Ic=IWd$>;3#!GvvglmJ z6rcXhtIp5?9u0oklY)0vfARYMbh#GWch@No&J_=#Y=sCJWjdqHpFI =UB-X}T#CVpj&4N6 zk4*_Xbm*R!B+rD{WSwvT;+%r=MTQR7{_q`g(04U!r2n)BS9; @h{+eB-GC(0vsk=taz YHtM@+ZZx4%g zQPZ%3st!Io!%D23Inuu@_{7RhC(AM%768pr@#11B8(0CFr!&TVmG&Eoh&(O^HWALE zYG_RizVOpvh4Qi&oj6?ocb(V%E4D)gb&(XnVIRN4sKfe-tyY?BDEZ?(5_OPqurkf+ zGa4R60igWO9d?Q`*ruI }gtx_tUL$uhTWRaIasNm^H!ct9&TrgRKQ* nz@SXd)2qOZ{ISBOILu?MRB8ppvWhu44^j}k2IR=a z%oduXWoL1ZNB!K!QGEtyZdV4y-6fSCVy%%BzoB3*pB-kmO1aeRs7sP0AGyB5Wn!5J zy!F^8qFRj@r*=X9Cutn#lINwnF3t91*v9V6nR=Ky(r3D7bG!P jTv?wA9GqhNIhOxMz^I_b zu?<|eo>%$>g1gjFpw)c;j2fqp9Z99|w0s++zlg&d?>!Fn7b5@!zGj}tEu8IkX>N~X zAB<0{Q>R&Q8*DBTBu#5HqDjv#;<0DmCdCh8Ijj46IHaKd_=(*fcNcx*s_P!^p53&F z&bl =O``E(cKoOFnGgxJFxZ1Ok!TG40{tT#C@q(HR^e`vqTTaxNX{k}=# z^(QAI7DM#6{_nyLiMI4D?4llLS7%lMwps& T ze&4)eu{EKw~nVMvLCE|-??Gti7f2`^{MLVv29jNgsxWal98hM`c|pm#)_ z+#Gg_6>>SYplZVf1qw3m7;uiKP&2?7eLf)*POMLpfHAFrtNI3R-yJpa9fAZ!d@z9h zK%-;Y_NWPi*mo}P$*&4^H2hkMhpk?rOYnnOwaqu{poh{{cE`5who}h{si-qJcq*p{ z@^#&l#y&NB*_+XxTDSSWA2l`Uz}e 2n#jzRL#qX-in-K>YqYk#d-)j-_4beeq} z8HL&bhd1&?130&Tz|2;h!M?!(eDRrD((g}@rL`S4xgOVDfCWFj^sA2+Fz09d*5S2G zw0&@2*KNCNII|qGyCLd<@1K?m&(&_iui2kt&Z3g#fI_KCLeTF?ZbwE0 $wRYgq5IOX!@=;xzwsY7%6q(CM3~%9` zbts{5U%U)(Y>$nHXb -PQomkm 7M^3Ycmc#7yY$6J9137e2gRODlT2e`21=St* zRLLJ(ynq2dM~LRFyD$(rWdk+k;AHwBrj1|B!funhE1D5So_#X>I+JbGbY-(aRO!}? zgOm63?k=VPsC6EZu?e4)7tJ !`sE->&_h7-pO(| zjvU_mUxYqI>WnB}%FyTMb= h~d3_~~49SwXa%iTO%2MF4WTET$K> z_Tn2f=4`gQ&mdVEEh}A&>A2TQ#$!yx=Iz6KA>Ho-s`?DRjm7fa)xBV_L6LE`< bQpiFQV!gopI%fH*w5GCM6noHPRsd&bq?8aJf5 z{`pCih{*8jz~%7AOvk_XV4Qyz2e?1Ufz^#DviazrQLEjSl+?_SelIQl%goYknO92d zV{fPmEgEz>a1^!PtdSz?!DTt(Id~q@9njS9Gtngpt9-)sQt9hf*)EC8b0(W7DZ6?` ze kZH#d8B(4me{omw9=`42=WiidW_O8>LmGb>}Xt^&?T?vL1WHfAYNsP7lV%X49Rd zE#9{Ue5 pSY{(T37w9kW9TClkT-J3(W5>*w8~-xiMMou2P7qsd>5 zPy+ly!ti-Sxxw$
-$YKTOmB@ea#b~fo%C=F z4kh+sALcSOQgUg4uUpwo1x$E)hmsdf b}0I)amP0dho&LG4gDACl8=;loj#MLdNA^jFF!?g5XBTGXb-r z97_?=wD*`%-WNq$3j>?iEB$ctK zoLuMa9bhapwu&-)H9%71zY2GGi;-fhifd7wW{+a%1N7NrN>i1RvMDVP_NZ&U195Mz z6D}lpm1yV?p2 Vq*-ax@-hAMx~tcOMGFscM-2jb94Y z2!;%+O|us={Jj>Jf_N{<|D>6%aP|&74tSCa%xnu)ThdmquT=vS&7<~0@j-vWb|qG* X5_)J=A58xJn+`(LK;w&=?YsX2HuGA3 literal 0 HcmV?d00001 diff --git a/src/assets/svgs/403.svg b/src/assets/svgs/403.svg new file mode 100644 index 0000000..4500596 --- /dev/null +++ b/src/assets/svgs/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svgs/404.svg b/src/assets/svgs/404.svg new file mode 100644 index 0000000..5244d8d --- /dev/null +++ b/src/assets/svgs/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svgs/500.svg b/src/assets/svgs/500.svg new file mode 100644 index 0000000..9c02092 --- /dev/null +++ b/src/assets/svgs/500.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/axios/config.ts b/src/axios/config.ts new file mode 100644 index 0000000..93d3f19 --- /dev/null +++ b/src/axios/config.ts @@ -0,0 +1,52 @@ +import { AxiosResponse, InternalAxiosRequestConfig } from './types' +import { ElMessage } from 'element-plus' +import qs from 'qs' +import { SUCCESS_CODE, TRANSFORM_REQUEST_DATA } from '@/constants' +import { useUserStoreWithOut } from '@/store/modules/user' +import { objToFormData } from '@/utils' + +const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => { + if ( + config.method === 'post' && + config.headers['Content-Type'] === 'application/x-www-form-urlencoded' + ) { + config.data = qs.stringify(config.data) + } else if ( + TRANSFORM_REQUEST_DATA && + config.method === 'post' && + config.headers['Content-Type'] === 'multipart/form-data' + ) { + config.data = objToFormData(config.data) + } + if (config.method === 'get' && config.params) { + let url = config.url as string + url += '?' + const keys = Object.keys(config.params) + for (const key of keys) { + if (config.params[key] !== void 0 && config.params[key] !== null) { + url += `${key}=${encodeURIComponent(config.params[key])}&` + } + } + url = url.substring(0, url.length - 1) + config.params = {} + config.url = url + } + return config +} + +const defaultResponseInterceptors = (response: AxiosResponse) => { + if (response?.config?.responseType === 'blob') { + // 如果是文件流,直接过 + return response + } else if (response.data.code === SUCCESS_CODE) { + return response.data + } else { + ElMessage.error(response?.data?.message) + if (response?.data?.code === 401) { + const userStore = useUserStoreWithOut() + userStore.logout() + } + } +} + +export { defaultResponseInterceptors, defaultRequestInterceptors } diff --git a/src/axios/index.ts b/src/axios/index.ts new file mode 100644 index 0000000..94cbfc3 --- /dev/null +++ b/src/axios/index.ts @@ -0,0 +1,41 @@ +import service from './service' +import { CONTENT_TYPE } from '@/constants' +import { useUserStoreWithOut } from '@/store/modules/user' + +const request = (option: AxiosConfig) => { + const { url, method, params, data, headers, responseType } = option + const userStore = useUserStoreWithOut() + return service.request({ + url: url, + method, + params, + data, + responseType: responseType, + headers: { + 'Content-Type': CONTENT_TYPE, + [userStore.getTokenKey ?? 'Authorization']: userStore.getToken ?? '', + ...headers + } + }) +} + +export default { + get: (option: AxiosConfig) => { + return request({ method: 'get', ...option }) as Promise > + }, + post: (option: AxiosConfig) => { + return request({ method: 'post', ...option }) as Promise > + }, + delete: (option: AxiosConfig) => { + return request({ method: 'delete', ...option }) as Promise > + }, + put: (option: AxiosConfig) => { + return request({ method: 'put', ...option }) as Promise > + }, + cancelRequest: (url: string | string[]) => { + return service.cancelRequest(url) + }, + cancelAllRequest: () => { + return service.cancelAllRequest() + } +} diff --git a/src/axios/service.ts b/src/axios/service.ts new file mode 100644 index 0000000..9933bda --- /dev/null +++ b/src/axios/service.ts @@ -0,0 +1,74 @@ +import axios, { AxiosError } from 'axios' +import { defaultRequestInterceptors, defaultResponseInterceptors } from './config' + +import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './types' +import { ElMessage } from 'element-plus' +import { REQUEST_TIMEOUT } from '@/constants' + +export const PATH_URL = import.meta.env.VITE_API_BASE_PATH + +const abortControllerMap: Map = new Map() + +const axiosInstance: AxiosInstance = axios.create({ + timeout: REQUEST_TIMEOUT, + baseURL: PATH_URL +}) + +axiosInstance.interceptors.request.use((res: InternalAxiosRequestConfig) => { + const controller = new AbortController() + const url = res.url || '' + res.signal = controller.signal + abortControllerMap.set(url, controller) + return res +}) + +axiosInstance.interceptors.response.use( + (res: AxiosResponse) => { + const url = res.config.url || '' + abortControllerMap.delete(url) + // 这里不能做任何处理,否则后面的 interceptors 拿不到完整的上下文了 + return res + }, + (error: AxiosError) => { + console.log('err: ' + error) // for debug + ElMessage.error(error.message) + return Promise.reject(error) + } +) + +axiosInstance.interceptors.request.use(defaultRequestInterceptors) +axiosInstance.interceptors.response.use(defaultResponseInterceptors) + +const service = { + request: (config: RequestConfig) => { + return new Promise((resolve, reject) => { + if (config.interceptors?.requestInterceptors) { + config = config.interceptors.requestInterceptors(config as any) + } + + axiosInstance + .request(config) + .then((res) => { + resolve(res) + }) + .catch((err: any) => { + reject(err) + }) + }) + }, + cancelRequest: (url: string | string[]) => { + const urlList = Array.isArray(url) ? url : [url] + for (const _url of urlList) { + abortControllerMap.get(_url)?.abort() + abortControllerMap.delete(_url) + } + }, + cancelAllRequest() { + for (const [_, controller] of abortControllerMap) { + controller.abort() + } + abortControllerMap.clear() + } +} + +export default service diff --git a/src/axios/types/index.ts b/src/axios/types/index.ts new file mode 100644 index 0000000..b70eb26 --- /dev/null +++ b/src/axios/types/index.ts @@ -0,0 +1,31 @@ +import type { + InternalAxiosRequestConfig, + AxiosResponse, + AxiosRequestConfig, + AxiosInstance, + AxiosRequestHeaders, + AxiosError +} from 'axios' + +interface RequestInterceptors { + // 请求拦截 + requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig + requestInterceptorsCatch?: (err: any) => any + // 响应拦截 + responseInterceptors?: (config: T) => T + responseInterceptorsCatch?: (err: any) => any +} + +interface RequestConfig extends AxiosRequestConfig { + interceptors?: RequestInterceptors +} + +export { + AxiosResponse, + RequestInterceptors, + RequestConfig, + AxiosInstance, + InternalAxiosRequestConfig, + AxiosRequestHeaders, + AxiosError +} diff --git a/src/components/Backtop/index.ts b/src/components/Backtop/index.ts new file mode 100644 index 0000000..96de88d --- /dev/null +++ b/src/components/Backtop/index.ts @@ -0,0 +1,3 @@ +import Backtop from './src/Backtop.vue' + +export { Backtop } diff --git a/src/components/Backtop/src/Backtop.vue b/src/components/Backtop/src/Backtop.vue new file mode 100644 index 0000000..f5c2fc4 --- /dev/null +++ b/src/components/Backtop/src/Backtop.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/src/components/Breadcrumb/index.ts b/src/components/Breadcrumb/index.ts new file mode 100644 index 0000000..93ffe70 --- /dev/null +++ b/src/components/Breadcrumb/index.ts @@ -0,0 +1,3 @@ +import Breadcrumb from './src/Breadcrumb.vue' + +export { Breadcrumb } diff --git a/src/components/Breadcrumb/src/Breadcrumb.vue b/src/components/Breadcrumb/src/Breadcrumb.vue new file mode 100644 index 0000000..0a2dcc5 --- /dev/null +++ b/src/components/Breadcrumb/src/Breadcrumb.vue @@ -0,0 +1,126 @@ + + + diff --git a/src/components/Breadcrumb/src/helper.ts b/src/components/Breadcrumb/src/helper.ts new file mode 100644 index 0000000..fb3ec19 --- /dev/null +++ b/src/components/Breadcrumb/src/helper.ts @@ -0,0 +1,31 @@ +import { pathResolve } from '@/utils/routerHelper' +import type { RouteMeta } from 'vue-router' + +export const filterBreadcrumb = ( + routes: AppRouteRecordRaw[], + parentPath = '' +): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + + for (const route of routes) { + const meta = route?.meta as RouteMeta + if (meta.hidden && !meta.canTo) { + continue + } + + const data: AppRouteRecordRaw = + !meta.alwaysShow && route.children?.length === 1 + ? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) } + : { ...route } + + data.path = pathResolve(parentPath, data.path) + + if (data.children) { + data.children = filterBreadcrumb(data.children, data.path) + } + if (data) { + res.push(data) + } + } + return res +} diff --git a/src/components/Button/index.ts b/src/components/Button/index.ts new file mode 100644 index 0000000..6ae43b1 --- /dev/null +++ b/src/components/Button/index.ts @@ -0,0 +1,3 @@ +import BaseButton from './src/Button.vue' + +export { BaseButton } diff --git a/src/components/Button/src/Button.vue b/src/components/Button/src/Button.vue new file mode 100644 index 0000000..e5af71b --- /dev/null +++ b/src/components/Button/src/Button.vue @@ -0,0 +1,121 @@ + + + + emits('click')" + > + + diff --git a/src/components/Collapse/index.ts b/src/components/Collapse/index.ts new file mode 100644 index 0000000..73f65a3 --- /dev/null +++ b/src/components/Collapse/index.ts @@ -0,0 +1,3 @@ +import Collapse from './src/Collapse.vue' + +export { Collapse } diff --git a/src/components/Collapse/src/Collapse.vue b/src/components/Collapse/src/Collapse.vue new file mode 100644 index 0000000..6d6362e --- /dev/null +++ b/src/components/Collapse/src/Collapse.vue @@ -0,0 +1,34 @@ + + + ++ + + ++ diff --git a/src/components/ConfigGlobal/index.ts b/src/components/ConfigGlobal/index.ts new file mode 100644 index 0000000..eaeb7d0 --- /dev/null +++ b/src/components/ConfigGlobal/index.ts @@ -0,0 +1,5 @@ +import ConfigGlobal from './src/ConfigGlobal.vue' + +export type { ConfigGlobalTypes } from './src/types' + +export { ConfigGlobal } diff --git a/src/components/ConfigGlobal/src/ConfigGlobal.vue b/src/components/ConfigGlobal/src/ConfigGlobal.vue new file mode 100644 index 0000000..5bd90cf --- /dev/null +++ b/src/components/ConfigGlobal/src/ConfigGlobal.vue @@ -0,0 +1,62 @@ + + + ++ + + diff --git a/src/components/ConfigGlobal/src/types/index.ts b/src/components/ConfigGlobal/src/types/index.ts new file mode 100644 index 0000000..d41e4d3 --- /dev/null +++ b/src/components/ConfigGlobal/src/types/index.ts @@ -0,0 +1,5 @@ +import { ComponentSize } from 'element-plus' + +export interface ConfigGlobalTypes { + size?: ComponentSize +} diff --git a/src/components/ContentDetailWrap/index.ts b/src/components/ContentDetailWrap/index.ts new file mode 100644 index 0000000..1871cac --- /dev/null +++ b/src/components/ContentDetailWrap/index.ts @@ -0,0 +1,3 @@ +import ContentDetailWrap from './src/ContentDetailWrap.vue' + +export { ContentDetailWrap } diff --git a/src/components/ContentDetailWrap/src/ContentDetailWrap.vue b/src/components/ContentDetailWrap/src/ContentDetailWrap.vue new file mode 100644 index 0000000..29b4ff7 --- /dev/null +++ b/src/components/ContentDetailWrap/src/ContentDetailWrap.vue @@ -0,0 +1,25 @@ + + + ++ ++ diff --git a/src/components/ContentWrap/index.ts b/src/components/ContentWrap/index.ts new file mode 100644 index 0000000..8c22cc8 --- /dev/null +++ b/src/components/ContentWrap/index.ts @@ -0,0 +1,3 @@ +import ContentWrap from './src/ContentWrap.vue' + +export { ContentWrap } diff --git a/src/components/ContentWrap/src/ContentWrap.vue b/src/components/ContentWrap/src/ContentWrap.vue new file mode 100644 index 0000000..4538fc0 --- /dev/null +++ b/src/components/ContentWrap/src/ContentWrap.vue @@ -0,0 +1,33 @@ + + + ++ ++++ + + + + diff --git a/src/components/ContextMenu/index.ts b/src/components/ContextMenu/index.ts new file mode 100644 index 0000000..1b5442d --- /dev/null +++ b/src/components/ContextMenu/index.ts @@ -0,0 +1,12 @@ +import ContextMenu from './src/ContextMenu.vue' +import { ElDropdown } from 'element-plus' +import type { RouteLocationNormalizedLoaded } from 'vue-router' + +export type { ContextMenuSchema } from './src/types' + +export interface ContextMenuExpose { + elDropdownMenuRef: ComponentRef+ {{ title }} ++ ++ + +{{ message }}+ ++ +++ + tagItem: RouteLocationNormalizedLoaded +} + +export { ContextMenu } diff --git a/src/components/ContextMenu/src/ContextMenu.vue b/src/components/ContextMenu/src/ContextMenu.vue new file mode 100644 index 0000000..9583037 --- /dev/null +++ b/src/components/ContextMenu/src/ContextMenu.vue @@ -0,0 +1,72 @@ + + + + + + diff --git a/src/components/ContextMenu/src/types/index.ts b/src/components/ContextMenu/src/types/index.ts new file mode 100644 index 0000000..b2be72b --- /dev/null +++ b/src/components/ContextMenu/src/types/index.ts @@ -0,0 +1,7 @@ +export interface ContextMenuSchema { + disabled?: boolean + divided?: boolean + icon?: string + label: string + command?: (item: ContextMenuSchema) => void +} diff --git a/src/components/Descriptions/index.ts b/src/components/Descriptions/index.ts new file mode 100644 index 0000000..8f9a0f0 --- /dev/null +++ b/src/components/Descriptions/index.ts @@ -0,0 +1,5 @@ +import Descriptions from './src/Descriptions.vue' + +export type { DescriptionsSchema } from './src/types' + +export { Descriptions } diff --git a/src/components/Descriptions/src/Descriptions.vue b/src/components/Descriptions/src/Descriptions.vue new file mode 100644 index 0000000..20dcc71 --- /dev/null +++ b/src/components/Descriptions/src/Descriptions.vue @@ -0,0 +1,161 @@ + + + diff --git a/src/components/Descriptions/src/types/index.ts b/src/components/Descriptions/src/types/index.ts new file mode 100644 index 0000000..be55b1c --- /dev/null +++ b/src/components/Descriptions/src/types/index.ts @@ -0,0 +1,15 @@ +export interface DescriptionsSchema { + span?: number // 占多少分 + field: string // 字段名 + label?: string // label名 + width?: string | number + minWidth?: string | number + align?: 'left' | 'center' | 'right' + labelAlign?: 'left' | 'center' | 'right' + className?: string + labelClassName?: string + slots?: { + default?: (...args: any[]) => JSX.Element | null + label?: (...args: any[]) => JSX.Element | null + } +} diff --git a/src/components/Dialog/index.ts b/src/components/Dialog/index.ts new file mode 100644 index 0000000..1655dad --- /dev/null +++ b/src/components/Dialog/index.ts @@ -0,0 +1,3 @@ +import Dialog from './src/Dialog.vue' + +export { Dialog } diff --git a/src/components/Dialog/src/Dialog.vue b/src/components/Dialog/src/Dialog.vue new file mode 100644 index 0000000..7bba971 --- /dev/null +++ b/src/components/Dialog/src/Dialog.vue @@ -0,0 +1,136 @@ + + + ++ + + + ++ +{{ t(item.label) }} + + + + + + diff --git a/src/components/Editor/index.ts b/src/components/Editor/index.ts new file mode 100644 index 0000000..3fbf0a9 --- /dev/null +++ b/src/components/Editor/index.ts @@ -0,0 +1,8 @@ +import Editor from './src/Editor.vue' +import { IDomEditor } from '@wangeditor/editor' + +export interface EditorExpose { + getEditorRef: () => Promise++ + ++ {{ title }} + ++++ + + + + ++ + + +} + +export { Editor } diff --git a/src/components/Editor/src/Editor.vue b/src/components/Editor/src/Editor.vue new file mode 100644 index 0000000..b354db0 --- /dev/null +++ b/src/components/Editor/src/Editor.vue @@ -0,0 +1,137 @@ + + + + + ++ + + diff --git a/src/components/Error/index.ts b/src/components/Error/index.ts new file mode 100644 index 0000000..a52c6f9 --- /dev/null +++ b/src/components/Error/index.ts @@ -0,0 +1,3 @@ +import Error from './src/Error.vue' + +export { Error } diff --git a/src/components/Error/src/Error.vue b/src/components/Error/src/Error.vue new file mode 100644 index 0000000..8676a81 --- /dev/null +++ b/src/components/Error/src/Error.vue @@ -0,0 +1,58 @@ + + + ++ + + ++ diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts new file mode 100644 index 0000000..6688425 --- /dev/null +++ b/src/components/Form/index.ts @@ -0,0 +1,48 @@ +import Form from './src/Form.vue' +import type { FormSchema, FormSetProps } from './src/types' +export type { + ComponentNameEnum, + ComponentName, + InputComponentProps, + AutocompleteComponentProps, + InputNumberComponentProps, + SelectOption, + SelectComponentProps, + SelectV2ComponentProps, + CascaderComponentProps, + SwitchComponentProps, + RateComponentProps, + ColorPickerComponentProps, + TransferComponentProps, + RadioOption, + RadioGroupComponentProps, + RadioButtonComponentProps, + CheckboxOption, + CheckboxGroupComponentProps, + DividerComponentProps, + DatePickerComponentProps, + DateTimePickerComponentProps, + TimePickerComponentProps, + TimeSelectComponentProps, + ColProps, + FormSetProps, + FormItemProps, + FormSchema, + FormProps, + PlaceholderModel, + InputPasswordComponentProps, + TreeSelectComponentProps +} from './src/types' + +export interface FormExpose { + setValues: (data: Recordable) => void + setProps: (props: Recordable) => void + delSchema: (field: string) => void + addSchema: (formSchema: FormSchema, index?: number) => void + setSchema: (schemaProps: FormSetProps[]) => void + formModel: Recordable + getComponentExpose: (field: string) => any + getFormItemExpose: (field: string) => any +} + +export { Form } diff --git a/src/components/Form/src/Form.vue b/src/components/Form/src/Form.vue new file mode 100644 index 0000000..c31baa1 --- /dev/null +++ b/src/components/Form/src/Form.vue @@ -0,0 +1,420 @@ + + + diff --git a/src/components/Form/src/components/useRenderCheckbox.tsx b/src/components/Form/src/components/useRenderCheckbox.tsx new file mode 100644 index 0000000..173e5c3 --- /dev/null +++ b/src/components/Form/src/components/useRenderCheckbox.tsx @@ -0,0 +1,32 @@ +import { FormSchema, ComponentNameEnum, CheckboxGroupComponentProps } from '../types' +import { ElCheckbox, ElCheckboxButton } from 'element-plus' +import { defineComponent } from 'vue' + +export const useRenderCheckbox = () => { + const renderCheckboxOptions = (item: FormSchema) => { + // 如果有别名,就取别名 + const componentProps = item?.componentProps as CheckboxGroupComponentProps + const valueAlias = componentProps?.props?.value || 'value' + const labelAlias = componentProps?.props?.label || 'label' + const disabledAlias = componentProps?.props?.disabled || 'disabled' + const Com = ( + item.component === ComponentNameEnum.CHECKBOX_GROUP ? ElCheckbox : ElCheckboxButton + ) as ReturnType+++
{{ errorMap[type].message }}+++{{ errorMap[type].buttonText }} ++ return componentProps?.options?.map((option) => { + const { value, ...other } = option + return ( + + {option[labelAlias || 'label']} + + ) + }) + } + + return { + renderCheckboxOptions + } +} diff --git a/src/components/Form/src/components/useRenderRadio.tsx b/src/components/Form/src/components/useRenderRadio.tsx new file mode 100644 index 0000000..e198f1f --- /dev/null +++ b/src/components/Form/src/components/useRenderRadio.tsx @@ -0,0 +1,32 @@ +import { FormSchema, ComponentNameEnum, RadioGroupComponentProps } from '../types' +import { ElRadio, ElRadioButton } from 'element-plus' +import { defineComponent } from 'vue' + +export const useRenderRadio = () => { + const renderRadioOptions = (item: FormSchema) => { + // 如果有别名,就取别名 + const componentProps = item?.componentProps as RadioGroupComponentProps + const valueAlias = componentProps?.props?.value || 'value' + const labelAlias = componentProps?.props?.label || 'label' + const disabledAlias = componentProps?.props?.disabled || 'disabled' + const Com = ( + item.component === ComponentNameEnum.RADIO_GROUP ? ElRadio : ElRadioButton + ) as ReturnType+ return componentProps?.options?.map((option) => { + const { value, ...other } = option + return ( + + {option[labelAlias || 'label']} + + ) + }) + } + + return { + renderRadioOptions + } +} diff --git a/src/components/Form/src/components/useRenderSelect.tsx b/src/components/Form/src/components/useRenderSelect.tsx new file mode 100644 index 0000000..46389af --- /dev/null +++ b/src/components/Form/src/components/useRenderSelect.tsx @@ -0,0 +1,58 @@ +import { ElOption, ElOptionGroup } from 'element-plus' +import { FormSchema, SelectComponentProps, SelectOption } from '../types' + +export const useRenderSelect = () => { + // 渲染 select options + const renderSelectOptions = (item: FormSchema) => { + const componentsProps = item?.componentProps as SelectComponentProps + const optionGroupDefaultSlot = componentsProps?.slots?.optionGroupDefault + // 如果有别名,就取别名 + const labelAlias = componentsProps?.props?.label + const keyAlias = componentsProps?.props?.key + return componentsProps?.options?.map((option) => { + if (option?.options?.length) { + return optionGroupDefaultSlot ? ( + optionGroupDefaultSlot(option) + ) : ( ++ {{ + default: () => + option?.options?.map((v) => { + return renderSelectOptionItem(item, v) + }) + }} + + ) + } else { + return renderSelectOptionItem(item, option) + } + }) + } + + // 渲染 select option item + const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => { + // 如果有别名,就取别名 + const componentsProps = item.componentProps as SelectComponentProps + const labelAlias = componentsProps?.props?.label + const valueAlias = componentsProps?.props?.value + const keyAlias = componentsProps?.props?.key + const optionDefaultSlot = componentsProps.slots?.optionDefault + + return ( ++ {{ + default: () => (optionDefaultSlot ? optionDefaultSlot(option) : undefined) + }} + + ) + } + + return { + renderSelectOptions + } +} diff --git a/src/components/Form/src/helper/componentMap.ts b/src/components/Form/src/helper/componentMap.ts new file mode 100644 index 0000000..b40c8ce --- /dev/null +++ b/src/components/Form/src/helper/componentMap.ts @@ -0,0 +1,53 @@ +import type { Component } from 'vue' +import { + ElCascader, + ElCheckboxGroup, + ElColorPicker, + ElDatePicker, + ElInput, + ElInputNumber, + ElRadioGroup, + ElRate, + ElSelect, + ElSelectV2, + ElSlider, + ElSwitch, + ElTimePicker, + ElTimeSelect, + ElTransfer, + ElAutocomplete, + ElDivider, + ElTreeSelect, + ElUpload +} from 'element-plus' +import { InputPassword } from '@/components/InputPassword' +import { Editor } from '@/components/Editor' +import { ComponentName } from '../types' + +const componentMap: Recordable= { + RadioGroup: ElRadioGroup, + RadioButton: ElRadioGroup, + CheckboxGroup: ElCheckboxGroup, + CheckboxButton: ElCheckboxGroup, + Input: ElInput, + Autocomplete: ElAutocomplete, + InputNumber: ElInputNumber, + Select: ElSelect, + Cascader: ElCascader, + Switch: ElSwitch, + Slider: ElSlider, + TimePicker: ElTimePicker, + DatePicker: ElDatePicker, + Rate: ElRate, + ColorPicker: ElColorPicker, + Transfer: ElTransfer, + Divider: ElDivider, + TimeSelect: ElTimeSelect, + SelectV2: ElSelectV2, + InputPassword: InputPassword, + Editor: Editor, + TreeSelect: ElTreeSelect, + Upload: ElUpload +} + +export { componentMap } diff --git a/src/components/Form/src/helper/index.ts b/src/components/Form/src/helper/index.ts new file mode 100644 index 0000000..d537ad4 --- /dev/null +++ b/src/components/Form/src/helper/index.ts @@ -0,0 +1,162 @@ +import { useI18n } from '@/hooks/web/useI18n' +import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types' +import { isFunction } from '@/utils/is' +import { firstUpperCase, humpToDash } from '@/utils' +import { set, get } from 'lodash-es' + +const { t } = useI18n() + +/** + * + * @param schema 对应组件数据 + * @returns 返回提示信息对象 + * @description 用于自动设置placeholder + */ +export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => { + const textMap = [ + ComponentNameEnum.INPUT, + ComponentNameEnum.AUTOCOMPLETE, + ComponentNameEnum.INPUT_NUMBER, + ComponentNameEnum.INPUT_PASSWORD + ] + const selectMap = [ + ComponentNameEnum.SELECT, + ComponentNameEnum.TIME_PICKER, + ComponentNameEnum.DATE_PICKER, + ComponentNameEnum.TIME_SELECT, + ComponentNameEnum.SELECT_V2 + ] + if (textMap.includes(schema?.component as ComponentNameEnum)) { + return { + placeholder: t('common.inputText') + } + } + if (selectMap.includes(schema?.component as ComponentNameEnum)) { + // 一些范围选择器 + const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange'] + if ( + twoTextMap.includes( + ((schema?.componentProps as any)?.type || + (schema?.componentProps as any)?.isRange) as string + ) + ) { + return { + startPlaceholder: t('common.startTimeText'), + endPlaceholder: t('common.endTimeText'), + rangeSeparator: '-' + } + } else { + return { + placeholder: t('common.selectText') + } + } + } + return {} +} + +/** + * + * @param col 内置栅格 + * @returns 返回栅格属性 + * @description 合并传入进来的栅格属性 + */ +export const setGridProp = (col: ColProps = {}): ColProps => { + const colProps: ColProps = { + // 如果有span,代表用户优先级更高,所以不需要默认栅格 + ...(col.span + ? {} + : { + xs: 24, + sm: 12, + md: 12, + lg: 12, + xl: 12 + }), + ...col + } + return colProps +} + +/** + * + * @param item 传入的组件属性 + * @returns 默认添加 clearable 属性 + */ +export const setComponentProps = (item: FormSchema): Recordable => { + // const notNeedClearable = ['ColorPicker'] + // 拆分事件并组合 + const onEvents = (item?.componentProps as any)?.on || {} + const newOnEvents: Recordable = {} + + for (const key in onEvents) { + if (onEvents[key]) { + newOnEvents[`on${firstUpperCase(key)}`] = (...args: any[]) => { + onEvents[key](...args) + } + } + } + + const componentProps: Recordable = { + clearable: true, + ...item.componentProps, + ...newOnEvents + } + // 需要删除额外的属性 + if (componentProps.slots) { + delete componentProps.slots + } + if (componentProps.on) { + delete componentProps.on + } + return componentProps +} + +/** + * + * @param formModel 表单数据 + * @param slotsProps 插槽属性 + */ +export const setItemComponentSlots = (slotsProps: Recordable = {}): Recordable => { + const slotObj: Recordable = {} + for (const key in slotsProps) { + if (slotsProps[key]) { + if (isFunction(slotsProps[key])) { + slotObj[humpToDash(key)] = (...args: any[]) => { + return slotsProps[key]?.(...args) + } + } else { + slotObj[humpToDash(key)] = () => { + return slotsProps[key] + } + } + } + } + return slotObj +} + +/** + * + * @param schema Form表单结构化数组 + * @param formModel FormMoel + * @returns FormMoel + * @description 生成对应的formModel + */ +export const initModel = (schema: FormSchema[], formModel: Recordable) => { + const model: Recordable = { ...formModel } + schema.map((v) => { + if (v.remove) { + delete model[v.field] + } else if (v.component !== 'Divider') { + // const hasField = Reflect.has(model, v.field) + const hasField = get(model, v.field) + // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值 + set( + model, + v.field, + hasField !== void 0 ? get(model, v.field) : v.value !== void 0 ? v.value : undefined + ) + // model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : undefined + } + }) + return model +} diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts new file mode 100644 index 0000000..727dbf7 --- /dev/null +++ b/src/components/Form/src/types/index.ts @@ -0,0 +1,665 @@ +import { + AutocompleteProps, + InputNumberProps, + CascaderProps, + CascaderNode, + CascaderValue, + SwitchProps, + ComponentSize, + InputProps, + RateProps, + ColorPickerProps, + TransferProps, + RadioGroupProps, + RadioButtonProps, + CheckboxGroupProps, + DividerProps, + DatePickerProps, + FormItemProps as ElFormItemProps, + FormProps as ElFormProps, + ISelectProps, + UploadProps +} from 'element-plus' +import { IEditorConfig } from '@wangeditor/editor' +import { CSSProperties } from 'vue' + +export interface PlaceholderModel { + placeholder?: string + startPlaceholder?: string + endPlaceholder?: string + rangeSeparator?: string +} + +export enum ComponentNameEnum { + RADIO_GROUP = 'RadioGroup', + RADIO_BUTTON = 'RadioButton', + CHECKBOX_GROUP = 'CheckboxGroup', + CHECKBOX_BUTTON = 'CheckboxButton', + INPUT = 'Input', + AUTOCOMPLETE = 'Autocomplete', + INPUT_NUMBER = 'InputNumber', + SELECT = 'Select', + CASCADER = 'Cascader', + SWITCH = 'Switch', + SLIDER = 'Slider', + TIME_PICKER = 'TimePicker', + DATE_PICKER = 'DatePicker', + RATE = 'Rate', + COLOR_PICKER = 'ColorPicker', + TRANSFER = 'Transfer', + DIVIDER = 'Divider', + TIME_SELECT = 'TimeSelect', + SELECT_V2 = 'SelectV2', + INPUT_PASSWORD = 'InputPassword', + EDITOR = 'Editor', + TREE_SELECT = 'TreeSelect', + UPLOAD = 'Upload' +} + +type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K + ? K extends string + ? K extends `${infer A}_${infer B}` + ? `${Capitalize >}${Capitalize >}` + : Capitalize > + : never + : never + +export type ComponentName = CamelCaseComponentName + +export interface InputPasswordComponentProps { + strength?: boolean + style?: CSSProperties +} + +export interface InputComponentProps extends Partial { + rows?: number + on?: { + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + change?: (value: string | number) => void + clear?: () => void + input?: (value: string | number) => void + } + slots?: { + prefix?: (...args: any[]) => JSX.Element | null + suffix?: (...args: any[]) => JSX.Element | null + prepend?: (...args: any[]) => JSX.Element | null + append?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface AutocompleteComponentProps extends Partial { + on?: { + select?: (item: any) => void + change?: (value: string | number) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + prefix?: (...args: any[]) => JSX.Element | null + suffix?: (...args: any[]) => JSX.Element | null + prepend?: (...args: any[]) => JSX.Element | null + append?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface InputNumberComponentProps extends Partial { + on?: { + change?: (currentValue: number | undefined, oldValue: number | undefined) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + } + style?: CSSProperties +} + +export interface SelectOption { + label?: string + disabled?: boolean + value?: any + key?: string | number + options?: SelectOption[] + [key: string]: any +} + +export interface SelectComponentProps extends Omit , 'options'> { + /** + * 数据源的字段别名 + */ + props?: { + key?: string + value?: string + label?: string + children?: string + } + on?: { + change?: (value: string | number | boolean | Object) => void + visibleChange?: (visible: boolean) => void + removeTag?: (tag: any) => void + clear?: () => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + } + slots?: { + default?: (options: SelectOption[]) => JSX.Element[] | null + optionGroupDefault?: (item: SelectOption) => JSX.Element + optionDefault?: (option: SelectOption) => JSX.Element | null + prefix?: (...args: any[]) => JSX.Element | null + empty?: (...args: any[]) => JSX.Element | null + } + options?: SelectOption[] + style?: CSSProperties +} + +export interface SelectV2ComponentProps { + multiple?: boolean + disabled?: boolean + valueKey?: string + size?: ComponentSize + clearable?: boolean + clearIcon?: string | JSX.Element | null + collapseTags?: boolean + multipleLimit?: number + name?: string + effect?: string + autocomplete?: string + placeholder?: string + filterable?: boolean + allowCreate?: boolean + reserveKeyword?: boolean + noDataText?: string + popperClass?: string + teleported?: boolean + persistent?: boolean + popperOptions?: any + automaticDropdown?: boolean + height?: number + scrollbarAlwaysOn?: boolean + remote?: boolean + remoteMethod?: (query: string) => void + validateEvent?: boolean + placement?: AutocompleteProps['placement'] + collapseTagsTooltip?: boolean + on?: { + change?: (value: string | number | boolean | Object) => void + visibleChange?: (visible: boolean) => void + removeTag?: (tag: any) => void + clear?: () => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + } + options?: SelectOption[] + slots?: { + default?: (option: SelectOption) => JSX.Element | null + } + style?: CSSProperties +} + +export interface CascaderComponentProps { + options?: Record [] + props?: CascaderProps + size?: ComponentSize + placeholder?: string + disabled?: boolean + clearable?: boolean + showAllLevels?: boolean + collapseTags?: boolean + collapseTagsTooltip?: boolean + separator?: string + filterable?: boolean + filterMethod?: (node: CascaderNode, keyword: string) => boolean + debounce?: number + beforeFilter?: (value: string) => boolean + popperClass?: string + teleported?: boolean + tagType?: ElementPlusInfoType + validateEvent?: boolean + on?: { + change?: (value: CascaderValue) => void + expandChange?: (value: CascaderValue) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + visibleChange?: (value: boolean) => void + removeTag?: (value: CascaderNode['valueByOption']) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + empty?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface SwitchComponentProps extends Partial { + on?: { + change?: (value: boolean | string | number) => void + } + style?: CSSProperties +} + +export interface RateComponentProps extends Partial { + on?: { + change?: (value: number) => void + } + style?: CSSProperties +} + +export interface ColorPickerComponentProps extends Partial { + on?: { + change?: (value: string) => void + activeChange?: (value: string) => void + } + style?: CSSProperties +} + +export interface TransferComponentProps extends Partial { + on?: { + change?: ( + value: number | string, + direction: 'left' | 'right', + movedKeys: string[] | number[] + ) => void + leftCheckChange?: (value: any[]) => void + rightCheckChange?: (value: any[]) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + leftFooter?: (...args: any[]) => JSX.Element | null + rightFooter?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface RadioOption { + label?: string + value?: string | number | boolean + disabled?: boolean + border?: boolean + size?: ComponentSize + name?: string + [key: string]: any +} +export interface RadioGroupComponentProps extends Partial { + options?: RadioOption[] + /** + * 数据源的字段别名 + */ + props?: { + label?: string + value?: string + disabled?: string + } + on?: { + change?: (value: string | number | boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element[] | null + } + style?: CSSProperties +} + +export interface RadioButtonComponentProps extends Partial { + options?: RadioOption[] + /** + * 数据源的字段别名 + */ + props?: { + label?: string + value?: string + disabled?: string + } + on?: { + change?: (value: string | number | boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element[] | null + } + style?: CSSProperties +} + +export interface CheckboxOption { + label?: string + value?: string | number | boolean + disabled?: boolean + trueLabel?: string | number + falseLabel?: string | number + border?: boolean + size?: ComponentSize + name?: string + checked?: boolean + indeterminate?: boolean + validateEvent?: boolean + tabindex?: number | string + id?: string + controls?: boolean + [key: string]: any +} + +export interface CheckboxGroupComponentProps extends Partial { + options?: CheckboxOption[] + /** + * 数据源的字段别名 + */ + props?: { + label?: string + value?: string + disabled?: string + } + on?: { + change?: (value: string | number | boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element[] | null + } + style?: CSSProperties +} + +export interface DividerComponentProps extends Partial { + on?: { + change?: (value: number) => void + input?: (value: number) => void + } + style?: CSSProperties +} + +export interface DatePickerComponentProps extends Partial { + on?: { + change?: (value: string | Date | number | string[]) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + calendarChange?: (val: [Date, Date]) => void + panelChange?: (date, mode, view) => void + visibleChange?: (visibility: boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + rangeSeparator?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface DateTimePickerComponentProps { + readonly?: boolean + disabled?: boolean + editable?: boolean + clearable?: boolean + size?: ComponentSize + placeholder?: string + startPlaceholder?: string + endPlaceholder?: string + timeArrowControl?: boolean + type?: 'year' | 'month' | 'date' | 'datetime' | 'datetimerange' | 'daterange' | 'week' + format?: string + popperClass?: string + rangeSeparator?: string + defaultValue?: Date | [Date, Date] + defaultTime?: Date | [Date, Date] + valueFormat?: string + id?: string + name?: string + unlinkPanels?: boolean + prefixIcon?: string | JSX.Element + clearIcon?: string | JSX.Element + shortcuts?: Array<{ text: string; value: Date | Function }> + disabledDate?: (date: Date) => boolean + cellClassName?: string | ((date: Date) => string | undefined) + teleported?: boolean + on?: { + change?: (value: string | Date | number | string[]) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + calendarChange?: (val: [Date, Date]) => void + visibleChange?: (visibility: boolean) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + rangeSeparator?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface TimePickerComponentProps { + readonly?: boolean + disabled?: boolean + editable?: boolean + clearable?: boolean + size?: ComponentSize + placeholder?: string + startPlaceholder?: string + endPlaceholder?: string + isRange?: boolean + arrowControl?: boolean + popperClass?: string + rangeSeparator?: string + format?: string + defaultValue?: Date | [Date, Date] + id?: string + name?: string + label?: string + prefixIcon?: string | JSX.Element + clearIcon?: string | JSX.Element + disabledHours?: (role: string, comparingDate?: any) => number[] + disabledMinutes?: (hour: number, role: string, comparingDate?: any) => number[] + disabledSeconds?: (hour: number, minute: number, role: string, comparingDate?: any) => number[] + teleported?: boolean + tabindex?: number | string + on?: { + change: ( + val: number | string | Date | [number, number] | [string, string] | [Date, Date] + ) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + visibleChange?: (visibility: boolean) => void + } + style?: CSSProperties +} + +export interface TimeSelectComponentProps { + disabled?: boolean + editable?: boolean + clearable?: boolean + size?: ComponentSize + placeholder?: string + name?: string + effect?: string + prefixIcon?: string | JSX.Element + clearIcon?: string | JSX.Element + start?: string + end?: string + step?: string + minTime?: string + maxTime?: string + format?: string + on?: { + change?: (val: string) => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + } + style?: CSSProperties +} + +export interface EditorComponentProps { + editorConfig?: IEditorConfig + style?: CSSProperties +} + +export interface ColProps { + span?: number + xs?: number + sm?: number + md?: number + lg?: number + xl?: number + tag?: string +} + +export interface FormSetProps { + field: string + path: string + value: any +} + +export interface FormItemProps extends Partial { + style?: CSSProperties + slots?: { + default?: (...args: any[]) => JSX.Element | null + label?: (...args: any[]) => JSX.Element | null + error?: (...args: any[]) => JSX.Element | null + } +} + +export interface UploadComponentProps extends Partial { + slots?: { + default?: (...args: any[]) => JSX.Element | null + trigger?: (...args: any[]) => JSX.Element | null + tip?: (...args: any[]) => JSX.Element | null + file?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface TreeSelectComponentProps extends Omit< + Partial , + 'props' | 'on' | 'slots' +> { + data?: any[] + emptyText?: string + nodeKey?: string + props?: { + children?: string + label?: string | ((...args: any[]) => string) + disabled?: string | ((...args: any[]) => string) + isLeaf?: string | ((...args: any[]) => string) + class?: string | ((...args: any[]) => string) + } + renderAfterExpand?: boolean + load?: (...args: any[]) => Promise + renderContent?: (...args: any[]) => JSX.Element | null + highlightCurrent?: boolean + defaultExpandAll?: boolean + expandOnClickNode?: boolean + checkOnClickNode?: boolean + autoExpandParent?: boolean + defaultExpandedKeys?: any[] + showCheckbox?: boolean + checkStrictly?: boolean + defaultCheckedKeys?: any[] + currentNodeKey?: string | number + filterNodeMethod?: (...args: any[]) => boolean + accordion?: boolean + indent?: number + icon?: string | ((...args: any[]) => JSX.Element | null) + lazy?: boolean + draggable?: boolean + allowDrag?: (...args: any[]) => boolean + allowDrop?: (...args: any[]) => boolean + on?: { + change?: (value: string | number | boolean | Object) => void + visibleChange?: (visible: boolean) => void + removeTag?: (tag: any) => void + clear?: () => void + blur?: (event: FocusEvent) => void + focus?: (event: FocusEvent) => void + nodeClick?: (...args: any[]) => void + nodeContextMenu?: (...args: any[]) => void + checkChange?: (...args: any[]) => void + check?: (...args: any[]) => void + currentChange?: (...args: any[]) => void + nodeExpand?: (...args: any[]) => void + nodeCollapse?: (...args: any[]) => void + nodeDragStart?: (...args: any[]) => void + nodeDragEnter?: (...args: any[]) => void + nodeDragLeave?: (...args: any[]) => void + nodeDragOver?: (...args: any[]) => void + nodeDragEnd?: (...args: any[]) => void + nodeDrop?: (...args: any[]) => void + } + slots?: { + default?: (...args: any[]) => JSX.Element | null + optionGroupDefault?: (item: SelectOption) => JSX.Element + optionDefault?: (option: SelectOption) => JSX.Element | null + prefix?: (...args: any[]) => JSX.Element | null + empty?: (...args: any[]) => JSX.Element | null + } + style?: CSSProperties +} + +export interface FormSchema { + /** + * 唯一标识 + */ + field: string + + /** + * 标题 + */ + label?: string + + /** + * col组件属性 + */ + colProps?: ColProps + + /** + * 表单组件属性,具体可以查看element-plus文档 + */ + componentProps?: + | InputComponentProps + | AutocompleteComponentProps + | InputNumberComponentProps + | SelectComponentProps + | SelectV2ComponentProps + | CascaderComponentProps + | SwitchComponentProps + | RateComponentProps + | ColorPickerComponentProps + | TransferComponentProps + | RadioGroupComponentProps + | RadioButtonComponentProps + | DividerComponentProps + | DatePickerComponentProps + | DateTimePickerComponentProps + | TimePickerComponentProps + | InputPasswordComponentProps + | TreeSelectComponentProps + | UploadComponentProps + | any + + /** + * formItem组件属性,具体可以查看element-plus文档 + */ + formItemProps?: FormItemProps + + /** + * 渲染的组件名称 + */ + component?: ComponentName + + /** + * 初始值 + */ + value?: any + + /** + * 是否隐藏,如果为true,会连同值一同删除,类似v-if + */ + remove?: boolean + + /** + * 样式隐藏,不会把值一同删掉,类似v-show + */ + hidden?: boolean + + /** + * @returns 远程加载下拉项 + */ + optionApi?: any +} + +export interface FormProps extends Partial { + schema?: FormSchema[] + isCol?: boolean + model?: Recordable + autoSetPlaceholder?: boolean + isCustom?: boolean + [key: string]: any +} diff --git a/src/components/Icon/index.ts b/src/components/Icon/index.ts new file mode 100644 index 0000000..83469d4 --- /dev/null +++ b/src/components/Icon/index.ts @@ -0,0 +1,5 @@ +import Icon from './src/Icon.vue' + +export type { IconTypes } from './src/types' + +export { Icon } diff --git a/src/components/Icon/src/Icon.vue b/src/components/Icon/src/Icon.vue new file mode 100644 index 0000000..1b5bc4c --- /dev/null +++ b/src/components/Icon/src/Icon.vue @@ -0,0 +1,74 @@ + + + + + + + + + + + diff --git a/src/components/Icon/src/types/index.ts b/src/components/Icon/src/types/index.ts new file mode 100644 index 0000000..632e4e8 --- /dev/null +++ b/src/components/Icon/src/types/index.ts @@ -0,0 +1,6 @@ +export interface IconTypes { + size?: number + color?: string + icon: string + hoverColor?: string +} diff --git a/src/components/InputPassword/index.ts b/src/components/InputPassword/index.ts new file mode 100644 index 0000000..1dcc38e --- /dev/null +++ b/src/components/InputPassword/index.ts @@ -0,0 +1,3 @@ +import InputPassword from './src/InputPassword.vue' + +export { InputPassword } diff --git a/src/components/InputPassword/src/InputPassword.vue b/src/components/InputPassword/src/InputPassword.vue new file mode 100644 index 0000000..1f04dbb --- /dev/null +++ b/src/components/InputPassword/src/InputPassword.vue @@ -0,0 +1,142 @@ + + + ++ + + ++ + + diff --git a/src/components/LocaleDropdown/index.ts b/src/components/LocaleDropdown/index.ts new file mode 100644 index 0000000..4d9b98a --- /dev/null +++ b/src/components/LocaleDropdown/index.ts @@ -0,0 +1,5 @@ +import LocaleDropdown from './src/LocaleDropdown.vue' + +export type { Language, LocaleDropdownType } from './src/types' + +export { LocaleDropdown } diff --git a/src/components/LocaleDropdown/src/LocaleDropdown.vue b/src/components/LocaleDropdown/src/LocaleDropdown.vue new file mode 100644 index 0000000..3c2a535 --- /dev/null +++ b/src/components/LocaleDropdown/src/LocaleDropdown.vue @@ -0,0 +1,52 @@ + + + ++ + +++ + diff --git a/src/components/LocaleDropdown/src/types/index.ts b/src/components/LocaleDropdown/src/types/index.ts new file mode 100644 index 0000000..c749dce --- /dev/null +++ b/src/components/LocaleDropdown/src/types/index.ts @@ -0,0 +1,10 @@ +export interface Language { + el: Recordable + name: string +} + +export interface LocaleDropdownType { + lang: LocaleType + name?: string + elLocale?: Language +} diff --git a/src/components/Logo/index.ts b/src/components/Logo/index.ts new file mode 100644 index 0000000..1c4224c --- /dev/null +++ b/src/components/Logo/index.ts @@ -0,0 +1,3 @@ +import Logo from './src/Logo.vue' + +export { Logo } diff --git a/src/components/Logo/src/Logo.vue b/src/components/Logo/src/Logo.vue new file mode 100644 index 0000000..c411537 --- /dev/null +++ b/src/components/Logo/src/Logo.vue @@ -0,0 +1,86 @@ + + + ++ + + + ++ {{ item.name }} + +++ diff --git a/src/components/Menu/index.ts b/src/components/Menu/index.ts new file mode 100644 index 0000000..a6ec696 --- /dev/null +++ b/src/components/Menu/index.ts @@ -0,0 +1,3 @@ +import Menu from './src/Menu.vue' + +export { Menu } diff --git a/src/components/Menu/src/Menu.vue b/src/components/Menu/src/Menu.vue new file mode 100644 index 0000000..1950824 --- /dev/null +++ b/src/components/Menu/src/Menu.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/src/components/Menu/src/components/useRenderMenuItem.tsx b/src/components/Menu/src/components/useRenderMenuItem.tsx new file mode 100644 index 0000000..301313f --- /dev/null +++ b/src/components/Menu/src/components/useRenderMenuItem.tsx @@ -0,0 +1,50 @@ +import { ElSubMenu, ElMenuItem } from 'element-plus' +import { hasOneShowingChild } from '../helper' +import { isUrl } from '@/utils/is' +import { useRenderMenuTitle } from './useRenderMenuTitle' +import { pathResolve } from '@/utils/routerHelper' + +const { renderMenuTitle } = useRenderMenuTitle() + +export const useRenderMenuItem = () => + // allRouters: AppRouteRecordRaw[] = [], + { + const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { + return routers + .filter((v) => !v.meta?.hidden) + .map((v) => { + const meta = v.meta ?? {} + const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) + const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath+ ++
+ {{ title }} ++(allRouters, v.path).join('/') + + if ( + oneShowingChild && + (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && + !meta?.alwaysShow + ) { + return ( + + {{ + default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta) + }} + + ) + } else { + return ( ++ {{ + title: () => renderMenuTitle(meta), + default: () => renderMenuItem(v.children!, fullPath) + }} + + ) + } + }) + } + + return { + renderMenuItem + } + } diff --git a/src/components/Menu/src/components/useRenderMenuTitle.tsx b/src/components/Menu/src/components/useRenderMenuTitle.tsx new file mode 100644 index 0000000..8941d9d --- /dev/null +++ b/src/components/Menu/src/components/useRenderMenuTitle.tsx @@ -0,0 +1,27 @@ +import type { RouteMeta } from 'vue-router' +import { Icon } from '@/components/Icon' +import { useI18n } from '@/hooks/web/useI18n' + +export const useRenderMenuTitle = () => { + const renderMenuTitle = (meta: RouteMeta) => { + const { t } = useI18n() + const { title = 'Please set title', icon } = meta + + return icon ? ( + <> ++ + > + ) : ( + + ) + } + + return { + renderMenuTitle + } +} diff --git a/src/components/Menu/src/helper.ts b/src/components/Menu/src/helper.ts new file mode 100644 index 0000000..003cf10 --- /dev/null +++ b/src/components/Menu/src/helper.ts @@ -0,0 +1,54 @@ +import { ref, unref } from 'vue' +import { findPath } from '@/utils/tree' + +type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean } + +interface HasOneShowingChild { + oneShowingChild?: boolean + onlyOneChild?: OnlyOneChildType +} + +export const getAllParentPath = (treeData: T[], path: string) => { + const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[] + return (menuList || []).map((item) => item.path) +} + +export const hasOneShowingChild = ( + children: AppRouteRecordRaw[] = [], + parent: AppRouteRecordRaw +): HasOneShowingChild => { + const onlyOneChild = ref () + + const showingChildren = children.filter((v) => { + const meta = v.meta ?? {} + if (meta.hidden) { + return false + } else { + // Temp set(will be used if only has one showing child) + onlyOneChild.value = v + return true + } + }) + + // When there is only one child router, the child router is displayed by default + if (showingChildren.length === 1) { + return { + oneShowingChild: true, + onlyOneChild: unref(onlyOneChild) + } + } + + // Show parent if there are no child router to display + if (!showingChildren.length) { + onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } + return { + oneShowingChild: true, + onlyOneChild: unref(onlyOneChild) + } + } + + return { + oneShowingChild: false, + onlyOneChild: unref(onlyOneChild) + } +} diff --git a/src/components/Screenfull/index.ts b/src/components/Screenfull/index.ts new file mode 100644 index 0000000..faec2d8 --- /dev/null +++ b/src/components/Screenfull/index.ts @@ -0,0 +1,3 @@ +import Screenfull from './src/Screenfull.vue' + +export { Screenfull } diff --git a/src/components/Screenfull/src/Screenfull.vue b/src/components/Screenfull/src/Screenfull.vue new file mode 100644 index 0000000..32a12e7 --- /dev/null +++ b/src/components/Screenfull/src/Screenfull.vue @@ -0,0 +1,30 @@ + + + + ++ diff --git a/src/components/Search/index.ts b/src/components/Search/index.ts new file mode 100644 index 0000000..68e74b4 --- /dev/null +++ b/src/components/Search/index.ts @@ -0,0 +1,15 @@ +import { FormSchema, FormSetProps } from '../Form' +import Search from './src/Search.vue' + +export type { SearchProps } from './src/types' + +export interface SearchExpose { + setValues: (data: Recordable) => void + setProps: (props: Recordable) => void + delSchema: (field: string) => void + addSchema: (formSchema: FormSchema, index?: number) => void + setSchema: (schemaProps: FormSetProps[]) => void + formModel: Recordable +} + +export { Search } diff --git a/src/components/Search/src/Search.vue b/src/components/Search/src/Search.vue new file mode 100644 index 0000000..5a15b4f --- /dev/null +++ b/src/components/Search/src/Search.vue @@ -0,0 +1,269 @@ + + + + + + ++ ++ + diff --git a/src/components/Search/src/components/ActionButton.vue b/src/components/Search/src/components/ActionButton.vue new file mode 100644 index 0000000..fad88f3 --- /dev/null +++ b/src/components/Search/src/components/ActionButton.vue @@ -0,0 +1,59 @@ + + + ++ + {{ t('common.query') }} + ++ {{ t('common.reset') }} + ++ {{ t(visible ? 'common.shrink' : 'common.expand') }} + + diff --git a/src/components/Search/src/types/index.ts b/src/components/Search/src/types/index.ts new file mode 100644 index 0000000..7a1901d --- /dev/null +++ b/src/components/Search/src/types/index.ts @@ -0,0 +1,16 @@ +import { FormSchema } from '@/components/Form' + +export interface SearchProps { + schema?: FormSchema[] + isCol?: boolean + labelWidth?: string | number + layout?: 'inline' | 'bottom' + buttonPosition?: 'left' | 'right' | 'center' + showSearch?: boolean + showReset?: boolean + showExpand?: boolean + expandField?: string + inline?: boolean + removeNoValueItem?: boolean + model?: Recordable +} diff --git a/src/components/Setting/index.ts b/src/components/Setting/index.ts new file mode 100644 index 0000000..b64c9ad --- /dev/null +++ b/src/components/Setting/index.ts @@ -0,0 +1,3 @@ +import Setting from './src/Setting.vue' + +export { Setting } diff --git a/src/components/Setting/src/Setting.vue b/src/components/Setting/src/Setting.vue new file mode 100644 index 0000000..e2077a4 --- /dev/null +++ b/src/components/Setting/src/Setting.vue @@ -0,0 +1,257 @@ + + + +++ ++ + + {{ t('setting.projectSetting') }} + + + + + + diff --git a/src/components/Setting/src/components/ColorRadioPicker.vue b/src/components/Setting/src/components/ColorRadioPicker.vue new file mode 100644 index 0000000..bc40e10 --- /dev/null +++ b/src/components/Setting/src/components/ColorRadioPicker.vue @@ -0,0 +1,65 @@ + + + ++ ++ + +{{ t('setting.theme') }} ++ + + {{ t('setting.layout') }} ++ + + {{ t('setting.systemTheme') }} ++ + + {{ t('setting.headerTheme') }} ++ + + {{ t('setting.menuTheme') }} ++ {{ t('setting.interfaceDisplay') }} ++ + + ++{{ + t('setting.copy') + }} ++++ {{ t('setting.clearAndReset') }} + ++ ++ + + diff --git a/src/components/Setting/src/components/InterfaceDisplay.vue b/src/components/Setting/src/components/InterfaceDisplay.vue new file mode 100644 index 0000000..e328d8a --- /dev/null +++ b/src/components/Setting/src/components/InterfaceDisplay.vue @@ -0,0 +1,215 @@ + + + ++ + ++ diff --git a/src/components/Setting/src/components/LayoutRadioPicker.vue b/src/components/Setting/src/components/LayoutRadioPicker.vue new file mode 100644 index 0000000..de6e80f --- /dev/null +++ b/src/components/Setting/src/components/LayoutRadioPicker.vue @@ -0,0 +1,171 @@ + + + ++ {{ t('setting.breadcrumb') }} ++ ++ + {{ t('setting.breadcrumbIcon') }} ++ ++ + {{ t('setting.hamburgerIcon') }} ++ ++ + {{ t('setting.screenfullIcon') }} ++ ++ + {{ t('setting.sizeIcon') }} ++ ++ + {{ t('setting.localeIcon') }} ++ ++ + {{ t('setting.tagsView') }} ++ ++ + {{ t('setting.tagsViewIcon') }} ++ ++ + {{ t('setting.logo') }} ++ ++ + {{ t('setting.uniqueOpened') }} ++ ++ + {{ t('setting.fixedHeader') }} ++ ++ + {{ t('setting.greyMode') }} ++ ++ + {{ t('setting.dynamicRouter') }} ++ ++ + {{ t('setting.serverDynamicRouter') }} ++ ++ + {{ t('setting.fixedMenu') }} +++ + + + ++ + + diff --git a/src/components/SizeDropdown/index.ts b/src/components/SizeDropdown/index.ts new file mode 100644 index 0000000..516488d --- /dev/null +++ b/src/components/SizeDropdown/index.ts @@ -0,0 +1,3 @@ +import SizeDropdown from './src/SizeDropdown.vue' + +export { SizeDropdown } diff --git a/src/components/SizeDropdown/src/SizeDropdown.vue b/src/components/SizeDropdown/src/SizeDropdown.vue new file mode 100644 index 0000000..1edbf74 --- /dev/null +++ b/src/components/SizeDropdown/src/SizeDropdown.vue @@ -0,0 +1,39 @@ + + + ++ +++ + diff --git a/src/components/TabMenu/index.ts b/src/components/TabMenu/index.ts new file mode 100644 index 0000000..b5fd71c --- /dev/null +++ b/src/components/TabMenu/index.ts @@ -0,0 +1,3 @@ +import TabMenu from './src/TabMenu.vue' + +export { TabMenu } diff --git a/src/components/TabMenu/src/TabMenu.vue b/src/components/TabMenu/src/TabMenu.vue new file mode 100644 index 0000000..28d3b62 --- /dev/null +++ b/src/components/TabMenu/src/TabMenu.vue @@ -0,0 +1,246 @@ + + + diff --git a/src/components/TabMenu/src/helper.ts b/src/components/TabMenu/src/helper.ts new file mode 100644 index 0000000..2213133 --- /dev/null +++ b/src/components/TabMenu/src/helper.ts @@ -0,0 +1,52 @@ +import { getAllParentPath } from '@/components/Menu/src/helper' +import type { RouteMeta } from 'vue-router' +import { isUrl } from '@/utils/is' +import { cloneDeep } from 'lodash-es' +import { reactive } from 'vue' + +export type TabMapTypes = { + [key: string]: string[] +} + +export const tabPathMap = reactive+ + + + ++ {{ t(`size.${item}`) }} + +({}) + +export const initTabMap = (routes: AppRouteRecordRaw[]) => { + for (const v of routes) { + const meta = (v.meta ?? {}) as RouteMeta + if (!meta?.hidden) { + tabPathMap[v.path] = [] + } + } +} + +export const filterMenusPath = ( + routes: AppRouteRecordRaw[], + allRoutes: AppRouteRecordRaw[] +): AppRouteRecordRaw[] => { + const res: AppRouteRecordRaw[] = [] + for (const v of routes) { + let data: Nullable = null + const meta = (v.meta ?? {}) as RouteMeta + if (!meta.hidden || meta.canTo) { + const allParentPath = getAllParentPath (allRoutes, v.path) + + const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/') + + data = cloneDeep(v) + data.path = fullPath + if (v.children && data) { + data.children = filterMenusPath(v.children, allRoutes) + } + + if (data) { + res.push(data) + } + + if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) { + tabPathMap[allParentPath[0]].push(fullPath) + } + } + } + + return res +} diff --git a/src/components/Table/index.ts b/src/components/Table/index.ts new file mode 100644 index 0000000..472ae5d --- /dev/null +++ b/src/components/Table/index.ts @@ -0,0 +1,21 @@ +import Table from './src/Table.vue' +import { ElTable } from 'element-plus' +import { TableColumn, TableSetProps } from './src/types' + +export type { + TableColumn, + TableSlotDefault, + Pagination, + TableSetProps, + TableProps +} from './src/types' + +export interface TableExpose { + setProps: (props: Recordable) => void + setColumn: (columnProps: TableSetProps[]) => void + addColumn: (column: TableColumn, index?: number) => void + delColumn: (field: string) => void + elTableRef: ComponentRef +} + +export { Table } diff --git a/src/components/Table/src/Table.vue b/src/components/Table/src/Table.vue new file mode 100644 index 0000000..c01085c --- /dev/null +++ b/src/components/Table/src/Table.vue @@ -0,0 +1,588 @@ + diff --git a/src/components/Table/src/components/ColumnSetting.vue b/src/components/Table/src/components/ColumnSetting.vue new file mode 100644 index 0000000..94eaaf5 --- /dev/null +++ b/src/components/Table/src/components/ColumnSetting.vue @@ -0,0 +1,166 @@ + + + + + + diff --git a/src/components/Table/src/components/TableActions.vue b/src/components/Table/src/components/TableActions.vue new file mode 100644 index 0000000..c62cb48 --- /dev/null +++ b/src/components/Table/src/components/TableActions.vue @@ -0,0 +1,105 @@ + diff --git a/src/components/Table/src/helper/index.ts b/src/components/Table/src/helper/index.ts new file mode 100644 index 0000000..d8b34a8 --- /dev/null +++ b/src/components/Table/src/helper/index.ts @@ -0,0 +1,8 @@ +export const setIndex = (reserveIndex: boolean, index: number, size: number, current: number) => { + const newIndex = index + 1 + if (reserveIndex) { + return size * (current - 1) + newIndex + } else { + return newIndex + } +} diff --git a/src/components/Table/src/types/index.ts b/src/components/Table/src/types/index.ts new file mode 100644 index 0000000..ef7ddbf --- /dev/null +++ b/src/components/Table/src/types/index.ts @@ -0,0 +1,97 @@ +import { TableProps as ElTableProps } from 'element-plus' +export interface TableColumn { + field: string + label?: string + type?: string + /** + * 是否隐藏 + */ + hidden?: boolean + children?: TableColumn[] + slots?: { + default?: (...args: any[]) => JSX.Element | JSX.Element[] | null + header?: (...args: any[]) => JSX.Element | null + } + index?: number | ((index: number) => number) + columnKey?: string + width?: string | number + minWidth?: string | number + fixed?: boolean | 'left' | 'right' + renderHeader?: (...args: any[]) => JSX.Element | null + // sortable?: boolean + sortMethod?: (...args: any[]) => number + sortBy?: string | string[] | ((...args: any[]) => string | string[]) + sortOrders?: (string | null)[] + resizable?: boolean + formatter?: (...args: any[]) => any + showOverflowTooltip?: boolean + align?: 'left' | 'center' | 'right' + headerAlign?: 'left' | 'center' | 'right' + className?: string + labelClassName?: string + selectable?: (...args: any[]) => boolean + reserveSelection?: boolean + filters?: Array<{ text: string; value: string }> + filterPlacement?: string + filterMultiple?: boolean + filterMethod?: (...args: any[]) => boolean + filteredValue?: string[] + [key: string]: any +} + +export interface TableSlotDefault { + row: Recordable + column: TableColumn + $index: number + [key: string]: any +} + +export interface Pagination { + small?: boolean + background?: boolean + pageSize?: number + defaultPageSize?: number + total?: number + pageCount?: number + pagerCount?: number + currentPage?: number + defaultCurrentPage?: number + layout?: string + pageSizes?: number[] + popperClass?: string + prevText?: string + nextText?: string + disabled?: boolean + hideOnSinglePage?: boolean +} + +export interface TableSetProps { + field: string + path: string + value: any +} + +export interface TableProps extends Omit++ ++++++ {{ checkColumns.length }} / {{ settingColumns?.length }} +固定 / 排序 ++++ ++ ++++ {{ item.label }} + ++++ + ++ ++ + ++ + ++ +++ +还原 +确定 +>, 'data'> { + pageSize?: number + currentPage?: number + showAction?: boolean + // 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip, + showOverflowTooltip?: boolean + // 表头 + columns?: TableColumn[] + // 是否展示分页 + pagination?: Pagination | undefined + // 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key) + reserveSelection?: boolean + // 加载状态 + loading?: boolean + // 是否叠加索引 + reserveIndex?: boolean + // 对齐方式 + align?: 'left' | 'center' | 'right' + // 表头对齐方式 + headerAlign?: 'left' | 'center' | 'right' + preview?: string[] + sortable?: boolean + data?: Recordable +} diff --git a/src/components/TagsView/index.ts b/src/components/TagsView/index.ts new file mode 100644 index 0000000..30e604a --- /dev/null +++ b/src/components/TagsView/index.ts @@ -0,0 +1,3 @@ +import TagsView from './src/TagsView.vue' + +export { TagsView } diff --git a/src/components/TagsView/src/TagsView.vue b/src/components/TagsView/src/TagsView.vue new file mode 100644 index 0000000..b4876c5 --- /dev/null +++ b/src/components/TagsView/src/TagsView.vue @@ -0,0 +1,585 @@ + + + + + ++ + + diff --git a/src/components/TagsView/src/helper.ts b/src/components/TagsView/src/helper.ts new file mode 100644 index 0000000..22f6a50 --- /dev/null +++ b/src/components/TagsView/src/helper.ts @@ -0,0 +1,21 @@ +import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router' +import { pathResolve } from '@/utils/routerHelper' + +export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => { + let tags: RouteLocationNormalizedLoaded[] = [] + routes.forEach((route) => { + const meta = route.meta as RouteMeta + const tagPath = pathResolve(parentPath, route.path) + if (meta?.affix) { + tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded) + } + if (route.children) { + const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath) + if (tempTags.length >= 1) { + tags = [...tags, ...tempTags] + } + } + }) + + return tags +} diff --git a/src/components/ThemeSwitch/index.ts b/src/components/ThemeSwitch/index.ts new file mode 100644 index 0000000..823a276 --- /dev/null +++ b/src/components/ThemeSwitch/index.ts @@ -0,0 +1,3 @@ +import ThemeSwitch from './src/ThemeSwitch.vue' + +export { ThemeSwitch } diff --git a/src/components/ThemeSwitch/src/ThemeSwitch.vue b/src/components/ThemeSwitch/src/ThemeSwitch.vue new file mode 100644 index 0000000..acf6c31 --- /dev/null +++ b/src/components/ThemeSwitch/src/ThemeSwitch.vue @@ -0,0 +1,50 @@ + + + +++ + ++ ++++ ++++ ++++ {{ t(item?.meta?.title as string) }} + + + + ++ + + diff --git a/src/components/UserInfo/index.ts b/src/components/UserInfo/index.ts new file mode 100644 index 0000000..c3a34ab --- /dev/null +++ b/src/components/UserInfo/index.ts @@ -0,0 +1,3 @@ +import UserInfo from './src/UserInfo.vue' + +export { UserInfo } diff --git a/src/components/UserInfo/src/UserInfo.vue b/src/components/UserInfo/src/UserInfo.vue new file mode 100644 index 0000000..4685f01 --- /dev/null +++ b/src/components/UserInfo/src/UserInfo.vue @@ -0,0 +1,84 @@ + + + + + + +++ ++ {{ + userStore.getUserInfo?.username + }} +
+ + ++ +{{ t('lock.lockScreen') }}++ +{{ t('common.loginOut') }}++ + + + + diff --git a/src/components/UserInfo/src/components/LockDialog.vue b/src/components/UserInfo/src/components/LockDialog.vue new file mode 100644 index 0000000..a5f0164 --- /dev/null +++ b/src/components/UserInfo/src/components/LockDialog.vue @@ -0,0 +1,101 @@ + + + + + + + diff --git a/src/components/UserInfo/src/components/LockPage.vue b/src/components/UserInfo/src/components/LockPage.vue new file mode 100644 index 0000000..bf10b60 --- /dev/null +++ b/src/components/UserInfo/src/components/LockPage.vue @@ -0,0 +1,272 @@ + + + ++ ++ ++ + + diff --git a/src/components/VideoPlayer/index.ts b/src/components/VideoPlayer/index.ts new file mode 100644 index 0000000..247deca --- /dev/null +++ b/src/components/VideoPlayer/index.ts @@ -0,0 +1,27 @@ +import { VNode, createVNode, render } from 'vue' +import VideoPlayer from './src/VideoPlayer.vue' +import { isClient } from '@/utils/is' +import { VideoPlayerViewer } from '@/components/VideoPlayerViewer' +import { toAnyString } from '@/utils' + +export { VideoPlayer } + +let instance: Nullable++ ++ {{ t('lock.unlock') }} + +++ {{ hour }} + + {{ meridiem }} + +++ {{ minute }} +++ + ++++++++ Archer +
+ + {{ t('lock.message') }} + + +++ {{ t('common.back') }} + ++ {{ t('lock.backToLogin') }} + ++ {{ t('lock.entrySystem') }} + ++++ {{ hour }}:{{ minute }} {{ meridiem }} ++{{ year }}/{{ month }}/{{ day }} {{ week }}+= null + +export function createVideoViewer(options: { url: string; poster?: string; show?: boolean }) { + if (!isClient) return + const { url, poster } = options + + const propsData: Partial<{ url: string; poster?: string; show?: boolean; id?: string }> = {} + const container = document.createElement('div') + const id = toAnyString() + container.id = id + propsData.url = url + propsData.poster = poster + propsData.show = true + propsData.id = id + + document.body.appendChild(container) + instance = createVNode(VideoPlayerViewer, propsData) + render(instance, container) +} diff --git a/src/components/VideoPlayer/src/VideoPlayer.vue b/src/components/VideoPlayer/src/VideoPlayer.vue new file mode 100644 index 0000000..4fd639a --- /dev/null +++ b/src/components/VideoPlayer/src/VideoPlayer.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/components/VideoPlayerViewer/index.ts b/src/components/VideoPlayerViewer/index.ts new file mode 100644 index 0000000..7f99155 --- /dev/null +++ b/src/components/VideoPlayerViewer/index.ts @@ -0,0 +1,3 @@ +import VideoPlayerViewer from './src/VideoPlayerViewer.vue' + +export { VideoPlayerViewer } diff --git a/src/components/VideoPlayerViewer/src/VideoPlayerViewer.vue b/src/components/VideoPlayerViewer/src/VideoPlayerViewer.vue new file mode 100644 index 0000000..73f6f2d --- /dev/null +++ b/src/components/VideoPlayerViewer/src/VideoPlayerViewer.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..218d5f4 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,8 @@ +import type { App } from 'vue' +import { Icon } from './Icon' +import { BaseButton } from './Button' + +export const setupGlobCom = (app: App+++++ + ): void => { + app.component('Icon', Icon) + app.component('BaseButton', BaseButton) +} diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..3774e27 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,34 @@ +/** + * 请求成功状态码 + */ +export const SUCCESS_CODE = 0 + +/** + * 请求contentType + */ +export const CONTENT_TYPE: AxiosContentType = 'application/json' + +/** + * 请求超时时间 + */ +export const REQUEST_TIMEOUT = 60000 + +/** + * 不重定向白名单 + */ +export const NO_REDIRECT_WHITE_LIST = ['/login'] + +/** + * 不重置路由白名单 + */ +export const NO_RESET_WHITE_LIST = ['Redirect', 'RedirectParent', 'Login', 'NoFind', 'Root'] + +/** + * 表格默认过滤列设置字段 + */ +export const DEFAULT_FILTER_COLUMN = ['expand', 'selection'] + +/** + * 是否根据headers->content-type自动转换数据格式 + */ +export const TRANSFORM_REQUEST_DATA = true diff --git a/src/dark-theme.css b/src/dark-theme.css deleted file mode 100644 index df9e807..0000000 --- a/src/dark-theme.css +++ /dev/null @@ -1,85 +0,0 @@ -html.dark { - color-scheme:dark; - --el-bg-color:#242830; - --el-bg-color-page:#1b1e24; - --el-bg-color-overlay:#2a2f38; - --el-text-color-primary:#e5e8ed; - --el-text-color-regular:#c0c6cf; - --el-text-color-secondary:#9ba3ae; - --el-border-color:#414751; - --el-border-color-light:#373d47; - --el-border-color-lighter:#303640; - --el-fill-color-blank:#242830; - --el-fill-color-light:#303640; - --el-fill-color-lighter:#2a2f38; - --el-fill-color:#343a44; - --el-color-primary:#82a9dc; - --el-color-success:#66b587; - --el-color-warning:#d5a348; - --el-color-danger:#d27676} -html.dark body,html.dark .app-shell,html.dark .login-screen { - background:#1b1e24; - color:#e5e8ed} -html.dark .sidebar,html.dark .topbar,html.dark .page-tabs,html.dark .panel,html.dark .stat-card,html.dark .cover-card,html.dark .add-cover,html.dark .login-card { - background:#242830; - border-color:#3b414b} -html.dark .brand { - border-color:#3b414b} -html.dark .brand b,html.dark .page-heading h1,html.dark .panel-heading h2,html.dark .stat-value,html.dark .drama-cell b,html.dark .episode-title b,html.dark .role-copy b,html.dark .login-heading h1 { - color:#e5e8ed} -html.dark .brand small,html.dark .page-heading p,html.dark .panel-heading p,html.dark .stat-top,html.dark .breadcrumb,html.dark .drama-table td,html.dark .role-copy small,html.dark .role-copy span,html.dark .episode-title small,html.dark .login-heading p { - color:#a6aeb9} -html.dark .side-menu,html.dark .side-menu>.el-menu-item,html.dark .side-menu .el-sub-menu__title { - background:transparent; - color:#c0c6cf} -html.dark .side-menu .el-menu-item:hover,html.dark .side-menu .el-sub-menu__title:hover { - background:#2e3540; - color:#e5e8ed} -html.dark .side-menu .el-menu-item.is-active { - background:#303d50; - color:#a8c7f0} -html.dark .drama-table th { - background:#2b3038; - color:#b1b8c2} -html.dark .drama-table td,html.dark .episode-row,html.dark .role-card,html.dark .page-tabs { - border-color:#3b414b} -html.dark .episode-row,html.dark .role-card { - background:#242830} -html.dark .role-active { - background:#2d3540; - border-color:#607998} -html.dark .episode-thumb,html.dark .drama-cover,html.dark .cover-placeholder { - background:#353b45!important; - color:#b9c1cb} -html.dark .permission-note,html.dark .library-toolbar { - background:#2b3038; - border-color:#3b414b} -html.dark .page-tabs .el-tabs__item { - color:#b8c0ca} -html.dark .page-tabs .el-tabs__item.is-active { - background:#2b3340; - color:#a8c7f0; - border-color:#3b414b!important} -html.dark .cover-card-info b,html.dark .cover-card-info small,html.dark .selected-file { - color:#d3d8e0} -html.dark .login-card { - box-shadow:0 8px 30px #0003} -html.dark .demo-credentials { - border-color:#3b414b; - color:#aab2bc} -html.dark .el-dialog { - background:#242830} -html.dark .el-dialog__title,html.dark .el-form-item__label { - color:#e1e5eb} -html.dark .el-upload-dragger { - background:#2b3038; - border-color:#4a515c} -html.dark .fullscreen-button,html.dark .theme-button,html.dark .logout-button,html.dark .role-switch { - color:#c5ccd5!important} -.theme-button { - font-size:18px!important; - color:#56616e!important; - padding:7px!important} -.theme-button:hover { - color:#355f97!important; - background:#f1f4f7!important} diff --git a/src/data/customMenus.js b/src/data/customMenus.js index 874bbce..b0fdb34 100644 --- a/src/data/customMenus.js +++ b/src/data/customMenus.js @@ -1,11 +1,22 @@ import { reactive } from 'vue' const storageKey = 'reelshort-custom-menus' -let savedMenus = [] -try { savedMenus = JSON.parse(localStorage.getItem(storageKey) || '[]') } catch { savedMenus = [] } +let savedMenus +try { + savedMenus = JSON.parse(localStorage.getItem(storageKey) || '[]') +} catch { + savedMenus = [] +} export const customMenus = reactive(Array.isArray(savedMenus) ? savedMenus : []) -export const menuIconNames = ['Document', 'DataAnalysis', 'Picture', 'User', 'Setting', 'VideoCamera'] +export const menuIconNames = [ + 'Document', + 'DataAnalysis', + 'Picture', + 'User', + 'Setting', + 'VideoCamera' +] export function saveCustomMenus() { localStorage.setItem(storageKey, JSON.stringify(customMenus)) diff --git a/src/data/dramas.js b/src/data/dramas.js index f5114ca..01342c7 100644 --- a/src/data/dramas.js +++ b/src/data/dramas.js @@ -1,9 +1,59 @@ import { reactive } from 'vue' export const dramas = reactive([ - { id: 1, title: '替嫁新娘:总裁的掌心宠', genre: '都市情感', status: '已上线', episodes: 68, views: '2.4M', updatedAt: '2025-06-12', description: '一场意外替嫁,让她成为冷面总裁的契约新娘。', cover: '' }, - { id: 2, title: '重生之我在豪门当千金', genre: '重生逆袭', status: '审核中', episodes: 42, views: '—', updatedAt: '2025-06-18', description: '重活一世,她要拿回属于自己的一切。', cover: '' }, - { id: 3, title: '心动信号:请和我恋爱', genre: '甜宠爱情', status: '已上线', episodes: 56, views: '1.8M', updatedAt: '2025-05-29', description: '恋爱观察室里,心动正在悄悄发生。', cover: '' }, - { id: 4, title: '她从深渊中走来', genre: '悬疑复仇', status: '草稿', episodes: 24, views: '—', updatedAt: '2025-06-20', description: '真相只有一个,而她亲手揭开迷雾。', cover: '' }, - { id: 5, title: '闪婚后,傅先生每天都在撒糖', genre: '都市情感', status: '已上线', episodes: 72, views: '3.1M', updatedAt: '2025-05-21', description: '一纸婚约,开启了甜蜜又心动的同居生活。', cover: '' }, + { + id: 1, + title: '替嫁新娘:总裁的掌心宠', + genre: '都市情感', + status: '已上线', + episodes: 68, + views: '2.4M', + updatedAt: '2025-06-12', + description: '一场意外替嫁,让她成为冷面总裁的契约新娘。', + cover: '' + }, + { + id: 2, + title: '重生之我在豪门当千金', + genre: '重生逆袭', + status: '审核中', + episodes: 42, + views: '—', + updatedAt: '2025-06-18', + description: '重活一世,她要拿回属于自己的一切。', + cover: '' + }, + { + id: 3, + title: '心动信号:请和我恋爱', + genre: '甜宠爱情', + status: '已上线', + episodes: 56, + views: '1.8M', + updatedAt: '2025-05-29', + description: '恋爱观察室里,心动正在悄悄发生。', + cover: '' + }, + { + id: 4, + title: '她从深渊中走来', + genre: '悬疑复仇', + status: '草稿', + episodes: 24, + views: '—', + updatedAt: '2025-06-20', + description: '真相只有一个,而她亲手揭开迷雾。', + cover: '' + }, + { + id: 5, + title: '闪婚后,傅先生每天都在撒糖', + genre: '都市情感', + status: '已上线', + episodes: 72, + views: '3.1M', + updatedAt: '2025-05-21', + description: '一纸婚约,开启了甜蜜又心动的同居生活。', + cover: '' + } ]) diff --git a/src/data/episodes.js b/src/data/episodes.js index 11ffcb9..1141d01 100644 --- a/src/data/episodes.js +++ b/src/data/episodes.js @@ -1,8 +1,40 @@ import { reactive } from 'vue' export const episodeData = reactive([ - { id: 1, dramaId: 1, number: 1, title: '命运的相遇', duration: '02:18', status: '已上线', views: '48,201' }, - { id: 2, dramaId: 1, number: 2, title: '意外的婚约', duration: '01:56', status: '已上线', views: '42,817' }, - { id: 3, dramaId: 1, number: 3, title: '心动的开始', duration: '02:04', status: '已上线', views: '39,506' }, - { id: 4, dramaId: 1, number: 4, title: '他的秘密', duration: '02:31', status: '待发布', views: '—' }, + { + id: 1, + dramaId: 1, + number: 1, + title: '命运的相遇', + duration: '02:18', + status: '已上线', + views: '48,201' + }, + { + id: 2, + dramaId: 1, + number: 2, + title: '意外的婚约', + duration: '01:56', + status: '已上线', + views: '42,817' + }, + { + id: 3, + dramaId: 1, + number: 3, + title: '心动的开始', + duration: '02:04', + status: '已上线', + views: '39,506' + }, + { + id: 4, + dramaId: 1, + number: 4, + title: '他的秘密', + duration: '02:31', + status: '待发布', + views: '—' + } ]) diff --git a/src/data/roles.js b/src/data/roles.js index f52f136..c297378 100644 --- a/src/data/roles.js +++ b/src/data/roles.js @@ -1,8 +1,40 @@ import { reactive } from 'vue' export const roleData = reactive([ - { id: 1, name: '超级管理员', description: '拥有系统全部管理权限', memberCount: 2, category: '系统管理员', level: 1, status: '启用' }, - { id: 2, name: '内容运营', description: '管理短剧、剧集及封面素材', memberCount: 8, category: '内容管理', level: 2, status: '启用' }, - { id: 3, name: '审核专员', description: '审核短剧内容与发布状态', memberCount: 5, category: '内容管理', level: 3, status: '启用' }, - { id: 4, name: '数据分析师', description: '查看内容数据与运营报表', memberCount: 3, category: '数据分析', level: 4, status: '启用' }, + { + id: 1, + name: '超级管理员', + description: '拥有系统全部管理权限', + memberCount: 2, + category: '系统管理员', + level: 1, + status: '启用' + }, + { + id: 2, + name: '内容运营', + description: '管理短剧、剧集及封面素材', + memberCount: 8, + category: '内容管理', + level: 2, + status: '启用' + }, + { + id: 3, + name: '审核专员', + description: '审核短剧内容与发布状态', + memberCount: 5, + category: '内容管理', + level: 3, + status: '启用' + }, + { + id: 4, + name: '数据分析师', + description: '查看内容数据与运营报表', + memberCount: 3, + category: '数据分析', + level: 4, + status: '启用' + } ]) diff --git a/src/drama-management.css b/src/drama-management.css deleted file mode 100644 index 6c84856..0000000 --- a/src/drama-management.css +++ /dev/null @@ -1,252 +0,0 @@ -.library-toolbar { - justify-content:flex-start; - gap:16px; - min-height:64px; - padding:12px 0!important} -.drama-filters { - display:flex; - align-items:center; - gap:10px; - flex:0 0 auto} -.drama-filters>.el-input { - width:300px!important; - flex:0 0 300px!important} -.drama-filters>.el-select { - width:160px!important; - flex:0 0 160px!important} -.table-result-count { - margin-left:auto; - color:#858b95; - font-size:13px; - white-space:nowrap} -.table-actions { - display:flex; - align-items:center; - gap:8px; - white-space:nowrap} -.table-actions .el-button { - font-size:13px!important; - font-weight:400!important; - padding:5px 4px!important} -.table-actions .danger-action { - color:#a66a6a!important} -.table-actions .danger-action:hover { - color:#b54848!important} -.drama-cell .drama-cover,.drama-cell .drama-cover-image { - width:44px; - height:56px; - flex:0 0 44px; - border-radius:4px; - object-fit:cover} -.drama-cell .drama-cover { - display:grid; - place-content:center; - text-align:center; - font-size:10px; - line-height:1.3; - color:#7e8791; - background:#f0f2f4!important} -.dark .table-result-count { - color:#a6aeb9} -.dark .drama-cell .drama-cover { - background:#353b45!important; - color:#b9c1cb} -@media(max-width:800px) { - .drama-filters>.el-input { - width:min(300px,45vw)!important; - flex-basis:min(300px,45vw)!important} -.drama-filters>.el-select { - width:150px!important; - flex-basis:150px!important} -.table-result-count { - display:none} -} -.drama-el-table,.dashboard-el-table { - width:100%; - font-size:13px; - --el-table-header-bg-color:#f7f8fa; - --el-table-border-color:#eaecf0; - --el-table-row-hover-bg-color:#f8fafc} -.drama-el-table .el-table__cell,.dashboard-el-table .el-table__cell { - padding:14px 0} -.drama-el-table th.el-table__cell,.dashboard-el-table th.el-table__cell { - font-size:12px; - font-weight:600; - color:#737b85} -.drama-el-table td.el-table__cell,.dashboard-el-table td.el-table__cell { - font-size:13px; - color:#505862} -.drama-el-table .cell,.dashboard-el-table .cell { - line-height:1.5} -.drama-el-table .el-table__inner-wrapper:before,.dashboard-el-table .el-table__inner-wrapper:before { - height:0} -.dark .drama-el-table,.dark .dashboard-el-table { - --el-table-header-bg-color:#2b3038; - --el-table-tr-bg-color:#242830; - --el-table-bg-color:#242830; - --el-table-border-color:#3b414b; - --el-table-row-hover-bg-color:#2e3540; - --el-table-text-color:#c0c6cf; - --el-table-header-text-color:#b1b8c2} -.dark .drama-el-table td.el-table__cell,.dark .dashboard-el-table td.el-table__cell { - color:#c0c6cf} -.table-actions .el-button { - font-size:13px!important; - font-weight:600!important; - padding:7px 11px!important} -.table-actions .el-button--primary { - --el-button-bg-color:#456fae; - --el-button-border-color:#456fae; - --el-button-hover-bg-color:#385f97; - --el-button-hover-border-color:#385f97} -.table-actions .el-button--success { - --el-button-bg-color:#4d866a; - --el-button-border-color:#4d866a; - --el-button-hover-bg-color:#3c7358; - --el-button-hover-border-color:#3c7358} -.table-actions .el-button--danger { - --el-button-bg-color:#bd5b5b; - --el-button-border-color:#bd5b5b; - --el-button-hover-bg-color:#a94b4b; - --el-button-hover-border-color:#a94b4b} -.pagination { - display:flex; - justify-content:flex-end; - align-items:center; - padding:18px 0 4px!important; - overflow-x:auto} -.pagination .el-pagination { - font-size:13px!important; - --el-pagination-font-size:13px; - --el-pagination-button-bg-color:#f1f3f6; - --el-pagination-hover-color:#456fae; - --el-pagination-button-color:#4c5561; - gap:5px} -.pagination .el-pagination__sizes { - margin-right:8px} -.pagination .el-select { - width:112px} -.pagination .el-pagination__jump { - margin-left:8px; - white-space:nowrap} -.dark .pagination .el-pagination { - --el-pagination-button-bg-color:#353b45; - --el-pagination-button-color:#c3cad3} -.drama-el-table td.el-table__cell,.drama-el-table th.el-table__cell { - font-size:14px} -.drama-cell b { - font-size:15px} -.drama-cell small { - font-size:13px} -.table-result-count { - font-size:14px} -.table-actions .el-button { - font-size:14px!important} -.library-toolbar .el-input__inner,.library-toolbar .el-select__placeholder { - font-size:14px} -.pagination .el-pagination { - font-size:14px!important; - --el-pagination-font-size:14px} -.el-message-box__title,.el-message-box__message { - font-size:15px} -.el-message-box__btns .el-button { - font-size:14px} -.episode-toolbar { - margin-bottom:16px} -.episode-el-table { - margin-top:4px} -.episode-table-title b,.episode-table-title small { - display:block} -.episode-table-title b { - font-size:14px; - color:#343a43} -.episode-table-title small { - margin-top:4px; - font-size:13px; - color:#858b95} -.dark .episode-table-title b { - color:#e5e8ed} -.dark .episode-table-title small { - color:#a6aeb9} -.menu-management-panel { - margin-top:20px} -.menu-management-panel .panel-heading { - display:flex; - align-items:center; - justify-content:space-between; - gap:16px} -.menu-management-panel .el-alert { - margin-top:4px} -.menu-management-panel .el-select { - width:100%} -.admin-user-search { - width:min(440px,65vw)} -.muted-text { - font-size:13px; - color:#9298a1} -.role-permission-layout { - display:grid; - grid-template-columns:minmax(520px,1.25fr) minmax(340px,.9fr); - gap:18px; - align-items:start} -.role-table-panel,.permission-tree-panel { - min-width:0} -.role-config-table .el-table__row { - cursor:pointer} -.permission-tree-panel .el-tree { - max-height:68vh; - overflow:auto; - padding:5px 8px} -.permission-tree-panel .el-tree-node__label { - font-size:14px} -.permission-warning { - color:#d66b4f!important} -.admin-users-panel { - overflow:hidden} -.admin-user-toolbar { - display:flex; - align-items:center; - gap:10px; - flex-wrap:wrap; - padding:0 0 16px} -.admin-user-toolbar>.el-input { - width:230px} -.admin-user-toolbar>.el-select { - width:190px} -.admin-users-table { - min-width:1250px} -.menu-editor-layout { - display:grid; - grid-template-columns:minmax(270px,.7fr) minmax(520px,1.5fr); - gap:18px; - align-items:start} -.menu-tree-panel,.menu-form-panel { - min-width:0} -.menu-tree-panel .el-tree { - max-height:70vh; - overflow:auto} -.menu-form-panel .panel-heading { - display:flex; - justify-content:space-between; - align-items:center} -.menu-editor-form { - max-width:680px; - padding:16px 12px 0} -.menu-editor-form .el-input,.menu-editor-form .el-select { - max-width:360px; - width:100%} -.menu-editor-form .el-textarea { - max-width:360px} -.menu-editor-form .el-input-number { - width:180px} -.menu-form-footer { - padding:12px 0 4px 122px; - border-top:1px solid #eceef0} -.dark .menu-form-footer { - border-color:#3b414b} -@media(max-width:1150px) { - .role-permission-layout,.menu-editor-layout { - grid-template-columns:1fr} -.permission-tree-panel .el-tree,.menu-tree-panel .el-tree { - max-height:45vh} -} diff --git a/src/hooks/event/useEmitt.ts b/src/hooks/event/useEmitt.ts new file mode 100644 index 0000000..26b5d62 --- /dev/null +++ b/src/hooks/event/useEmitt.ts @@ -0,0 +1,23 @@ +import mitt from 'mitt' +import { onBeforeUnmount } from 'vue' + +interface Option { + name: string // 事件名称 + callback: Fn // 回调 +} + +const emitter = mitt() + +export const useEmitt = (option?: Option) => { + if (option) { + emitter.on(option.name, option.callback) + + onBeforeUnmount(() => { + emitter.off(option.name) + }) + } + + return { + emitter + } +} diff --git a/src/hooks/event/useScrollTo.ts b/src/hooks/event/useScrollTo.ts new file mode 100644 index 0000000..74fd673 --- /dev/null +++ b/src/hooks/event/useScrollTo.ts @@ -0,0 +1,62 @@ +import { ref, unref } from 'vue' + +export interface ScrollToParams { + el: HTMLElement + to: number + position: string + duration?: number + callback?: () => void +} + +const easeInOutQuad = (t: number, b: number, c: number, d: number) => { + t /= d / 2 + if (t < 1) { + return (c / 2) * t * t + b + } + t-- + return (-c / 2) * (t * (t - 2) - 1) + b +} +const move = (el: HTMLElement, position: string, amount: number) => { + el[position] = amount +} + +export function useScrollTo({ + el, + position = 'scrollLeft', + to, + duration = 500, + callback +}: ScrollToParams) { + const isActiveRef = ref(false) + const start = el[position] + const change = to - start + const increment = 20 + let currentTime = 0 + + function animateScroll() { + if (!unref(isActiveRef)) { + return + } + currentTime += increment + const val = easeInOutQuad(currentTime, start, change, duration) + move(el, position, val) + if (currentTime < duration && unref(isActiveRef)) { + requestAnimationFrame(animateScroll) + } else { + if (callback) { + callback() + } + } + } + + function run() { + isActiveRef.value = true + animateScroll() + } + + function stop() { + isActiveRef.value = false + } + + return { start: run, stop } +} diff --git a/src/hooks/web/useConfigGlobal.ts b/src/hooks/web/useConfigGlobal.ts new file mode 100644 index 0000000..d1e490b --- /dev/null +++ b/src/hooks/web/useConfigGlobal.ts @@ -0,0 +1,10 @@ +import { ConfigGlobalTypes } from '@/components/ConfigGlobal' +import { inject } from 'vue' + +export const useConfigGlobal = () => { + const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes + + return { + configGlobal + } +} diff --git a/src/hooks/web/useCrudSchemas.ts b/src/hooks/web/useCrudSchemas.ts new file mode 100644 index 0000000..1115a81 --- /dev/null +++ b/src/hooks/web/useCrudSchemas.ts @@ -0,0 +1,158 @@ +import { reactive } from 'vue' +import { eachTree, treeMap, filter } from '@/utils/tree' +import { FormSchema } from '@/components/Form' +import { TableColumn } from '@/components/Table' +import { DescriptionsSchema } from '@/components/Descriptions' + +export type CrudSchema = Omit & { + search?: CrudSearchParams + table?: CrudTableParams + form?: CrudFormParams + detail?: CrudDescriptionsParams + children?: CrudSchema[] +} + +interface CrudSearchParams extends Omit { + // 是否隐藏在查询项 + hidden?: boolean +} + +interface CrudTableParams extends Omit { + // 是否隐藏表头 + hidden?: boolean +} + +interface CrudFormParams extends Omit { + // 是否隐藏表单项 + hidden?: boolean +} + +interface CrudDescriptionsParams extends Omit { + // 是否隐藏表单项 + hidden?: boolean +} + +interface AllSchemas { + searchSchema: FormSchema[] + tableColumns: TableColumn[] + formSchema: FormSchema[] + detailSchema: DescriptionsSchema[] +} + +// 过滤所有结构 +export const useCrudSchemas = ( + crudSchema: CrudSchema[] +): { + allSchemas: AllSchemas +} => { + // 所有结构数据 + const allSchemas = reactive ({ + searchSchema: [], + tableColumns: [], + formSchema: [], + detailSchema: [] + }) + + const searchSchema = filterSearchSchema(crudSchema) + // @ts-ignore + allSchemas.searchSchema = searchSchema || [] + + const tableColumns = filterTableSchema(crudSchema) + allSchemas.tableColumns = tableColumns || [] + + const formSchema = filterFormSchema(crudSchema) + allSchemas.formSchema = formSchema + + const detailSchema = filterDescriptionsSchema(crudSchema) + allSchemas.detailSchema = detailSchema + + return { + allSchemas + } +} + +// 过滤 Search 结构 +const filterSearchSchema = (crudSchema: CrudSchema[]): FormSchema[] => { + const searchSchema: FormSchema[] = [] + const length = crudSchema.length + + for (let i = 0; i < length; i++) { + const schemaItem = crudSchema[i] + const searchSchemaItem = { + component: schemaItem?.search?.component || 'Input', + ...schemaItem.search, + field: schemaItem.field, + label: schemaItem.label + } + + searchSchema.push(searchSchemaItem) + } + + return searchSchema +} + +// 过滤 table 结构 +const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => { + const tableColumns = treeMap (crudSchema, { + conversion: (schema: CrudSchema) => { + if (!schema?.table?.hidden) { + return { + ...schema.table, + ...schema + } + } + } + }) + + // 第一次过滤会有 undefined 所以需要二次过滤 + return filter (tableColumns as TableColumn[], (data) => { + if (data.children === void 0) { + delete data.children + } + return !!data.field + }) +} + +// 过滤 form 结构 +const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => { + const formSchema: FormSchema[] = [] + const length = crudSchema.length + + for (let i = 0; i < length; i++) { + const formItem = crudSchema[i] + // 判断是否隐藏 + const formSchemaItem = { + component: formItem?.form?.component || 'Input', + ...formItem.form, + field: formItem.field, + label: formItem.label + } + + formSchema.push(formSchemaItem) + } + + return formSchema +} + +// 过滤 descriptions 结构 +const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => { + const descriptionsSchema: FormSchema[] = [] + + eachTree(crudSchema, (schemaItem: CrudSchema) => { + // 判断是否隐藏 + if (!schemaItem?.detail?.hidden) { + const descriptionsSchemaItem = { + ...schemaItem.detail, + field: schemaItem.field, + label: schemaItem.detail?.label || schemaItem.label + } + + // 删除不必要的字段 + delete descriptionsSchemaItem.hidden + + descriptionsSchema.push(descriptionsSchemaItem) + } + }) + + return descriptionsSchema +} diff --git a/src/hooks/web/useDesign.ts b/src/hooks/web/useDesign.ts new file mode 100644 index 0000000..1ec349f --- /dev/null +++ b/src/hooks/web/useDesign.ts @@ -0,0 +1,18 @@ +import variables from '@/styles/variables.module.less' + +export const useDesign = () => { + const lessVariables = variables + + /** + * @param scope 类名 + * @returns 返回空间名-类名 + */ + const getPrefixCls = (scope: string) => { + return `${lessVariables.namespace}-${scope}` + } + + return { + variables: lessVariables, + getPrefixCls + } +} diff --git a/src/hooks/web/useForm.ts b/src/hooks/web/useForm.ts new file mode 100644 index 0000000..3846210 --- /dev/null +++ b/src/hooks/web/useForm.ts @@ -0,0 +1,130 @@ +import type { Form, FormExpose } from '@/components/Form' +import type { ElForm, ElFormItem } from 'element-plus' +import { ref, unref, nextTick } from 'vue' +import { FormSchema, FormSetProps, FormProps } from '@/components/Form' + +export const useForm = () => { + // From实例 + const formRef = ref () + + // ElForm实例 + const elFormRef = ref >() + + /** + * @param ref Form实例 + * @param elRef ElForm实例 + */ + const register = (ref: typeof Form & FormExpose, elRef: ComponentRef ) => { + formRef.value = ref + elFormRef.value = elRef + } + + const getForm = async () => { + await nextTick() + const form = unref(formRef) + if (!form) { + console.error('The form is not registered. Please use the register method to register') + } + return form + } + + // 一些内置的方法 + const methods = { + /** + * @description 设置form组件的props + * @param props form组件的props + */ + setProps: async (props: FormProps = {}) => { + const form = await getForm() + form?.setProps(props) + if (props.model) { + form?.setValues(props.model) + } + }, + + /** + * @description 设置form的值 + * @param data 需要设置的数据 + */ + setValues: async (data: Recordable) => { + const form = await getForm() + form?.setValues(data) + }, + + /** + * @description 设置schema + * @param schemaProps 需要设置的schemaProps + */ + setSchema: async (schemaProps: FormSetProps[]) => { + const form = await getForm() + form?.setSchema(schemaProps) + }, + + /** + * @description 新增schema + * @param formSchema 需要新增数据 + * @param index 在哪里新增 + */ + addSchema: async (formSchema: FormSchema, index?: number) => { + const form = await getForm() + form?.addSchema(formSchema, index) + }, + + /** + * @description 删除schema + * @param field 删除哪个数据 + */ + delSchema: async (field: string) => { + const form = await getForm() + form?.delSchema(field) + }, + + /** + * @description 获取表单数据 + * @returns form data + */ + getFormData: async (): Promise => { + const form = await getForm() + return form?.formModel as T + }, + + /** + * @description 获取表单组件的实例 + * @param field 表单项唯一标识 + * @returns component instance + */ + getComponentExpose: async (field: string) => { + const form = await getForm() + return form?.getComponentExpose(field) + }, + + /** + * @description 获取formItem组件的实例 + * @param field 表单项唯一标识 + * @returns formItem instance + */ + getFormItemExpose: async (field: string) => { + const form = await getForm() + return form?.getFormItemExpose(field) as ComponentRef + }, + + /** + * @description 获取ElForm组件的实例 + * @returns ElForm instance + */ + getElFormExpose: async () => { + await getForm() + return unref(elFormRef) + }, + + getFormExpose: async () => { + await getForm() + return unref(formRef) + } + } + + return { + formRegister: register, + formMethods: methods + } +} diff --git a/src/hooks/web/useI18n.ts b/src/hooks/web/useI18n.ts new file mode 100644 index 0000000..f1e0e7b --- /dev/null +++ b/src/hooks/web/useI18n.ts @@ -0,0 +1,52 @@ +import { i18n } from '@/plugins/vueI18n' + +type I18nGlobalTranslation = { + (key: string): string + (key: string, locale: string): string + (key: string, locale: string, list: unknown[]): string + (key: string, locale: string, named: Record ): string + (key: string, list: unknown[]): string + (key: string, named: Record ): string +} + +type I18nTranslationRestParameters = [string, any] + +const getKey = (namespace: string | undefined, key: string) => { + if (!namespace) { + return key + } + if (key.startsWith(namespace)) { + return key + } + return `${namespace}.${key}` +} + +export const useI18n = ( + namespace?: string +): { + t: I18nGlobalTranslation +} => { + const normalFn = { + t: (key: string) => { + return getKey(namespace, key) + } + } + + if (!i18n) { + return normalFn + } + + const { t, ...methods } = i18n.global + + const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => { + if (!key) return '' + if (!key.includes('.') && !namespace) return key + return (t as any)(getKey(namespace, key), ...(arg as I18nTranslationRestParameters)) + } + return { + ...methods, + t: tFn + } +} + +export const t = (key: string) => key diff --git a/src/hooks/web/useIcon.ts b/src/hooks/web/useIcon.ts new file mode 100644 index 0000000..b76bb29 --- /dev/null +++ b/src/hooks/web/useIcon.ts @@ -0,0 +1,7 @@ +import { h } from 'vue' +import type { VNode } from 'vue' +import { Icon, IconTypes } from '@/components/Icon' + +export const useIcon = (props: IconTypes): VNode => { + return h(Icon, props) +} diff --git a/src/hooks/web/useLocale.ts b/src/hooks/web/useLocale.ts new file mode 100644 index 0000000..c65070e --- /dev/null +++ b/src/hooks/web/useLocale.ts @@ -0,0 +1,35 @@ +import { i18n } from '@/plugins/vueI18n' +import { useLocaleStoreWithOut } from '@/store/modules/locale' +import { setHtmlPageLang } from '@/plugins/vueI18n/helper' + +const setI18nLanguage = (locale: LocaleType) => { + const localeStore = useLocaleStoreWithOut() + + if (i18n.mode === 'legacy') { + i18n.global.locale = locale + } else { + ;(i18n.global.locale as any).value = locale + } + localeStore.setCurrentLocale({ + lang: locale + }) + setHtmlPageLang(locale) +} + +export const useLocale = () => { + // Switching the language will change the locale of useI18n + // And submit to configuration modification + const changeLocale = async (locale: LocaleType) => { + const globalI18n = i18n.global + + const langModule = await import(`../../locales/${locale}.ts`) + + globalI18n.setLocaleMessage(locale, langModule.default) + + setI18nLanguage(locale) + } + + return { + changeLocale + } +} diff --git a/src/hooks/web/useNProgress.ts b/src/hooks/web/useNProgress.ts new file mode 100644 index 0000000..28990e7 --- /dev/null +++ b/src/hooks/web/useNProgress.ts @@ -0,0 +1,34 @@ +import { nextTick, unref } from 'vue' +import type { NProgressOptions } from 'nprogress' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import { useCssVar } from '@vueuse/core' + +const primaryColor = useCssVar('--el-color-primary', document.documentElement) + +export const useNProgress = () => { + NProgress.configure({ showSpinner: false } as NProgressOptions) + + const initColor = async () => { + await nextTick() + const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef + if (bar) { + bar.style.background = unref(primaryColor.value) + } + } + + initColor() + + const start = () => { + NProgress.start() + } + + const done = () => { + NProgress.done() + } + + return { + start, + done + } +} diff --git a/src/hooks/web/useNow.ts b/src/hooks/web/useNow.ts new file mode 100644 index 0000000..09d3176 --- /dev/null +++ b/src/hooks/web/useNow.ts @@ -0,0 +1,60 @@ +import { dateUtil } from '@/utils/dateUtil' +import { reactive, toRefs } from 'vue' +import { tryOnMounted, tryOnUnmounted } from '@vueuse/core' + +export const useNow = (immediate = true) => { + let timer: IntervalHandle + + const state = reactive({ + year: 0, + month: 0, + week: '', + day: 0, + hour: '', + minute: '', + second: 0, + meridiem: '' + }) + + const update = () => { + const now = dateUtil() + + const h = now.format('HH') + const m = now.format('mm') + const s = now.get('s') + + state.year = now.get('y') + state.month = now.get('M') + 1 + state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()] + state.day = now.get('date') + state.hour = h + state.minute = m + state.second = s + + state.meridiem = now.format('A') + } + + function start() { + update() + clearInterval(timer) + timer = setInterval(() => update(), 1000) + } + + function stop() { + clearInterval(timer) + } + + tryOnMounted(() => { + immediate && start() + }) + + tryOnUnmounted(() => { + stop() + }) + + return { + ...toRefs(state), + start, + stop + } +} diff --git a/src/hooks/web/usePageLoading.ts b/src/hooks/web/usePageLoading.ts new file mode 100644 index 0000000..2f0e393 --- /dev/null +++ b/src/hooks/web/usePageLoading.ts @@ -0,0 +1,18 @@ +import { useAppStoreWithOut } from '@/store/modules/app' + +export const usePageLoading = () => { + const loadStart = () => { + const appStore = useAppStoreWithOut() + appStore.setPageLoading(true) + } + + const loadDone = () => { + const appStore = useAppStoreWithOut() + appStore.setPageLoading(false) + } + + return { + loadStart, + loadDone + } +} diff --git a/src/hooks/web/useSearch.ts b/src/hooks/web/useSearch.ts new file mode 100644 index 0000000..c3503ee --- /dev/null +++ b/src/hooks/web/useSearch.ts @@ -0,0 +1,91 @@ +import { ref, unref, nextTick } from 'vue' +import { FormSchema, FormSetProps } from '@/components/Form' +import { SearchExpose, SearchProps } from '@/components/Search' + +export const useSearch = () => { + // Search实例 + const searchRef = ref () + + /** + * @param ref Search实例 + * @param elRef ElForm实例 + */ + const register = (ref: SearchExpose) => { + searchRef.value = ref + } + + const getSearch = async () => { + await nextTick() + const search = unref(searchRef) + if (!search) { + console.error('The Search is not registered. Please use the register method to register') + } + return search + } + + // 一些内置的方法 + const methods = { + /** + * @description 设置search组件的props + * @param field FormItem的field + */ + setProps: async (props: SearchProps = {}) => { + const search = await getSearch() + search?.setProps(props) + if (props.model) { + search?.setValues(props.model) + } + }, + + /** + * @description 设置form的值 + * @param data 需要设置的数据 + */ + setValues: async (data: Recordable) => { + const search = await getSearch() + search?.setValues(data) + }, + + /** + * @description 设置schema + * @param schemaProps 需要设置的schemaProps + */ + setSchema: async (schemaProps: FormSetProps[]) => { + const search = await getSearch() + search?.setSchema(schemaProps) + }, + + /** + * @description 新增schema + * @param formSchema 需要新增数据 + * @param index 在哪里新增 + */ + addSchema: async (formSchema: FormSchema, index?: number) => { + const search = await getSearch() + search?.addSchema(formSchema, index) + }, + + /** + * @description 删除schema + * @param field 删除哪个数据 + */ + delSchema: async (field: string) => { + const search = await getSearch() + search?.delSchema(field) + }, + + /** + * @description 获取表单数据 + * @returns form data + */ + getFormData: async (): Promise => { + const search = await getSearch() + return search?.formModel as T + } + } + + return { + searchRegister: register, + searchMethods: methods + } +} diff --git a/src/hooks/web/useStorage.ts b/src/hooks/web/useStorage.ts new file mode 100644 index 0000000..cd077df --- /dev/null +++ b/src/hooks/web/useStorage.ts @@ -0,0 +1,31 @@ +import { isArray, isObject } from '@/utils/is' + +export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => { + const setStorage = (key: string, value: any) => { + window[type].setItem(key, isArray(value) || isObject(value) ? JSON.stringify(value) : value) + } + + const getStorage = (key: string) => { + const value = window[type].getItem(key) + try { + return JSON.parse(value || '') + } catch (error) { + return value + } + } + + const removeStorage = (key: string) => { + window[type].removeItem(key) + } + + const clear = () => { + window[type].clear() + } + + return { + setStorage, + getStorage, + removeStorage, + clear + } +} diff --git a/src/hooks/web/useTable.ts b/src/hooks/web/useTable.ts new file mode 100644 index 0000000..eaa75dd --- /dev/null +++ b/src/hooks/web/useTable.ts @@ -0,0 +1,193 @@ +import { useI18n } from '@/hooks/web/useI18n' +import { Table, TableExpose, TableProps, TableSetProps, TableColumn } from '@/components/Table' +import { ElTable, ElMessageBox, ElMessage } from 'element-plus' +import { ref, watch, unref, nextTick, onMounted } from 'vue' + +const { t } = useI18n() + +interface UseTableConfig { + /** + * 是否初始化的时候请求一次 + */ + immediate?: boolean + fetchDataApi: () => Promise<{ + list: any[] + total?: number + }> + fetchDelApi?: () => Promise +} + +export const useTable = (config: UseTableConfig) => { + const { immediate = true } = config + + const loading = ref(false) + const currentPage = ref(1) + const pageSize = ref(10) + const total = ref(0) + const dataList = ref ([]) + + watch( + () => currentPage.value, + () => { + methods.getList() + } + ) + + watch( + () => pageSize.value, + () => { + // 当前页不为1时,修改页数后会导致多次调用getList方法 + if (unref(currentPage) === 1) { + methods.getList() + } else { + currentPage.value = 1 + methods.getList() + } + } + ) + + onMounted(() => { + if (immediate) { + methods.getList() + } + }) + + // Table实例 + const tableRef = ref () + + // ElTable实例 + const elTableRef = ref >() + + const register = (ref: typeof Table & TableExpose, elRef: ComponentRef ) => { + tableRef.value = ref + elTableRef.value = unref(elRef) + } + + const getTable = async () => { + await nextTick() + const table = unref(tableRef) + if (!table) { + console.error('The table is not registered. Please use the register method to register') + } + return table + } + + const methods = { + /** + * 获取表单数据 + */ + getList: async () => { + loading.value = true + try { + const res = await config?.fetchDataApi() + console.log('fetchDataApi res', res) + if (res) { + dataList.value = res.list + total.value = res.total || 0 + } + } catch (err) { + console.log('fetchDataApi error') + } finally { + loading.value = false + } + }, + + /** + * @description 设置table组件的props + * @param props table组件的props + */ + setProps: async (props: TableProps = {}) => { + const table = await getTable() + table?.setProps(props) + }, + + /** + * @description 设置column + * @param columnProps 需要设置的列 + */ + setColumn: async (columnProps: TableSetProps[]) => { + const table = await getTable() + table?.setColumn(columnProps) + }, + + /** + * @description 新增column + * @param tableColumn 需要新增数据 + * @param index 在哪里新增 + */ + addColumn: async (tableColumn: TableColumn, index?: number) => { + const table = await getTable() + table?.addColumn(tableColumn, index) + }, + + /** + * @description 删除column + * @param field 删除哪个数据 + */ + delColumn: async (field: string) => { + const table = await getTable() + table?.delColumn(field) + }, + + /** + * @description 获取ElTable组件的实例 + * @returns ElTable instance + */ + getElTableExpose: async () => { + await getTable() + return unref(elTableRef) + }, + + refresh: () => { + methods.getList() + }, + + // sortableChange: (e: any) => { + // console.log('sortableChange', e) + // const { oldIndex, newIndex } = e + // dataList.value.splice(newIndex, 0, dataList.value.splice(oldIndex, 1)[0]) + // // to do something + // } + // 删除数据 + delList: async (idsLength: number) => { + const { fetchDelApi } = config + if (!fetchDelApi) { + console.warn('fetchDelApi is undefined') + return + } + ElMessageBox.confirm(t('common.delMessage'), t('common.delWarning'), { + confirmButtonText: t('common.delOk'), + cancelButtonText: t('common.delCancel'), + type: 'warning' + }).then(async () => { + const res = await fetchDelApi() + if (res) { + ElMessage.success(t('common.delSuccess')) + + // 计算出临界点 + const current = + unref(total) % unref(pageSize) === idsLength || unref(pageSize) === 1 + ? unref(currentPage) > 1 + ? unref(currentPage) - 1 + : unref(currentPage) + : unref(currentPage) + + currentPage.value = current + methods.getList() + } + }) + } + } + + return { + tableRegister: register, + tableMethods: methods, + tableState: { + currentPage, + pageSize, + total, + dataList, + loading + } + } +} diff --git a/src/hooks/web/useTagsView.ts b/src/hooks/web/useTagsView.ts new file mode 100644 index 0000000..31eadb0 --- /dev/null +++ b/src/hooks/web/useTagsView.ts @@ -0,0 +1,63 @@ +import { useTagsViewStoreWithOut } from '@/store/modules/tagsView' +import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router' +import { computed, nextTick, unref } from 'vue' + +export const useTagsView = () => { + const tagsViewStore = useTagsViewStoreWithOut() + + const { replace, currentRoute } = useRouter() + + const selectedTag = computed(() => tagsViewStore.getSelectedTag) + + const closeAll = (callback?: Fn) => { + tagsViewStore.delAllViews() + callback?.() + } + + const closeLeft = (callback?: Fn) => { + tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeRight = (callback?: Fn) => { + tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeOther = (callback?: Fn) => { + tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded) + callback?.() + } + + const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { + if (view?.meta?.affix) return + tagsViewStore.delView(view || unref(currentRoute)) + + callback?.() + } + + const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => { + tagsViewStore.delCachedView() + const { path, query } = view || unref(currentRoute) + await nextTick() + replace({ + path: '/redirect' + path, + query: query + }) + callback?.() + } + + const setTitle = (title: string, path?: string) => { + tagsViewStore.setTitle(title, path) + } + + return { + closeAll, + closeLeft, + closeRight, + closeOther, + closeCurrent, + refreshPage, + setTitle + } +} diff --git a/src/hooks/web/useTimeAgo.ts b/src/hooks/web/useTimeAgo.ts new file mode 100644 index 0000000..5675e5e --- /dev/null +++ b/src/hooks/web/useTimeAgo.ts @@ -0,0 +1,50 @@ +import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core' +import { computed, unref } from 'vue' +import { useLocaleStoreWithOut } from '@/store/modules/locale' + +const TIME_AGO_MESSAGE_MAP: { + 'zh-CN': UseTimeAgoMessages + en: UseTimeAgoMessages +} = { + 'zh-CN': { + justNow: '刚刚', + invalid: '无效时间', + past: (n) => (n.match(/\d/) ? `${n}前` : n), + future: (n) => (n.match(/\d/) ? `${n}后` : n), + month: (n, past) => (n === 1 ? (past ? '上个月' : '下个月') : `${n} 个月`), + year: (n, past) => (n === 1 ? (past ? '去年' : '明年') : `${n} 年`), + day: (n, past) => (n === 1 ? (past ? '昨天' : '明天') : `${n} 天`), + week: (n, past) => (n === 1 ? (past ? '上周' : '下周') : `${n} 周`), + hour: (n) => `${n} 小时`, + minute: (n) => `${n} 分钟`, + second: (n) => `${n} 秒` + }, + en: { + justNow: '刚刚', + invalid: 'Invalid Date', + past: (n) => (n.match(/\d/) ? `${n} ago` : n), + future: (n) => (n.match(/\d/) ? `in ${n}` : n), + month: (n, past) => + n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`, + year: (n, past) => + n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`, + day: (n, past) => (n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`), + week: (n, past) => + n === 1 ? (past ? 'last week' : 'next week') : `${n} week${n > 1 ? 's' : ''}`, + hour: (n) => `${n} hour${n > 1 ? 's' : ''}`, + minute: (n) => `${n} minute${n > 1 ? 's' : ''}`, + second: (n) => `${n} second${n > 1 ? 's' : ''}` + } +} + +export const useTimeAgo = (time: Date | number | string) => { + const localeStore = useLocaleStoreWithOut() + + const currentLocale = computed(() => localeStore.getCurrentLocale) + + const timeAgo = useTimeAgoCore(time, { + messages: TIME_AGO_MESSAGE_MAP[unref(currentLocale).lang] + }) + + return timeAgo +} diff --git a/src/hooks/web/useTitle.ts b/src/hooks/web/useTitle.ts new file mode 100644 index 0000000..546267d --- /dev/null +++ b/src/hooks/web/useTitle.ts @@ -0,0 +1,25 @@ +import { watch, ref } from 'vue' +import { isString } from '@/utils/is' +import { useAppStoreWithOut } from '@/store/modules/app' +import { useI18n } from '@/hooks/web/useI18n' + +export const useTitle = (newTitle?: string) => { + const { t } = useI18n() + const appStore = useAppStoreWithOut() + + const title = ref( + newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle + ) + + watch( + title, + (n, o) => { + if (isString(n) && n !== o && document) { + document.title = n + } + }, + { immediate: true } + ) + + return title +} diff --git a/src/hooks/web/useValidator.ts b/src/hooks/web/useValidator.ts new file mode 100644 index 0000000..a0d36c3 --- /dev/null +++ b/src/hooks/web/useValidator.ts @@ -0,0 +1,64 @@ +import { useI18n } from '@/hooks/web/useI18n' + +const { t } = useI18n() + +type Callback = (error?: string | Error | undefined) => void + +interface LengthRange { + min: number + max: number + message: string +} + +export const useValidator = () => { + const required = (message?: string) => { + return { + required: true, + message: message || t('common.required') + } + } + + const lengthRange = (val: any, callback: Callback, options: LengthRange) => { + const { min, max, message } = options + if (val.length < min || val.length > max) { + callback(new Error(message)) + } else { + callback() + } + } + + const notSpace = (val: any, callback: Callback, message: string) => { + // 用户名不能有空格 + if (val.indexOf(' ') !== -1) { + callback(new Error(message)) + } else { + callback() + } + } + + const notSpecialCharacters = (val: any, callback: Callback, message: string) => { + // 密码不能是特殊字符 + if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { + callback(new Error(message)) + } else { + callback() + } + } + + // 两个字符串是否想等 + const isEqual = (val1: string, val2: string, callback: Callback, message: string) => { + if (val1 === val2) { + callback() + } else { + callback(new Error(message)) + } + } + + return { + required, + lengthRange, + notSpace, + notSpecialCharacters, + isEqual + } +} diff --git a/src/hooks/web/useWatermark.ts b/src/hooks/web/useWatermark.ts new file mode 100644 index 0000000..4a31359 --- /dev/null +++ b/src/hooks/web/useWatermark.ts @@ -0,0 +1,55 @@ +const domSymbol = Symbol('watermark-dom') + +export function useWatermark(appendEl: HTMLElement | null = document.body) { + let func: Fn = () => {} + const id = domSymbol.toString() + const clear = () => { + const domId = document.getElementById(id) + if (domId) { + const el = appendEl + el && el.removeChild(domId) + } + window.removeEventListener('resize', func) + } + const createWatermark = (str: string) => { + clear() + + const can = document.createElement('canvas') + can.width = 300 + can.height = 240 + + const cans = can.getContext('2d') + if (cans) { + cans.rotate((-20 * Math.PI) / 120) + cans.font = '15px Vedana' + cans.fillStyle = 'rgba(0, 0, 0, 0.15)' + cans.textAlign = 'left' + cans.textBaseline = 'middle' + cans.fillText(str, can.width / 20, can.height) + } + + const div = document.createElement('div') + div.id = id + div.style.pointerEvents = 'none' + div.style.top = '0px' + div.style.left = '0px' + div.style.position = 'absolute' + div.style.zIndex = '100000000' + div.style.width = document.documentElement.clientWidth + 'px' + div.style.height = document.documentElement.clientHeight + 'px' + div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat' + const el = appendEl + el && el.appendChild(div) + return id + } + + function setWatermark(str: string) { + createWatermark(str) + func = () => { + createWatermark(str) + } + window.addEventListener('resize', func) + } + + return { setWatermark, clear } +} diff --git a/src/layout/Layout.vue b/src/layout/Layout.vue new file mode 100644 index 0000000..485a778 --- /dev/null +++ b/src/layout/Layout.vue @@ -0,0 +1,84 @@ + + + diff --git a/src/layout/components/AppView.vue b/src/layout/components/AppView.vue new file mode 100644 index 0000000..cefc8f6 --- /dev/null +++ b/src/layout/components/AppView.vue @@ -0,0 +1,26 @@ + + + + + + diff --git a/src/layout/components/ToolHeader.vue b/src/layout/components/ToolHeader.vue new file mode 100644 index 0000000..18bf00a --- /dev/null +++ b/src/layout/components/ToolHeader.vue @@ -0,0 +1,82 @@ + + + diff --git a/src/layout/components/useRenderLayout.tsx b/src/layout/components/useRenderLayout.tsx new file mode 100644 index 0000000..8e74a70 --- /dev/null +++ b/src/layout/components/useRenderLayout.tsx @@ -0,0 +1,306 @@ +import { computed } from 'vue' +import { useAppStore } from '@/store/modules/app' +import { Menu } from '@/components/Menu' +import { TabMenu } from '@/components/TabMenu' +import { TagsView } from '@/components/TagsView' +import { Logo } from '@/components/Logo' +import AppView from './AppView.vue' +import ToolHeader from './ToolHeader.vue' +import { ElScrollbar } from 'element-plus' +import { useDesign } from '@/hooks/web/useDesign' + +const { getPrefixCls } = useDesign() + +const prefixCls = getPrefixCls('layout') + +const appStore = useAppStore() + +const pageLoading = computed(() => appStore.getPageLoading) + +// 标签页 +const tagsView = computed(() => appStore.getTagsView) + +// 菜单折叠 +const collapse = computed(() => appStore.getCollapse) + +// logo +const logo = computed(() => appStore.logo) + +// 固定头部 +const fixedHeader = computed(() => appStore.getFixedHeader) + +// 是否是移动端 +const mobile = computed(() => appStore.getMobile) + +// 固定菜单 +const fixedMenu = computed(() => appStore.getFixedMenu) + +export const useRenderLayout = () => { + const renderClassic = () => { + return ( + <> ++ + ++ + ++ + {logo.value ? ( +++ ) : undefined} + + ++ > + ) + } + + const renderTopLeft = () => { + return ( + <> ++ +++ ++ + {tagsView.value ? ( + + ) : undefined} + + + {logo.value ?+: undefined} + + + + ++ > + ) + } + + const renderTop = () => { + return ( + <> ++++ {tagsView.value ? ( + ++ ) : undefined} + + + + {logo.value ?+: undefined} + + + ++ > + ) + } + + const renderCutMenu = () => { + return ( + <> ++ {tagsView.value ? ( + ++ ) : undefined} + + + + {logo.value ?+: undefined} + + + ++ > + ) + } + + return { + renderClassic, + renderTopLeft, + renderTop, + renderCutMenu + } +} diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue deleted file mode 100644 index d9e0df4..0000000 --- a/src/layouts/AdminLayout.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - -+ +++ {tagsView.value ? ( + ++ ) : undefined} + + + - -- diff --git a/src/locales/en.ts b/src/locales/en.ts new file mode 100644 index 0000000..7fa5565 --- /dev/null +++ b/src/locales/en.ts @@ -0,0 +1,536 @@ +export default { + common: { + inputText: 'Please input', + selectText: 'Please select', + startTimeText: 'Start time', + endTimeText: 'End time', + login: 'Login', + required: 'This is required', + loginOut: 'Login out', + document: 'Document', + reminder: 'Reminder', + loginOutMessage: 'Exit the system?', + back: 'Back', + ok: 'OK', + cancel: 'Cancel', + reload: 'Reload current', + closeTab: 'Close current', + closeTheLeftTab: 'Close left', + closeTheRightTab: 'Close right', + closeOther: 'Close other', + closeAll: 'Close all', + prevLabel: 'Prev', + nextLabel: 'Next', + skipLabel: 'Jump', + doneLabel: 'End', + menu: 'Menu', + menuDes: 'Menu bar rendered in routed structure', + collapse: 'Collapse', + collapseDes: 'Expand and zoom the menu bar', + tagsView: 'Tags view', + tagsViewDes: 'Used to record routing history', + tool: 'Tool', + toolDes: 'Used to set up custom systems', + query: 'Query', + reset: 'Reset', + shrink: 'Put away', + expand: 'Expand', + delMessage: 'Delete the selected data?', + delWarning: 'Warning', + delOk: 'OK', + delCancel: 'Cancel', + delNoData: 'Please select the data to delete', + delSuccess: 'Deleted successfully', + refresh: 'Refresh', + fullscreen: 'Fullscreen', + size: 'Size', + columnSetting: 'Column setting' + }, + lock: { + lockScreen: 'Lock screen', + lock: 'Lock', + lockPassword: 'Lock screen password', + unlock: 'Click to unlock', + backToLogin: 'Back to login', + entrySystem: 'Entry the system', + placeholder: 'Please enter the lock screen password', + message: 'Lock screen password error' + }, + error: { + noPermission: `Sorry, you don't have permission to access this page.`, + pageError: 'Sorry, the page you visited does not exist.', + networkError: 'Sorry, the server reported an error.', + returnToHome: 'Return to home' + }, + setting: { + projectSetting: 'Project setting', + theme: 'Theme', + layout: 'Layout', + systemTheme: 'System theme', + menuTheme: 'Menu theme', + interfaceDisplay: 'Interface display', + breadcrumb: 'Breadcrumb', + breadcrumbIcon: 'Breadcrumb icon', + collapseMenu: 'Collapse menu', + hamburgerIcon: 'Hamburger icon', + screenfullIcon: 'Screenfull icon', + sizeIcon: 'Size icon', + localeIcon: 'Locale icon', + tagsView: 'Tags view', + logo: 'Logo', + greyMode: 'Grey mode', + fixedHeader: 'Fixed header', + headerTheme: 'Header theme', + cutMenu: 'Cut Menu', + copy: 'Copy', + clearAndReset: 'Clear cache and reset', + copySuccess: 'Copy success', + copyFailed: 'Copy failed', + footer: 'Footer', + uniqueOpened: 'Unique opened', + tagsViewIcon: 'Tags view icon', + dynamicRouter: 'Dynamic router', + reExperienced: 'Please exit the login experience again', + fixedMenu: 'Fixed menu', + serverDynamicRouter: 'Server dynamic router' + }, + size: { + default: 'Default', + large: 'Large', + small: 'Small' + }, + login: { + welcome: 'Short-drama workspace', + message: 'Manage dramas, episodes, publishing, and access.', + hint: 'Sign in with an admin account', + noAccount: 'No account yet?', + pointDrama: 'Dramas and episodes', + pointPublish: 'Publishing', + pointAccess: 'Access', + username: 'Username', + password: 'Password', + register: 'Register', + checkPassword: 'Confirm password', + login: 'Sign in', + otherLogin: 'Sign in with', + remember: 'Remember me', + hasUser: 'Existing account? Go to login', + forgetPassword: 'Forget password', + usernamePlaceholder: 'Please input username', + passwordPlaceholder: 'Please input password', + code: 'Verification code', + codePlaceholder: 'Please input verification code' + }, + router: { + login: 'Login', + level: 'Multi level menu', + menu: 'Menu', + menu1: 'Menu1', + menu11: 'Menu1-1', + menu111: 'Menu1-1-1', + menu12: 'Menu1-2', + menu2: 'Menu2', + dashboard: 'Dashboard', + analysis: 'Analysis', + workplace: 'Workplace', + guide: 'Guide', + component: 'Component', + icon: 'Icon', + echart: 'Echart', + countTo: 'Count to', + watermark: 'Watermark', + qrcode: 'Qrcode', + highlight: 'Highlight', + infotip: 'Infotip', + form: 'Form', + defaultForm: 'All examples', + search: 'Search', + table: 'Table', + defaultTable: 'Basic example', + editor: 'Editor', + richText: 'Rich text', + dialog: 'Dialog', + imageViewer: 'Image viewer', + descriptions: 'Descriptions', + example: 'Example', + exampleDialog: 'Example dialog', + examplePage: 'Example page', + exampleAdd: 'Example page - add', + exampleEdit: 'Example page - edit', + exampleDetail: 'Example page - detail', + errorPage: 'Error page', + authorization: 'Authorization', + user: 'User management', + role: 'Role management', + document: 'Document', + inputPassword: 'InputPassword', + sticky: 'Sticky', + treeTable: 'Tree table', + PicturePreview: 'Table Image Preview', + department: 'Department management', + menuManagement: 'Menu management' + }, + permission: { + hasPermission: 'Please set the operation permission value' + }, + analysis: { + newUser: 'New user', + unreadInformation: 'Unread information', + transactionAmount: 'Transaction amount', + totalShopping: 'Total Shopping', + monthlySales: 'Monthly sales', + userAccessSource: 'User access source', + january: 'January', + february: 'February', + march: 'March', + april: 'April', + may: 'May', + june: 'June', + july: 'July', + august: 'August', + september: 'September', + october: 'October', + november: 'November', + december: 'December', + estimate: 'Estimate', + actual: 'Actual', + directAccess: 'Airect access', + mailMarketing: 'Mail marketing', + allianceAdvertising: 'Alliance advertising', + videoAdvertising: 'Video advertising', + searchEngines: 'Search engines', + weeklyUserActivity: 'Weekly user activity', + activeQuantity: 'Active quantity', + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday', + sunday: 'Sunday' + }, + workplace: { + goodMorning: 'Good morning', + happyDay: 'Wish you happy every day!', + toady: `It's sunny today`, + project: 'Project', + access: 'Project access', + toDo: 'To do', + introduction: 'A serious introduction', + more: 'More', + shortcutOperation: 'Shortcut operation', + operation: 'Operation', + index: 'Index', + personal: 'Personal', + team: 'Team', + quote: 'Quote', + contribution: 'Contribution', + hot: 'Hot', + yield: 'Yield', + dynamic: 'Dynamic', + push: 'push', + pushCode: 'Archer push code to Github', + follow: 'Follow' + }, + formDemo: { + input: 'Input', + inputNumber: 'InputNumber', + default: 'Default', + icon: 'Icon', + mixed: 'Mixed', + password: 'Password', + textarea: 'Textarea', + remoteSearch: 'Remote search', + slot: 'Slot', + position: 'Position', + autocomplete: 'Autocomplete', + select: 'Select', + optionSlot: 'Option Slot', + selectGroup: 'Select Group', + selectV2: 'SelectV2', + cascader: 'Cascader', + switch: 'Switch', + rate: 'Rate', + colorPicker: 'Color Picker', + transfer: 'Transfer', + render: 'Render', + radio: 'Radio', + radioGroup: 'Radio Group', + button: 'Button', + checkbox: 'Checkbox', + checkboxButton: 'Checkbox Button', + checkboxGroup: 'Checkbox Group', + slider: 'Slider', + datePicker: 'Date Picker', + shortcuts: 'Shortcuts', + today: 'Today', + yesterday: 'Yesterday', + aWeekAgo: 'A week ago', + week: 'Week', + year: 'Year', + month: 'Month', + dates: 'Dates', + daterange: 'Date Range', + monthrange: 'Month Range', + dateTimePicker: 'DateTimePicker', + dateTimerange: 'Datetime Range', + timePicker: 'Time Picker', + timeSelect: 'Time Select', + inputPassword: 'input Password', + passwordStrength: 'Password Strength', + defaultForm: 'All examples', + formDes: + 'The secondary encapsulation of form components based on ElementPlus realizes data-driven and supports all Form parameters', + example: 'example', + operate: 'operate', + change: 'Change', + restore: 'Restore', + disabled: 'Disabled', + disablement: 'Disablement', + delete: 'Delete', + add: 'Add', + setValue: 'Set value', + resetValue: 'Reset value', + set: 'Set', + subitem: 'Subitem', + formValidation: 'Form validation', + verifyReset: 'Verify reset', + // 富文本编辑器 + richText: 'Rich text', + form: 'Form', + // 远程加载 + remoteLoading: 'Remote loading', + // 聚焦 + focus: 'Focus', + treeSelect: 'Tree Select', + showCheckbox: 'Show Checkbox', + selectAnyLevel: 'Select Any Level', + multiple: 'Multiple', + filterable: 'Filterable', + // 自定义节点内容 + customContent: 'Custom content', + // 懒加载 + lazyLoad: 'Lazy load', + upload: 'Upload', + // 用户头像 + userAvatar: 'User avatar' + }, + guideDemo: { + guide: 'Guide', + start: 'Start', + message: + 'The guide page is very useful for some people who enter the project for the first time. You can briefly introduce the functions of the project. The boot page is based on intro js' + }, + iconDemo: { + icon: 'Icon', + localIcon: 'Local Icon', + iconify: 'Iconify component', + recommendedUse: 'Recommended use', + recommendeDes: + 'Iconify component basically contains all icons. You can query any icon you want. And packaging will only package the icons used.', + accessAddress: 'Access address' + }, + echartDemo: { + echart: 'Echart', + echartDes: + 'Based on the secondary packaging components of eckarts, the width is adaptive. The corresponding chart can be displayed by passing in the options and height attributes.' + }, + countToDemo: { + countTo: 'CountTo', + countToDes: + 'The transformation is based on vue-count-to and supports all vue-count-to parameters.', + suffix: 'Suffix', + prefix: 'Prefix', + separator: 'Separator', + duration: 'Duration', + endVal: 'End val', + startVal: 'Start val', + start: 'Start', + pause: 'Pause', + resume: 'Resume' + }, + watermarkDemo: { + watermark: 'Watermark', + createdWatermark: 'Created watermark', + clearWatermark: 'Clear watermark', + resetWatermark: 'Reset watermark' + }, + qrcodeDemo: { + qrcode: 'Qrcode', + qrcodeDes: 'Secondary packaging based on qrcode', + basicUsage: 'Basic usage', + imgTag: 'Img tag', + style: 'Style config', + click: 'Click event', + asynchronousContent: 'Asynchronous content', + invalid: 'Invalid', + logoConfig: 'Logo config', + logoStyle: 'Logo style', + size: 'size config' + }, + highlightDemo: { + highlight: 'Highlight', + message: 'The best time to plant a tree is ten years ago, followed by now.', + keys1: 'ten years ago', + keys2: 'now' + }, + infotipDemo: { + infotip: 'Infotip', + infotipDes: 'Secondary packaging of components based on Highlight', + title: 'matters needing attention' + }, + levelDemo: { + menu: 'Multi level menu cache' + }, + searchDemo: { + search: 'Search', + searchDes: + 'Based on the secondary encapsulation of form components, the functions of query and reset are realized', + operate: 'operate', + change: 'Change', + grid: 'grid', + button: 'Button', + restore: 'Restore', + inline: 'inline', + bottom: 'Bottom', + position: 'position', + left: 'left', + center: 'center', + right: 'right', + dynamicOptions: 'Dynamic options', + // 删除单选框 + deleteRadio: 'Delete radio', + // 还原单选框 + restoreRadio: 'Restore radio', + loading: 'Loading', + reset: 'Reset' + }, + stickyDemo: { + sticky: 'Sticky' + }, + tableDemo: { + table: 'Table', + tableDes: 'Secondary packaging of Table components based on ElementPlus', + index: 'Index', + title: 'Title', + author: 'Author', + displayTime: 'Display time', + importance: 'Importance', + pageviews: 'Pageviews', + action: 'Action', + important: 'Important', + good: 'Good', + commonly: 'Commonly', + operate: 'operate', + example: 'example', + show: 'Show', + hidden: 'Hidden', + pagination: 'pagination', + reserveIndex: 'Reserve index', + restoreIndex: 'Restore index', + showSelections: 'Show selections', + hiddenSelections: 'Restore selections', + showExpandedRows: 'Show expanded rows', + hiddenExpandedRows: 'Hidden expanded rows', + changeTitle: 'Change title', + header: 'Header', + selectAllNone: 'Select all / none', + delOrAddAction: 'Delete or add action', + showOrHiddenStripe: 'Show or hidden stripe', + showOrHiddenBorder: 'Show or hidden border', + fixedHeaderOrAuto: 'Fixed header or auto', + getSelections: 'Get selections', + preview: 'Preview', + showOrHiddenSortable: 'Show or hidden sortable' + }, + richText: { + richText: 'Rich text', + richTextDes: 'Secondary packaging based on wangeditor' + }, + dialogDemo: { + dialog: 'Dialog', + dialogDes: 'Secondary packaging of Dialog components based on ElementPlus', + open: 'Open', + close: 'Close', + combineWithForm: 'Combine with form', + submit: 'Submit' + }, + imageViewerDemo: { + open: 'Open', + imageViewer: 'Image viewer', + imageViewerDes: 'Secondary packaging of ImageViewer components based on ElementPlus' + }, + descriptionsDemo: { + descriptions: 'Descriptions', + descriptionsDes: 'Secondary packaging of Descriptions components based on ElementPlus', + username: 'Username', + nickName: 'NickName', + phone: 'Phone', + email: 'Email', + addr: 'Address', + form: 'Combined with Form component' + }, + exampleDemo: { + title: 'Title', + add: 'Add', + del: 'Delete', + edit: 'Edit', + author: 'Author', + displayTime: 'Display time', + importance: 'Importance', + pageviews: 'Pageviews', + important: 'Important', + content: 'Content', + save: 'Save', + detail: 'Detail' + }, + userDemo: { + title: 'User management', + message: + 'Because it is simulated data, only two accounts with different permissions are provided, which can be modified and combined by developers according to the actual situation.', + index: 'Index', + action: 'Action', + username: 'Username', + password: 'Password', + role: 'Role', + remark: 'Remark', + remarkMessage1: 'Back end control routing permission', + remarkMessage2: 'Front end control routing permission', + // 部门列表 + departmentList: 'Department list', + // 搜索部门 + searchDepartment: 'Search department', + account: 'Account', + email: 'Email', + createTime: 'Create time', + // 所属部门 + department: 'Department', + departmentName: 'Department name', + status: 'Status', + enable: 'Enable', + disable: 'Disable', + superiorDepartment: 'Superior department' + }, + menu: { + menuName: 'Menu name', + icon: 'Icon', + // 权限 + permission: 'Permission', + component: 'Component', + path: 'Path', + status: 'Status', + hidden: 'Hidden', + alwaysShow: 'Always show', + noCache: 'No cache', + breadcrumb: 'Breadcrumb', + affix: 'Affix', + noTagsView: 'No tags view', + activeMenu: 'Active menu', + canTo: 'Can to', + name: 'Name' + }, + inputPasswordDemo: { + title: 'InputPassword', + inputPasswordDes: 'Secondary packaging of Input components based on ElementPlus' + } +} diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts new file mode 100644 index 0000000..baa3c99 --- /dev/null +++ b/src/locales/zh-CN.ts @@ -0,0 +1,530 @@ +export default { + common: { + inputText: '请输入', + selectText: '请选择', + startTimeText: '开始时间', + endTimeText: '结束时间', + login: '登录', + required: '该项为必填项', + loginOut: '退出系统', + document: '项目文档', + reminder: '温馨提示', + loginOutMessage: '是否退出本系统?', + back: '返回', + ok: '确定', + cancel: '取消', + reload: '重新加载', + closeTab: '关闭标签页', + closeTheLeftTab: '关闭左侧标签页', + closeTheRightTab: '关闭右侧标签页', + closeOther: '关闭其它标签页', + closeAll: '关闭全部标签页', + prevLabel: '上一步', + nextLabel: '下一步', + skipLabel: '跳过', + doneLabel: '结束', + menu: '菜单', + menuDes: '以路由的结构渲染的菜单栏', + collapse: '展开缩收', + collapseDes: '展开和缩放菜单栏', + tagsView: '标签页', + tagsViewDes: '用于记录路由历史记录', + tool: '工具', + toolDes: '用于设置定制系统', + query: '查询', + reset: '重置', + shrink: '收起', + expand: '展开', + delMessage: '是否删除所选中数据?', + delWarning: '提示', + delOk: '确定', + delCancel: '取消', + delNoData: '请选择需要删除的数据', + delSuccess: '删除成功', + refresh: '刷新', + fullscreen: '全屏', + size: '尺寸', + columnSetting: '列设置' + }, + lock: { + lockScreen: '锁定屏幕', + lock: '锁定', + lockPassword: '锁屏密码', + unlock: '点击解锁', + backToLogin: '返回登录', + entrySystem: '进入系统', + placeholder: '请输入锁屏密码', + message: '锁屏密码错误' + }, + error: { + noPermission: `抱歉,您无权访问此页面。`, + pageError: '抱歉,您访问的页面不存在。', + networkError: '抱歉,服务器报告错误。', + returnToHome: '返回首页' + }, + setting: { + projectSetting: '项目配置', + theme: '主题', + layout: '布局', + systemTheme: '系统主题', + menuTheme: '菜单主题', + interfaceDisplay: '界面显示', + breadcrumb: '面包屑', + breadcrumbIcon: '面包屑图标', + collapseMenu: '折叠菜单', + hamburgerIcon: '折叠图标', + screenfullIcon: '全屏图标', + sizeIcon: '尺寸图标', + localeIcon: '多语言图标', + tagsView: '标签页', + logo: '标志', + greyMode: '灰色模式', + fixedHeader: '固定头部', + headerTheme: '头部主题', + cutMenu: '切割菜单', + copy: '拷贝', + clearAndReset: '清除缓存并且重置', + copySuccess: '拷贝成功', + copyFailed: '拷贝失败', + footer: '页脚', + uniqueOpened: '菜单手风琴', + tagsViewIcon: '标签页图标', + dynamicRouter: '动态路由', + reExperienced: '请重新退出登录体验', + fixedMenu: '固定菜单', + serverDynamicRouter: '服务端动态路由' + }, + size: { + default: '默认', + large: '大', + small: '小' + }, + login: { + welcome: '短剧内容管理', + message: '管理短剧、剧集、发布状态和后台权限。', + hint: '使用管理员账号进入工作台', + noAccount: '还没有账号?', + pointDrama: '短剧与剧集', + pointPublish: '发布状态', + pointAccess: '角色权限', + username: '用户名', + password: '密码', + register: '注册', + checkPassword: '确认密码', + login: '登录', + otherLogin: '其它登录方式', + remember: '记住我', + hasUser: '已有账号?去登录', + forgetPassword: '忘记密码', + usernamePlaceholder: '请输入用户名', + passwordPlaceholder: '请输入密码', + code: '验证码', + codePlaceholder: '请输入验证码' + }, + router: { + login: '登录', + level: '多级菜单', + menu: '菜单', + menu1: '菜单1', + menu11: '菜单1-1', + menu111: '菜单1-1-1', + menu12: '菜单1-2', + menu2: '菜单2', + dashboard: '首页', + analysis: '分析页', + workplace: '工作台', + guide: '引导', + component: '组件', + icon: '图标', + echart: '图表', + countTo: '数字动画', + watermark: '水印', + qrcode: '二维码', + highlight: '高亮', + infotip: '信息提示', + form: '表单', + defaultForm: '全部示例', + search: '查询', + table: '表格', + defaultTable: '基础示例', + editor: '编辑器', + richText: '富文本', + dialog: '弹窗', + imageViewer: '图片预览', + descriptions: '描述', + example: '综合示例', + exampleDialog: '综合示例 - 弹窗', + examplePage: '综合示例 - 页面', + exampleAdd: '综合示例 - 新增', + exampleEdit: '综合示例 - 编辑', + exampleDetail: '综合示例 - 详情', + errorPage: '错误页面', + authorization: '权限管理', + user: '用户管理', + role: '角色管理', + document: '文档', + inputPassword: '密码输入框', + sticky: '黏性', + treeTable: '树形表格', + PicturePreview: '表格图片预览', + department: '部门管理', + menuManagement: '菜单管理' + }, + permission: { + hasPermission: '请设置操作权限值' + }, + analysis: { + newUser: '新增用户', + unreadInformation: '未读消息', + transactionAmount: '成交金额', + totalShopping: '购物总量', + monthlySales: '每月销售额', + userAccessSource: '用户访问来源', + january: '一月', + february: '二月', + march: '三月', + april: '四月', + may: '五月', + june: '六月', + july: '七月', + august: '八月', + september: '九月', + october: '十月', + november: '十一月', + december: '十二月', + estimate: '预计', + actual: '实际', + directAccess: '直接访问', + mailMarketing: '邮件营销', + allianceAdvertising: '联盟广告', + videoAdvertising: '视频广告', + searchEngines: '搜索引擎', + weeklyUserActivity: '每周用户活跃量', + activeQuantity: '活跃量', + monday: '周一', + tuesday: '周二', + wednesday: '周三', + thursday: '周四', + friday: '周五', + saturday: '周六', + sunday: '周日' + }, + workplace: { + goodMorning: '早安', + happyDay: '祝你开心每一天!', + toady: '今日晴', + project: '项目数', + access: '项目访问', + toDo: '待办', + introduction: '一个正经的简介', + more: '更多', + shortcutOperation: '快捷操作', + operation: '操作', + index: '指数', + personal: '个人', + team: '团队', + quote: '引用', + contribution: '贡献', + hot: '热度', + yield: '产量', + dynamic: '动态', + push: '推送', + pushCode: 'Archer 推送 代码到 Github', + follow: '关注' + }, + formDemo: { + input: '输入框', + inputNumber: '数字输入框', + default: '默认', + icon: '图标', + mixed: '复合型', + password: '密码框', + textarea: '多行文本', + remoteSearch: '远程搜索', + slot: '插槽', + position: '位置', + autocomplete: '自动补全', + select: '选择器', + optionSlot: '选项插槽', + selectGroup: '选项分组', + selectV2: '虚拟列表选择器', + cascader: '级联选择器', + switch: '开关', + rate: '评分', + colorPicker: '颜色选择器', + transfer: '穿梭框', + render: '渲染器', + radio: '单选框', + radioGroup: '单选框组', + button: '按钮', + checkbox: '多选框', + checkboxButton: '多选框按钮', + checkboxGroup: '多选框组', + slider: '滑块', + datePicker: '日期选择器', + shortcuts: '快捷选项', + today: '今天', + yesterday: '昨天', + aWeekAgo: '一周前', + week: '周', + year: '年', + month: '月', + dates: '日期', + daterange: '日期范围', + monthrange: '月份范围', + dateTimePicker: '日期时间选择器', + dateTimerange: '日期时间范围', + timePicker: '时间选择器', + timeSelect: '时间选择', + inputPassword: '密码输入框', + passwordStrength: '密码强度', + defaultForm: '全部示例', + formDes: '基于 ElementPlus 的 Form 组件二次封装,实现数据驱动,支持所有 Form 参数', + example: '示例', + operate: '操作', + change: '更改', + restore: '还原', + disabled: '禁用', + disablement: '解除禁用', + delete: '删除', + add: '添加', + setValue: '设置值', + resetValue: '重置值', + set: '设置', + subitem: '子项', + formValidation: '表单验证', + verifyReset: '验证重置', + // 富文本编辑器 + richText: '富文本编辑器', + form: '表单', + // 远程加载 + remoteLoading: '远程加载', + // 聚焦 + focus: '聚焦', + treeSelect: '树形选择器', + showCheckbox: '显示复选框', + selectAnyLevel: '选择任意级别', + multiple: '多选', + filterable: '可筛选', + customContent: '自定义内容', + lazyLoad: '懒加载', + upload: '上传', + userAvatar: '用户头像' + }, + guideDemo: { + guide: '引导页', + start: '开始', + message: + '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。引导页基于 intro.js' + }, + iconDemo: { + icon: '图标', + localIcon: '本地图标', + iconify: 'Iconify组件', + recommendedUse: '推荐使用', + recommendeDes: + 'Iconify组件基本包含所有的图标,你可以查询到你想要的任何图标。并且打包只会打包所用到的图标。', + accessAddress: '访问地址' + }, + echartDemo: { + echart: '图表', + echartDes: + '基于 echarts 二次封装组件,自适应宽度,只需传入 options 与 height 属性即可展示对应的图表。' + }, + countToDemo: { + countTo: '数字动画', + countToDes: '基于 vue-count-to 进行改造,支持所有 vue-count-to 参数。', + suffix: '后缀', + prefix: '前缀', + separator: '分割符号', + duration: '持续时间', + endVal: '结束值', + startVal: '开始值', + start: '开始', + pause: '暂停', + resume: '继续' + }, + watermarkDemo: { + watermark: '水印', + createdWatermark: '创建水印', + clearWatermark: '清除水印', + resetWatermark: '重置水印' + }, + qrcodeDemo: { + qrcode: '二维码', + qrcodeDes: '基于 qrcode 二次封装', + basicUsage: '基础用法', + imgTag: 'img标签', + style: '样式配置', + click: '点击事件', + asynchronousContent: '异步内容', + invalid: '失效', + logoConfig: 'logo配置', + logoStyle: 'logo样式', + size: '大小配置' + }, + highlightDemo: { + highlight: '高亮', + message: '种一棵树最好的时间是十年前,其次就是现在。', + keys1: '十年前', + keys2: '现在' + }, + infotipDemo: { + infotip: '信息提示', + infotipDes: '基于 Highlight 组件二次封装', + title: '注意事项' + }, + levelDemo: { + menu: '多级菜单缓存' + }, + searchDemo: { + search: '查询', + searchDes: '基于 Form 组件二次封装,实现查询、重置功能', + operate: '操作', + change: '更改', + grid: '栅格', + button: '按钮', + restore: '还原', + inline: '内联', + bottom: '底部', + position: '位置', + left: '左', + center: '中', + right: '右', + dynamicOptions: '动态选项', + // 删除单选框 + deleteRadio: '删除单选框', + // 还原单选框 + restoreRadio: '还原单选框', + loading: '加载中', + reset: '重置' + }, + stickyDemo: { + sticky: '黏性' + }, + tableDemo: { + table: '表格', + tableDes: '基于 ElementPlus 的 Table 组件二次封装', + index: '序号', + title: '标题', + author: '作者', + displayTime: '创建时间', + importance: '重要性', + pageviews: '阅读数', + action: '操作', + important: '重要', + good: '良好', + commonly: '一般', + operate: '操作', + example: '示例', + show: '显示', + hidden: '隐藏', + pagination: '分页', + reserveIndex: '叠加序号', + restoreIndex: '还原序号', + showSelections: '显示多选', + hiddenSelections: '隐藏多选', + showExpandedRows: '显示展开行', + hiddenExpandedRows: '隐藏展开行', + changeTitle: '修改标题', + header: '头部', + selectAllNone: '全选/全不选', + delOrAddAction: '删除/添加操作列', + showOrHiddenStripe: '显示/隐藏斑马纹', + showOrHiddenBorder: '显示/隐藏边框', + fixedHeaderOrAuto: '固定头部/自动', + getSelections: '获取多选数据', + preview: '封面', + showOrHiddenSortable: '显示/隐藏排序' + }, + richText: { + richText: '富文本', + richTextDes: '基于 wangeditor 二次封装' + }, + dialogDemo: { + dialog: '弹窗', + dialogDes: '基于 ElementPlus 的 Dialog 组件二次封装', + open: '打开', + close: '关闭', + combineWithForm: '与表单结合', + submit: '提交' + }, + imageViewerDemo: { + open: '打开', + imageViewer: '图片预览', + imageViewerDes: '基于 ElementPlus 的 ImageViewer 组件二次封装' + }, + descriptionsDemo: { + descriptions: '描述', + descriptionsDes: '基于 ElementPlus 的 Descriptions 组件二次封装', + username: '用户名', + nickName: '昵称', + phone: '联系电话', + email: '邮箱', + addr: '地址', + form: '与 Form 组件组合' + }, + exampleDemo: { + title: '标题', + add: '新增', + del: '删除', + edit: '编辑', + author: '作者', + displayTime: '创建时间', + importance: '重要性', + pageviews: '阅读数', + important: '重要', + content: '内容', + save: '保存', + detail: '详情' + }, + userDemo: { + title: '用户管理', + message: '由于是模拟数据,所以只提供了两种不同权限的帐号,开发者可根据实际情况自行改造结合。', + index: '序号', + action: '操作', + username: '用户名', + password: '密码', + role: '角色', + remark: '备注', + remarkMessage1: '后端控制路由权限', + remarkMessage2: '前端控制路由权限', + // 部门列表 + departmentList: '部门列表', + searchDepartment: '搜索部门', + account: '账号', + email: '邮箱', + createTime: '创建时间', + // 所属部门 + department: '所属部门', + departmentName: '部门名称', + status: '状态', + // 启用 + enable: '启用', + // 禁用 + disable: '禁用', + // 上级部门 + superiorDepartment: '上级部门' + }, + menu: { + menuName: '菜单名称', + icon: '图标', + permission: '权限标识', + component: '组件', + path: '路径', + status: '状态', + hidden: '是否隐藏', + alwaysShow: '是否一直显示', + noCache: '是否清除缓存', + breadcrumb: '是否显示面包屑', + affix: '是否固定在标签页', + noTagsView: '是否隐藏标签页', + activeMenu: '高亮菜单', + canTo: '是否可跳转', + name: '组件名称' + }, + inputPasswordDemo: { + title: '密码输入框', + inputPasswordDes: '基于 ElementPlus 的 Input 组件二次封装' + } +} diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 0817f26..0000000 --- a/src/main.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createApp } from 'vue' -import ElementPlus from 'element-plus' -import zhCn from 'element-plus/dist/locale/zh-cn.mjs' -import 'element-plus/dist/index.css' -import 'element-plus/theme-chalk/dark/css-vars.css' -import './style.css' -import './theme.css' -import './typography.css' -import './navigation.css' -import './dark-theme.css' -import './drama-management.css' -import './stores/theme.js' -import App from './App.vue' -import router from './router' - -createApp(App).use(ElementPlus, { locale: zhCn }).use(router).mount('#app') diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b15c625 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,54 @@ +import 'vue/jsx' + +// 引入unocss +import '@/plugins/unocss' + +// 导入全局的svg图标 +import '@/plugins/svgIcon' + +// 初始化多语言 +import { setupI18n } from '@/plugins/vueI18n' + +// 引入状态管理 +import { setupStore } from '@/store' + +// 全局组件 +import { setupGlobCom } from '@/components' + +// 引入element-plus +import { setupElementPlus } from '@/plugins/elementPlus' + +// 引入全局样式 +import '@/styles/index.less' +import '@/styles/business/index.css' + +// 引入动画 +import '@/plugins/animate.css' + +// 路由 +import { setupRouter } from './router' + +import { createApp } from 'vue' + +import App from './App.vue' + +import './permission' + +// 创建实例 +const setupAll = async () => { + const app = createApp(App) + + await setupI18n(app) + + setupStore(app) + + setupGlobCom(app) + + setupElementPlus(app) + + setupRouter(app) + + app.mount('#app') +} + +setupAll() diff --git a/src/navigation.css b/src/navigation.css deleted file mode 100644 index d822c71..0000000 --- a/src/navigation.css +++ /dev/null @@ -1,138 +0,0 @@ -.sidebar { - padding:22px 12px; - background:#fff} -.brand { - padding:0 10px 22px; - margin-bottom:8px; - border-bottom:1px solid #eceef0} -.side-menu { - border-right:0!important} -.side-menu>.el-menu-item,.side-menu>.el-sub-menu>.el-sub-menu__title { - height:46px; - line-height:46px; - font-size:14px; - border-radius:4px; - margin:3px 0} -.side-menu .el-sub-menu .el-menu-item { - min-width:0; - height:42px; - line-height:42px; - font-size:13px; - padding-left:48px!important} -.side-menu .el-sub-menu .el-menu-item { - padding-left:48px!important} -.main-area { - margin-left:250px} -.topbar { - height:62px; - padding:0 28px} -.top-actions { - gap:12px} -.current-user { - font-size:13px; - color:#535b66} -.role-switch,.logout-button { - font-size:13px!important; - color:#56616e!important} -.logout-button { - border-left:1px solid #e7e9ec; - border-radius:0!important; - padding-left:16px!important} -.logout-button .el-icon { - margin-right:5px} -.page-tabs { - height:43px; - background:#fff; - border-bottom:1px solid #e7e9ec; - padding:0 24px} -.page-tabs .el-tabs__header { - margin:0; - border-bottom:0; - height:43px} -.page-tabs .el-tabs__nav { - border:0!important; - height:43px} -.page-tabs .el-tabs__item { - height:43px; - line-height:43px; - padding:0 17px!important; - font-size:13px; - color:#68717c; - border:0!important; - background:transparent} -.page-tabs .el-tabs__item.is-active { - color:#365f96; - border-bottom:2px solid #456fae!important; - background:#f8f9fb} -.page-tabs .el-tabs__item .is-icon-close { - font-size:13px} -.page-tabs .el-tabs__content { - display:none} -.page-content { - padding-top:26px} -.brand-mark { - width:30px; - height:30px} -.sidebar>.side-menu { - margin-top:4px} -.el-sub-menu__title:hover,.side-menu .el-menu-item:hover { - background:#f4f6f8} -.side-menu .el-menu-item.is-active { - background:#edf2f8; - color:#355f97} -.page-heading { - margin-bottom:20px} -@media(max-width:1200px) { - .topbar { - padding:0 18px} -.page-tabs { - padding:0 14px} -.page-content { - padding:24px 24px 40px} -} -.side-menu>.el-menu-item,.side-menu>.el-sub-menu>.el-sub-menu__title { - box-sizing:border-box; - padding-left:20px!important; - display:flex; - align-items:center} -.side-menu>.el-menu-item>.el-icon,.side-menu>.el-sub-menu>.el-sub-menu__title>.el-icon { - width:18px; - min-width:18px; - margin-right:10px} -.side-menu .el-sub-menu .el-sub-menu__title .el-icon { - margin-right:10px} -.topbar { - position:sticky; - top:0; - z-index:20} -.page-tabs { - position:sticky; - top:62px; - z-index:19} -.fullscreen-button { - font-size:18px!important; - color:#56616e!important; - padding:7px!important} -.fullscreen-button:hover { - color:#355f97!important; - background:#f1f4f7!important} -.status-badge { - border:0!important; - font-weight:500} -.status-badge.is-online { - background:#eaf5ee!important; - color:#32724b!important} -.status-badge.is-reviewing { - background:#fff3df!important; - color:#946000!important} -.status-badge.is-draft { - background:#eef0f2!important; - color:#616a74!important} -.status-badge.is-pending { - background:#eaf2fb!important; - color:#35689d!important} -.status-badge.is-offline { - background:#fbeaea!important; - color:#a74444!important} -.side-menu>.el-menu-item>.el-icon,.side-menu>.el-sub-menu>.el-sub-menu__title>.el-icon { - font-size:16px} diff --git a/src/permission.ts b/src/permission.ts new file mode 100644 index 0000000..a14c3a8 --- /dev/null +++ b/src/permission.ts @@ -0,0 +1,64 @@ +import router from './router' +import { useAppStoreWithOut } from '@/store/modules/app' +import type { RouteRecordRaw } from 'vue-router' +import { useTitle } from '@/hooks/web/useTitle' +import { useNProgress } from '@/hooks/web/useNProgress' +import { usePermissionStoreWithOut } from '@/store/modules/permission' +import { usePageLoading } from '@/hooks/web/usePageLoading' +import { NO_REDIRECT_WHITE_LIST } from '@/constants' +import { useUserStoreWithOut } from '@/store/modules/user' + +const { start, done } = useNProgress() + +const { loadStart, loadDone } = usePageLoading() + +router.beforeEach(async (to, from, next) => { + start() + loadStart() + const permissionStore = usePermissionStoreWithOut() + const appStore = useAppStoreWithOut() + const userStore = useUserStoreWithOut() + if (userStore.getUserInfo) { + if (to.path === '/login') { + next({ path: '/' }) + } else { + if (permissionStore.getIsAddRouters) { + next() + return + } + + // 开发者可根据实际情况进行修改 + const roleRouters = userStore.getRoleRouters || [] + + // 是否使用动态路由 + if (appStore.getDynamicRouter) { + appStore.serverDynamicRouter + ? await permissionStore.generateRoutes('server', roleRouters as AppCustomRouteRecordRaw[]) + : await permissionStore.generateRoutes('frontEnd', roleRouters as string[]) + } else { + await permissionStore.generateRoutes('static') + } + + permissionStore.getAddRouters.forEach((route) => { + router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表 + }) + const redirectPath = from.query.redirect || to.path + const redirect = decodeURIComponent(redirectPath as string) + const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect } + permissionStore.setIsAddRouters(true) + next(nextData) + } + } else { + if (NO_REDIRECT_WHITE_LIST.indexOf(to.path) !== -1) { + next() + } else { + next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 + } + } +}) + +router.afterEach((to) => { + useTitle(to?.meta?.title as string) + done() // 结束Progress + loadDone() +}) diff --git a/src/permissions/permissions.js b/src/permissions/permissions.js index 1f2279b..c83feaf 100644 --- a/src/permissions/permissions.js +++ b/src/permissions/permissions.js @@ -3,13 +3,22 @@ export const permissionGroups = [ { name: '剧集管理', items: ['查看剧集列表', '上传剧集视频', '管理剧集排序'] }, { name: '封面管理', items: ['上传封面素材'] }, { name: '审核与系统', items: ['审核并发布', '查看数据报表'] }, - { name: '后台管理', items: ['角色与权限配置', '管理员用户列表', '菜单管理', '角色分类管理'] }, + { name: '后台管理', items: ['角色与权限配置', '管理员用户列表', '菜单管理', '角色分类管理'] } ] -export const allPermissions = permissionGroups.flatMap(group => group.items) +export const allPermissions = permissionGroups.flatMap((group) => group.items) export const defaultRolePermissions = { 超级管理员: [...allPermissions], - 内容运营: ['查看短剧列表', '创建短剧', '编辑短剧信息', '删除短剧', '查看剧集列表', '上传剧集视频', '管理剧集排序', '上传封面素材'], + 内容运营: [ + '查看短剧列表', + '创建短剧', + '编辑短剧信息', + '删除短剧', + '查看剧集列表', + '上传剧集视频', + '管理剧集排序', + '上传封面素材' + ], 审核专员: ['查看短剧列表', '查看剧集列表', '审核并发布', '查看数据报表'], - 数据分析师: ['查看短剧列表', '查看数据报表'], + 数据分析师: ['查看短剧列表', '查看数据报表'] } diff --git a/src/plugins/animate.css/index.ts b/src/plugins/animate.css/index.ts new file mode 100644 index 0000000..3e93451 --- /dev/null +++ b/src/plugins/animate.css/index.ts @@ -0,0 +1 @@ +import 'animate.css' diff --git a/src/plugins/echarts/index.ts b/src/plugins/echarts/index.ts new file mode 100644 index 0000000..34f756f --- /dev/null +++ b/src/plugins/echarts/index.ts @@ -0,0 +1,41 @@ +import * as echarts from 'echarts/core' + +import { + BarChart, + LineChart, + PieChart, + MapChart, + PictorialBarChart, + RadarChart +} from 'echarts/charts' + +import { + TitleComponent, + TooltipComponent, + GridComponent, + PolarComponent, + AriaComponent, + ParallelComponent, + LegendComponent +} from 'echarts/components' + +import { CanvasRenderer } from 'echarts/renderers' + +echarts.use([ + LegendComponent, + TitleComponent, + TooltipComponent, + GridComponent, + PolarComponent, + AriaComponent, + ParallelComponent, + BarChart, + LineChart, + PieChart, + MapChart, + CanvasRenderer, + PictorialBarChart, + RadarChart +]) + +export default echarts diff --git a/src/plugins/elementPlus/index.ts b/src/plugins/elementPlus/index.ts new file mode 100644 index 0000000..304877f --- /dev/null +++ b/src/plugins/elementPlus/index.ts @@ -0,0 +1,13 @@ +import type { App } from 'vue' + +// 业务页面直接使用 el-table / el-form 等标签,需要注册全部组件。 +// 模板自身页面仍按需 import,两套方式可以共存。 +import ElementPlus from 'element-plus' + +export const setupElementPlus = (app: App- -- 林小满 · {{currentRole}}切换角色 {{role.name}} 退出登录 - -- - ) => { + app.use(ElementPlus) + + if (import.meta.env.VITE_USE_ALL_ELEMENT_PLUS_STYLE === 'true') { + import('element-plus/dist/index.css') + } +} diff --git a/src/plugins/svgIcon/index.ts b/src/plugins/svgIcon/index.ts new file mode 100644 index 0000000..1b11e2d --- /dev/null +++ b/src/plugins/svgIcon/index.ts @@ -0,0 +1 @@ +import 'virtual:svg-icons-register' diff --git a/src/plugins/unocss/index.ts b/src/plugins/unocss/index.ts new file mode 100644 index 0000000..d366b5a --- /dev/null +++ b/src/plugins/unocss/index.ts @@ -0,0 +1 @@ +import 'virtual:uno.css' diff --git a/src/plugins/vueI18n/helper.ts b/src/plugins/vueI18n/helper.ts new file mode 100644 index 0000000..da6bc8c --- /dev/null +++ b/src/plugins/vueI18n/helper.ts @@ -0,0 +1,3 @@ +export const setHtmlPageLang = (locale: LocaleType) => { + document.querySelector('html')?.setAttribute('lang', locale) +} diff --git a/src/plugins/vueI18n/index.ts b/src/plugins/vueI18n/index.ts new file mode 100644 index 0000000..f845b13 --- /dev/null +++ b/src/plugins/vueI18n/index.ts @@ -0,0 +1,42 @@ +import type { App } from 'vue' +import { createI18n } from 'vue-i18n' +import { useLocaleStoreWithOut } from '@/store/modules/locale' +import type { I18n, I18nOptions } from 'vue-i18n' +import { setHtmlPageLang } from './helper' + +export let i18n: ReturnType + +const createI18nOptions = async (): Promise => { + const localeStore = useLocaleStoreWithOut() + const locale = localeStore.getCurrentLocale + const localeMap = localeStore.getLocaleMap + const defaultLocal = await import(`../../locales/${locale.lang}.ts`) + const message = defaultLocal.default ?? {} + + setHtmlPageLang(locale.lang) + + localeStore.setCurrentLocale({ + lang: locale.lang + // elLocale: elLocal + }) + + return { + legacy: false, + locale: locale.lang, + fallbackLocale: locale.lang, + messages: { + [locale.lang]: message + }, + availableLocales: localeMap.map((v) => v.lang), + sync: true, + silentTranslationWarn: true, + missingWarn: false, + silentFallbackWarn: true + } +} + +export const setupI18n = async (app: App ) => { + const options = await createI18nOptions() + i18n = createI18n(options) as I18n + app.use(i18n) +} diff --git a/src/router/index.js b/src/router/index.js deleted file mode 100644 index 8cb7aa1..0000000 --- a/src/router/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRouter, createWebHistory } from 'vue-router' -import { useAuth } from '../stores/auth' -import { useRbac } from '../stores/rbac' -import { customMenus } from '../data/customMenus' - -const router = createRouter({ - history: createWebHistory(), - routes: [ - { path: '/login', name: 'login', component: () => import('@/views/common/LoginView.vue'), meta: { public: true } }, - { path: '/', name: 'admin', component: () => import('@/layouts/AdminLayout.vue'), children: [ - { path: '', name: 'dashboard', component: () => import('@/views/common/DashboardView.vue'), meta: { title: '工作台', permission: '查看短剧列表' } }, - { path: 'dramas', name: 'dramas', component: () => import('@/views/contentManagement/DramaManagementView.vue'), meta: { title: '短剧管理', permission: '查看短剧列表' } }, - { path: 'episodes', name: 'episodes', component: () => import('@/views/contentManagement/EpisodeManagementView.vue'), meta: { title: '剧集管理', permission: '查看剧集列表' } }, - { path: 'access', name: 'access', component: () => import('@/views/permission/RoleManagementView.vue'), meta: { title: '角色权限配置', permission: '角色与权限配置' } }, - { path: 'admin-users', name: 'admin-users', component: () => import('@/views/userManagement/AdminUsersView.vue'), meta: { title: '管理员用户列表', permission: '管理员用户列表' } }, - { path: 'menus', name: 'menus', component: () => import('@/views/permission/MenuManagementView.vue'), meta: { title: '菜单管理', permission: '菜单管理' } }, - { path: 'role-categories', name: 'role-categories', component: () => import('@/views/permission/RoleCategoriesView.vue'), meta: { title: '角色分类', permission: '角色分类管理' } }, - { path: 'settings', name: 'settings', component: () => import('@/views/systemManagement/SettingsView.vue'), meta: { title: '系统设置', permission: '角色与权限配置' } }, - ] }, - { path: '/:pathMatch(.*)*', redirect: '/' }, - ], -}) - -export function registerCustomMenu(menu) { - const name = `custom-menu-${menu.key}` - if (router.hasRoute(name)) return - if (menu.type === '按钮') return - const path = String(menu.route || `custom/${menu.key}`).replace(/^\/+/, '') - router.addRoute('admin', { - path, - name, - component: () => import('@/views/permission/CustomMenuView.vue'), - meta: { title: menu.title, permission: menu.permission, menuKey: menu.key }, - }) -} - -customMenus.forEach(registerCustomMenu) - -router.beforeEach(to => { - const { isAuthenticated } = useAuth() - const { can } = useRbac() - if (!to.meta.public && !isAuthenticated.value) return { name: 'login' } - if (to.name === 'login' && isAuthenticated.value) return { name: 'dashboard' } - if (to.meta.permission && !can(to.meta.permission)) return { name: 'dashboard' } -}) - -export default router diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..70b565a --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,217 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' +import type { App } from 'vue' +import { Layout } from '@/utils/routerHelper' +import { customMenus } from '@/data/customMenus' + +export const constantRouterMap: AppRouteRecordRaw[] = [ + { + path: '/', + component: Layout, + redirect: '/dashboard/index', + name: 'Root', + meta: { + hidden: true + } + }, + { + path: '/redirect', + component: Layout, + name: 'RedirectParent', + children: [ + { + path: '/redirect/:path(.*)', + name: 'Redirect', + component: () => import('@/views/Redirect/Redirect.vue'), + meta: {} + } + ], + meta: { + hidden: true, + noTagsView: true + } + }, + { + path: '/login', + component: () => import('@/views/Login/Login.vue'), + name: 'Login', + meta: { + hidden: true, + title: '登录', + noTagsView: true + } + }, + { + path: '/404', + component: () => import('@/views/Error/404.vue'), + name: 'NoFind', + meta: { + hidden: true, + title: '404', + noTagsView: true + } + } +] + +export const asyncRouterMap: AppRouteRecordRaw[] = [ + { + path: '/dashboard', + component: Layout, + redirect: '/dashboard/index', + name: 'Dashboard', + meta: { + title: '工作台', + icon: 'ant-design:dashboard-filled' + }, + children: [ + { + path: 'index', + name: 'dashboard', + component: () => import('@/views/common/DashboardView.vue'), + meta: { + title: '工作台', + icon: 'ant-design:dashboard-filled', + affix: true + } + } + ] + }, + { + path: '/content', + component: Layout, + redirect: '/content/dramas', + name: 'Content', + meta: { + title: '内容管理', + icon: 'ep:video-camera', + alwaysShow: true + }, + children: [ + { + path: 'dramas', + name: 'dramas', + component: () => import('@/views/contentManagement/DramaManagementView.vue'), + meta: { + title: '短剧管理' + } + }, + { + path: 'episodes', + name: 'episodes', + component: () => import('@/views/contentManagement/EpisodeManagementView.vue'), + meta: { + title: '剧集管理' + } + } + ] + }, + { + path: '/system', + component: Layout, + redirect: '/system/access', + name: 'System', + meta: { + title: '后台管理', + icon: 'ep:setting', + alwaysShow: true + }, + children: [ + { + path: 'access', + name: 'access', + component: () => import('@/views/permission/RoleManagementView.vue'), + meta: { + title: '角色权限配置' + } + }, + { + path: 'admin-users', + name: 'admin-users', + component: () => import('@/views/userManagement/AdminUsersView.vue'), + meta: { + title: '管理员用户列表' + } + }, + { + path: 'menus', + name: 'menus', + component: () => import('@/views/permission/MenuManagementView.vue'), + meta: { + title: '菜单管理' + } + }, + { + path: 'role-categories', + name: 'role-categories', + component: () => import('@/views/permission/RoleCategoriesView.vue'), + meta: { + title: '角色分类' + } + }, + { + path: 'settings', + name: 'settings', + component: () => import('@/views/systemManagement/SettingsView.vue'), + meta: { + title: '系统设置' + } + } + ] + } +] + +export function registerCustomMenu(menu: { + key: string + title: string + route?: string + type?: string +}) { + const name = `custom-menu-${menu.key}` + if (router.hasRoute(name)) return + if (menu.type === '按钮') return + const path = `/${String(menu.route || `custom/${menu.key}`).replace(/^\/+/, '')}` + router.addRoute({ + path, + name, + component: Layout, + meta: { + title: menu.title, + hidden: true + }, + children: [ + { + path: '', + name: `${name}-page`, + component: () => import('@/views/permission/CustomMenuView.vue'), + meta: { + title: menu.title + } + } + ] + }) +} + +const router = createRouter({ + history: createWebHashHistory(), + strict: true, + routes: constantRouterMap as RouteRecordRaw[], + scrollBehavior: () => ({ left: 0, top: 0 }) +}) + +customMenus.forEach(registerCustomMenu) + +export const resetRouter = (): void => { + const resetWhiteNameList = ['Redirect', 'RedirectParent', 'Login', 'NoFind', 'Root'] + router.getRoutes().forEach((route) => { + const { name } = route + if (name && !resetWhiteNameList.includes(name as string)) { + router.hasRoute(name) && router.removeRoute(name) + } + }) +} + +export const setupRouter = (app: App ) => { + app.use(router) +} + +export default router diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..ee92ca8 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,13 @@ +import type { App } from 'vue' +import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' + +const store = createPinia() + +store.use(piniaPluginPersistedstate) + +export const setupStore = (app: App ) => { + app.use(store) +} + +export { store } diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts new file mode 100644 index 0000000..140fd22 --- /dev/null +++ b/src/store/modules/app.ts @@ -0,0 +1,338 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import { setCssVar, humpToUnderline } from '@/utils' +import { ElMessage, ComponentSize } from 'element-plus' +import { colorIsDark, hexToRGB, lighten, mix } from '@/utils/color' +import { unref } from 'vue' +import { useCssVar } from '@vueuse/core' +import { useDark } from '@vueuse/core' + +interface AppState { + breadcrumb: boolean + breadcrumbIcon: boolean + collapse: boolean + uniqueOpened: boolean + hamburger: boolean + screenfull: boolean + size: boolean + locale: boolean + tagsView: boolean + tagsViewIcon: boolean + logo: boolean + fixedHeader: boolean + greyMode: boolean + dynamicRouter: boolean + serverDynamicRouter: boolean + pageLoading: boolean + layout: LayoutType + title: string + isDark: boolean + currentSize: ComponentSize + sizeMap: ComponentSize[] + mobile: boolean + footer: boolean + theme: ThemeTypes + fixedMenu: boolean +} + +export const useAppStore = defineStore('app', { + state: (): AppState => { + return { + sizeMap: ['default', 'large', 'small'], + mobile: false, // 是否是移动端 + title: import.meta.env.VITE_APP_TITLE, // 标题 + pageLoading: false, // 路由跳转loading + breadcrumb: true, // 面包屑 + breadcrumbIcon: true, // 面包屑图标 + collapse: false, // 折叠菜单 + uniqueOpened: false, // 是否只保持一个子菜单的展开 + hamburger: true, // 折叠图标 + screenfull: true, // 全屏图标 + size: true, // 尺寸图标 + locale: true, // 多语言图标 + tagsView: true, // 标签页 + tagsViewIcon: true, // 是否显示标签图标 + logo: true, // logo + fixedHeader: true, // 固定toolheader + footer: false, + greyMode: false, // 是否开始灰色模式,用于特殊悼念日 + dynamicRouter: true, // 是否动态路由 + serverDynamicRouter: true, // 是否服务端渲染动态路由 + fixedMenu: false, // 是否固定菜单 + + layout: 'classic', // layout布局 + isDark: false, // 是否是暗黑模式 + currentSize: 'default', // 组件尺寸 + theme: { + // 主题色 + elColorPrimary: '#409eff', + // 左侧菜单边框颜色 + leftMenuBorderColor: 'inherit', + // 左侧菜单背景颜色 + leftMenuBgColor: '#001529', + // 左侧菜单浅色背景颜色 + leftMenuBgLightColor: '#0f2438', + // 左侧菜单选中背景颜色 + leftMenuBgActiveColor: 'var(--el-color-primary)', + // 左侧菜单收起选中背景颜色 + leftMenuCollapseBgActiveColor: 'var(--el-color-primary)', + // 左侧菜单字体颜色 + leftMenuTextColor: '#bfcbd9', + // 左侧菜单选中字体颜色 + leftMenuTextActiveColor: '#fff', + // logo字体颜色 + logoTitleTextColor: '#fff', + // logo边框颜色 + logoBorderColor: 'inherit', + // 头部背景颜色 + topHeaderBgColor: '#fff', + // 头部字体颜色 + topHeaderTextColor: 'inherit', + // 头部悬停颜色 + topHeaderHoverColor: '#f6f6f6', + // 头部边框颜色 + topToolBorderColor: '#eee' + } + } + }, + getters: { + getBreadcrumb(): boolean { + return this.breadcrumb + }, + getBreadcrumbIcon(): boolean { + return this.breadcrumbIcon + }, + getCollapse(): boolean { + return this.collapse + }, + getUniqueOpened(): boolean { + return this.uniqueOpened + }, + getHamburger(): boolean { + return this.hamburger + }, + getScreenfull(): boolean { + return this.screenfull + }, + getSize(): boolean { + return this.size + }, + getLocale(): boolean { + return this.locale + }, + getTagsView(): boolean { + return this.tagsView + }, + getTagsViewIcon(): boolean { + return this.tagsViewIcon + }, + getLogo(): boolean { + return this.logo + }, + getFixedHeader(): boolean { + return this.fixedHeader + }, + getGreyMode(): boolean { + return this.greyMode + }, + getDynamicRouter(): boolean { + return this.dynamicRouter + }, + getServerDynamicRouter(): boolean { + return this.serverDynamicRouter + }, + getFixedMenu(): boolean { + return this.fixedMenu + }, + getPageLoading(): boolean { + return this.pageLoading + }, + getLayout(): LayoutType { + return this.layout + }, + getTitle(): string { + return this.title + }, + getIsDark(): boolean { + return this.isDark + }, + getCurrentSize(): ComponentSize { + return this.currentSize + }, + getSizeMap(): ComponentSize[] { + return this.sizeMap + }, + getMobile(): boolean { + return this.mobile + }, + getTheme(): ThemeTypes { + return this.theme + }, + getFooter(): boolean { + return this.footer + } + }, + actions: { + setBreadcrumb(breadcrumb: boolean) { + this.breadcrumb = breadcrumb + }, + setBreadcrumbIcon(breadcrumbIcon: boolean) { + this.breadcrumbIcon = breadcrumbIcon + }, + setCollapse(collapse: boolean) { + this.collapse = collapse + }, + setUniqueOpened(uniqueOpened: boolean) { + this.uniqueOpened = uniqueOpened + }, + setHamburger(hamburger: boolean) { + this.hamburger = hamburger + }, + setScreenfull(screenfull: boolean) { + this.screenfull = screenfull + }, + setSize(size: boolean) { + this.size = size + }, + setLocale(locale: boolean) { + this.locale = locale + }, + setTagsView(tagsView: boolean) { + this.tagsView = tagsView + }, + setTagsViewIcon(tagsViewIcon: boolean) { + this.tagsViewIcon = tagsViewIcon + }, + setLogo(logo: boolean) { + this.logo = logo + }, + setFixedHeader(fixedHeader: boolean) { + this.fixedHeader = fixedHeader + }, + setGreyMode(greyMode: boolean) { + this.greyMode = greyMode + }, + setDynamicRouter(dynamicRouter: boolean) { + this.dynamicRouter = dynamicRouter + }, + setServerDynamicRouter(serverDynamicRouter: boolean) { + this.serverDynamicRouter = serverDynamicRouter + }, + setFixedMenu(fixedMenu: boolean) { + this.fixedMenu = fixedMenu + }, + setPageLoading(pageLoading: boolean) { + this.pageLoading = pageLoading + }, + setLayout(layout: LayoutType) { + if (this.mobile && layout !== 'classic') { + ElMessage.warning('移动端模式下不支持切换其它布局') + return + } + this.layout = layout + }, + setTitle(title: string) { + this.title = title + }, + setIsDark(isDark: boolean) { + this.isDark = isDark + if (this.isDark) { + document.documentElement.classList.add('dark') + document.documentElement.classList.remove('light') + } else { + document.documentElement.classList.add('light') + document.documentElement.classList.remove('dark') + } + this.setPrimaryLight() + }, + setCurrentSize(currentSize: ComponentSize) { + this.currentSize = currentSize + }, + setMobile(mobile: boolean) { + this.mobile = mobile + }, + setTheme(theme: ThemeTypes) { + this.theme = Object.assign(this.theme, theme) + }, + setCssVarTheme() { + for (const key in this.theme) { + setCssVar(`--${humpToUnderline(key)}`, this.theme[key]) + } + this.setPrimaryLight() + }, + setFooter(footer: boolean) { + this.footer = footer + }, + setPrimaryLight() { + if (this.theme.elColorPrimary) { + const elColorPrimary = this.theme.elColorPrimary + const color = this.isDark ? '#000000' : '#ffffff' + const lightList = [3, 5, 7, 8, 9] + lightList.forEach((v) => { + setCssVar(`--el-color-primary-light-${v}`, mix(color, elColorPrimary, v / 10)) + }) + setCssVar(`--el-color-primary-dark-2`, mix(color, elColorPrimary, 0.2)) + } + }, + setMenuTheme(color: string) { + const primaryColor = useCssVar('--el-color-primary', document.documentElement) + const isDarkColor = colorIsDark(color) + const theme: Recordable = { + // 左侧菜单边框颜色 + leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee', + // 左侧菜单背景颜色 + leftMenuBgColor: color, + // 左侧菜单浅色背景颜色 + leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color, + // 左侧菜单选中背景颜色 + leftMenuBgActiveColor: isDarkColor + ? 'var(--el-color-primary)' + : hexToRGB(unref(primaryColor), 0.1), + // 左侧菜单收起选中背景颜色 + leftMenuCollapseBgActiveColor: isDarkColor + ? 'var(--el-color-primary)' + : hexToRGB(unref(primaryColor), 0.1), + // 左侧菜单字体颜色 + leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333', + // 左侧菜单选中字体颜色 + leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)', + // logo字体颜色 + logoTitleTextColor: isDarkColor ? '#fff' : 'inherit', + // logo边框颜色 + logoBorderColor: isDarkColor ? color : '#eee' + } + this.setTheme(theme) + this.setCssVarTheme() + }, + setHeaderTheme(color: string) { + const isDarkColor = colorIsDark(color) + const textColor = isDarkColor ? '#fff' : 'inherit' + const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6' + const topToolBorderColor = isDarkColor ? color : '#eee' + setCssVar('--top-header-bg-color', color) + setCssVar('--top-header-text-color', textColor) + setCssVar('--top-header-hover-color', textHoverColor) + this.setTheme({ + topHeaderBgColor: color, + topHeaderTextColor: textColor, + topHeaderHoverColor: textHoverColor, + topToolBorderColor + }) + if (this.getLayout === 'top') { + this.setMenuTheme(color) + } + }, + initTheme() { + const isDark = useDark({ + valueDark: 'dark', + valueLight: 'light' + }) + isDark.value = this.getIsDark + } + }, + persist: true +}) + +export const useAppStoreWithOut = () => { + return useAppStore(store) +} diff --git a/src/store/modules/locale.ts b/src/store/modules/locale.ts new file mode 100644 index 0000000..8d2ba35 --- /dev/null +++ b/src/store/modules/locale.ts @@ -0,0 +1,59 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import en from 'element-plus/es/locale/lang/en' +import { useStorage } from '@/hooks/web/useStorage' +import { LocaleDropdownType } from '@/components/LocaleDropdown' + +const { getStorage, setStorage } = useStorage('localStorage') + +const elLocaleMap = { + 'zh-CN': zhCn, + en: en +} +interface LocaleState { + currentLocale: LocaleDropdownType + localeMap: LocaleDropdownType[] +} + +export const useLocaleStore = defineStore('locales', { + state: (): LocaleState => { + return { + currentLocale: { + lang: getStorage('lang') || 'zh-CN', + elLocale: elLocaleMap[getStorage('lang') || 'zh-CN'] + }, + // 多语言 + localeMap: [ + { + lang: 'zh-CN', + name: '简体中文' + }, + { + lang: 'en', + name: 'English' + } + ] + } + }, + getters: { + getCurrentLocale(): LocaleDropdownType { + return this.currentLocale + }, + getLocaleMap(): LocaleDropdownType[] { + return this.localeMap + } + }, + actions: { + setCurrentLocale(localeMap: LocaleDropdownType) { + // this.locale = Object.assign(this.locale, localeMap) + this.currentLocale.lang = localeMap?.lang + this.currentLocale.elLocale = elLocaleMap[localeMap?.lang] + setStorage('lang', localeMap?.lang) + } + } +}) + +export const useLocaleStoreWithOut = () => { + return useLocaleStore(store) +} diff --git a/src/store/modules/lock.ts b/src/store/modules/lock.ts new file mode 100644 index 0000000..70ff87f --- /dev/null +++ b/src/store/modules/lock.ts @@ -0,0 +1,48 @@ +import { defineStore } from 'pinia' +import { store } from '../index' + +interface lockInfo { + isLock?: boolean + password?: string +} + +interface LockState { + lockInfo: lockInfo +} + +export const useLockStore = defineStore('lock', { + state: (): LockState => { + return { + lockInfo: { + // isLock: false, // 是否锁定屏幕 + // password: '' // 锁屏密码 + } + } + }, + getters: { + getLockInfo(): lockInfo { + return this.lockInfo + } + }, + actions: { + setLockInfo(lockInfo: lockInfo) { + this.lockInfo = lockInfo + }, + resetLockInfo() { + this.lockInfo = {} + }, + unLock(password: string) { + if (this.lockInfo?.password === password) { + this.resetLockInfo() + return true + } else { + return false + } + } + }, + persist: true +}) + +export const useLockStoreWithOut = () => { + return useLockStore(store) +} diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts new file mode 100644 index 0000000..c997306 --- /dev/null +++ b/src/store/modules/permission.ts @@ -0,0 +1,87 @@ +import { defineStore } from 'pinia' +import { asyncRouterMap, constantRouterMap } from '@/router' +import { + generateRoutesByFrontEnd, + generateRoutesByServer, + flatMultiLevelRoutes +} from '@/utils/routerHelper' +import { store } from '../index' +import { cloneDeep } from 'lodash-es' + +export interface PermissionState { + routers: AppRouteRecordRaw[] + addRouters: AppRouteRecordRaw[] + isAddRouters: boolean + menuTabRouters: AppRouteRecordRaw[] +} + +export const usePermissionStore = defineStore('permission', { + state: (): PermissionState => ({ + routers: [], + addRouters: [], + isAddRouters: false, + menuTabRouters: [] + }), + getters: { + getRouters(): AppRouteRecordRaw[] { + return this.routers + }, + getAddRouters(): AppRouteRecordRaw[] { + return flatMultiLevelRoutes(cloneDeep(this.addRouters)) + }, + getIsAddRouters(): boolean { + return this.isAddRouters + }, + getMenuTabRouters(): AppRouteRecordRaw[] { + return this.menuTabRouters + } + }, + actions: { + generateRoutes( + type: 'server' | 'frontEnd' | 'static', + routers?: AppCustomRouteRecordRaw[] | string[] + ): Promise { + return new Promise ((resolve) => { + let routerMap: AppRouteRecordRaw[] = [] + if (type === 'server') { + // 模拟后端过滤菜单 + routerMap = generateRoutesByServer(routers as AppCustomRouteRecordRaw[]) + } else if (type === 'frontEnd') { + // 模拟前端过滤菜单 + routerMap = generateRoutesByFrontEnd(cloneDeep(asyncRouterMap), routers as string[]) + } else { + // 直接读取静态路由表 + routerMap = cloneDeep(asyncRouterMap) + } + // 动态路由,404一定要放到最后面 + this.addRouters = routerMap.concat([ + { + path: '/:path(.*)*', + redirect: '/404', + name: '404Page', + meta: { + hidden: true, + breadcrumb: false + } + } + ]) + // 渲染菜单的所有路由 + this.routers = cloneDeep(constantRouterMap).concat(routerMap) + resolve() + }) + }, + setIsAddRouters(state: boolean): void { + this.isAddRouters = state + }, + setMenuTabRouters(routers: AppRouteRecordRaw[]): void { + this.menuTabRouters = routers + } + }, + persist: { + paths: ['routers', 'addRouters', 'menuTabRouters'] + } +}) + +export const usePermissionStoreWithOut = () => { + return usePermissionStore(store) +} diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts new file mode 100644 index 0000000..d07a439 --- /dev/null +++ b/src/store/modules/tagsView.ts @@ -0,0 +1,163 @@ +import router from '@/router' +import type { RouteLocationNormalizedLoaded } from 'vue-router' +import { getRawRoute } from '@/utils/routerHelper' +import { defineStore } from 'pinia' +import { store } from '../index' +import { findIndex } from '@/utils' +import { useUserStoreWithOut } from './user' + +export interface TagsViewState { + visitedViews: RouteLocationNormalizedLoaded[] + cachedViews: Set + selectedTag?: RouteLocationNormalizedLoaded +} + +export const useTagsViewStore = defineStore('tagsView', { + state: (): TagsViewState => ({ + visitedViews: [], + cachedViews: new Set(), + selectedTag: undefined + }), + getters: { + getVisitedViews(): RouteLocationNormalizedLoaded[] { + return this.visitedViews + }, + getCachedViews(): string[] { + return Array.from(this.cachedViews) + }, + getSelectedTag(): RouteLocationNormalizedLoaded | undefined { + return this.selectedTag + } + }, + actions: { + // 新增缓存和tag + addView(view: RouteLocationNormalizedLoaded): void { + this.addVisitedView(view) + this.addCachedView() + }, + // 新增tag + addVisitedView(view: RouteLocationNormalizedLoaded) { + if (this.visitedViews.some((v) => v.path === view.path)) return + if (view.meta?.noTagsView) return + this.visitedViews.push( + Object.assign({}, view, { + title: view.meta?.title || 'no-name' + }) + ) + }, + // 新增缓存 + addCachedView() { + const cacheMap: Set = new Set() + for (const v of this.visitedViews) { + const item = getRawRoute(v) + const needCache = !item?.meta?.noCache + if (!needCache) { + continue + } + const name = item.name as string + cacheMap.add(name) + } + if (Array.from(this.cachedViews).sort().toString() === Array.from(cacheMap).sort().toString()) + return + this.cachedViews = cacheMap + }, + // 删除某个 + delView(view: RouteLocationNormalizedLoaded) { + this.delVisitedView(view) + this.addCachedView() + }, + // 删除tag + delVisitedView(view: RouteLocationNormalizedLoaded) { + for (const [i, v] of this.visitedViews.entries()) { + if (v.path === view.path) { + this.visitedViews.splice(i, 1) + break + } + } + }, + // 删除缓存 + delCachedView() { + const route = router.currentRoute.value + const index = findIndex (this.getCachedViews, (v) => v === route.name) + if (index > -1) { + this.cachedViews.delete(this.getCachedViews[index]) + } + }, + // 删除所有缓存和tag + delAllViews() { + this.delAllVisitedViews() + this.addCachedView() + }, + // 删除所有tag + delAllVisitedViews() { + const userStore = useUserStoreWithOut() + + // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix) + this.visitedViews = userStore.getUserInfo + ? this.visitedViews.filter((tag) => tag?.meta?.affix) + : [] + }, + // 删除其它 + delOthersViews(view: RouteLocationNormalizedLoaded) { + this.delOthersVisitedViews(view) + this.addCachedView() + }, + // 删除其它tag + delOthersVisitedViews(view: RouteLocationNormalizedLoaded) { + this.visitedViews = this.visitedViews.filter((v) => { + return v?.meta?.affix || v.path === view.path + }) + }, + // 删除左侧 + delLeftViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex ( + this.visitedViews, + (v) => v.path === view.path + ) + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.path === view.path || i > index + }) + this.addCachedView() + } + }, + // 删除右侧 + delRightViews(view: RouteLocationNormalizedLoaded) { + const index = findIndex ( + this.visitedViews, + (v) => v.path === view.path + ) + if (index > -1) { + this.visitedViews = this.visitedViews.filter((v, i) => { + return v?.meta?.affix || v.path === view.path || i < index + }) + this.addCachedView() + } + }, + updateVisitedView(view: RouteLocationNormalizedLoaded) { + for (let v of this.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + }, + // 设置当前选中的tag + setSelectedTag(tag: RouteLocationNormalizedLoaded) { + this.selectedTag = tag + }, + setTitle(title: string, path?: string) { + for (const v of this.visitedViews) { + if (v.path === (path ?? this.selectedTag?.path)) { + v.meta.title = title + break + } + } + } + }, + persist: false +}) + +export const useTagsViewStoreWithOut = () => { + return useTagsViewStore(store) +} diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts new file mode 100644 index 0000000..b29a2b7 --- /dev/null +++ b/src/store/modules/user.ts @@ -0,0 +1,102 @@ +import { defineStore } from 'pinia' +import { store } from '../index' +import { UserLoginType, UserType } from '@/api/login/types' +import { ElMessageBox } from 'element-plus' +import { useI18n } from '@/hooks/web/useI18n' +import { loginOutApi } from '@/api/login' +import { useTagsViewStore } from './tagsView' +import router from '@/router' + +interface UserState { + userInfo?: UserType + tokenKey: string + token: string + roleRouters?: string[] | AppCustomRouteRecordRaw[] + rememberMe: boolean + loginInfo?: UserLoginType +} + +export const useUserStore = defineStore('user', { + state: (): UserState => { + return { + userInfo: undefined, + tokenKey: 'Authorization', + token: '', + roleRouters: undefined, + // 记住我 + rememberMe: true, + loginInfo: undefined + } + }, + getters: { + getTokenKey(): string { + return this.tokenKey + }, + getToken(): string { + return this.token + }, + getUserInfo(): UserType | undefined { + return this.userInfo + }, + getRoleRouters(): string[] | AppCustomRouteRecordRaw[] | undefined { + return this.roleRouters + }, + getRememberMe(): boolean { + return this.rememberMe + }, + getLoginInfo(): UserLoginType | undefined { + return this.loginInfo + } + }, + actions: { + setTokenKey(tokenKey: string) { + this.tokenKey = tokenKey + }, + setToken(token: string) { + this.token = token + }, + setUserInfo(userInfo?: UserType) { + this.userInfo = userInfo + }, + setRoleRouters(roleRouters: string[] | AppCustomRouteRecordRaw[]) { + this.roleRouters = roleRouters + }, + logoutConfirm() { + const { t } = useI18n() + ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), { + confirmButtonText: t('common.ok'), + cancelButtonText: t('common.cancel'), + type: 'warning' + }) + .then(async () => { + const res = await loginOutApi().catch(() => {}) + if (res) { + this.reset() + } + }) + .catch(() => {}) + }, + reset() { + const tagsViewStore = useTagsViewStore() + tagsViewStore.delAllViews() + this.setToken('') + this.setUserInfo(undefined) + this.setRoleRouters([]) + router.replace('/login') + }, + logout() { + this.reset() + }, + setRememberMe(rememberMe: boolean) { + this.rememberMe = rememberMe + }, + setLoginInfo(loginInfo: UserLoginType | undefined) { + this.loginInfo = loginInfo + } + }, + persist: true +}) + +export const useUserStoreWithOut = () => { + return useUserStore(store) +} diff --git a/src/stores/auth.js b/src/stores/auth.js index 9dcfd53..c1fd0ac 100644 --- a/src/stores/auth.js +++ b/src/stores/auth.js @@ -17,6 +17,8 @@ export function useAuth() { currentRole.value = '超级管理员' authenticated.value = false } - function setRole(role) { currentRole.value = role } + function setRole(role) { + currentRole.value = role + } return { isAuthenticated, currentRole, login, logout, setRole } } diff --git a/src/stores/rbac.js b/src/stores/rbac.js index 7a6c6d1..558d6bc 100644 --- a/src/stores/rbac.js +++ b/src/stores/rbac.js @@ -3,23 +3,42 @@ import { defaultRolePermissions } from '../permissions/permissions' import { useAuth } from './auth' import { customMenus } from '../data/customMenus' -const rolePermissions = reactive(Object.fromEntries(Object.entries(defaultRolePermissions).map(([role, list]) => [role, [...list]]))) -rolePermissions['超级管理员'].push(...customMenus.map(menu => menu.permission)) +const rolePermissions = reactive( + Object.fromEntries( + Object.entries(defaultRolePermissions).map(([role, list]) => [role, [...list]]) + ) +) +rolePermissions['超级管理员'].push(...customMenus.map((menu) => menu.permission)) export function useRbac() { const { currentRole } = useAuth() const permissions = computed(() => rolePermissions[currentRole.value] || []) - const can = permission => permissions.value.includes(permission) + const can = (permission) => permissions.value.includes(permission) function updateRolePermission(role, permission, enabled) { const current = rolePermissions[role] || (rolePermissions[role] = []) - rolePermissions[role] = enabled ? [...new Set([...current, permission])] : current.filter(item => item !== permission) + rolePermissions[role] = enabled + ? [...new Set([...current, permission])] + : current.filter((item) => item !== permission) + } + function createRole(name) { + if (!rolePermissions[name]) rolePermissions[name] = [] } - function createRole(name) { if (!rolePermissions[name]) rolePermissions[name] = [] } function createPermission(permission) { - rolePermissions['超级管理员'] = [...new Set([...(rolePermissions['超级管理员'] || []), permission])] + rolePermissions['超级管理员'] = [ + ...new Set([...(rolePermissions['超级管理员'] || []), permission]) + ] } function removePermission(permission) { - for (const role of Object.keys(rolePermissions)) rolePermissions[role] = rolePermissions[role].filter(item => item !== permission) + for (const role of Object.keys(rolePermissions)) + rolePermissions[role] = rolePermissions[role].filter((item) => item !== permission) + } + return { + permissions, + can, + rolePermissions, + updateRolePermission, + createRole, + createPermission, + removePermission } - return { permissions, can, rolePermissions, updateRolePermission, createRole, createPermission, removePermission } } diff --git a/src/stores/theme.js b/src/stores/theme.js deleted file mode 100644 index 81ffe7c..0000000 --- a/src/stores/theme.js +++ /dev/null @@ -1,25 +0,0 @@ -import { ref } from 'vue' - -const storageKey = 'reelshort-theme' -const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') -const storedTheme = localStorage.getItem(storageKey) -const isDark = ref(storedTheme ? storedTheme === 'dark' : systemTheme.matches) - -function applyTheme(value) { - isDark.value = value - document.documentElement.classList.toggle('dark', value) -} - -applyTheme(isDark.value) -systemTheme.addEventListener?.('change', event => { - if (!localStorage.getItem(storageKey)) applyTheme(event.matches) -}) - -export function useTheme() { - function toggleTheme() { - const next = !isDark.value - localStorage.setItem(storageKey, next ? 'dark' : 'light') - applyTheme(next) - } - return { isDark, toggleTheme } -} diff --git a/src/style.css b/src/style.css deleted file mode 100644 index 25ff7dd..0000000 --- a/src/style.css +++ /dev/null @@ -1,1062 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;600;700&display=swap'); - :root { - font-family:'DM Sans','Noto Sans SC',sans-serif; - color:#27253b; - background:#f7f8fc; - font-synthesis:none; - text-rendering:optimizeLegibility; - font-optical-sizing:auto; - font-weight:400; - --purple:#7257e8; - --muted:#9695a8; - --line:#eeedf3} -* { - box-sizing:border-box} -body { - margin:0; - min-width:980px; - min-height:100vh} -.app-shell { - min-height:100vh} -.sidebar { - width:250px; - position:fixed; - inset:0 auto 0 0; - background:#fff; - border-right:1px solid #eeedf4; - padding:27px 17px 16px; - display:flex; - flex-direction:column; - z-index:4} -.brand { - display:flex; - align-items:center; - gap:11px; - padding:0 9px 28px} -.brand-mark { - width:32px; - height:32px; - border-radius:10px; - background:#7359e8; - display:flex; - align-items:center; - justify-content:center; - gap:2px; - transform:rotate(-8deg)} -.brand-mark span { - width:4px; - background:white; - border-radius:4px; - transform:skew(-15deg)} -.brand-mark span:nth-child(1) { - height:11px; - opacity:.72} -.brand-mark span:nth-child(2) { - height:18px} -.brand-mark span:nth-child(3) { - height:13px; - opacity:.85} -.brand b { - display:block; - font:800 18px Manrope,sans-serif; - letter-spacing:-.8px; - color:#242139} -.brand small { - font-size:8px; - letter-spacing:1.5px; - color:#a3a1b1; - display:block; - margin-top:1px} -.workspace { - height:58px; - border:1px solid #efedf4; - border-radius:10px; - display:flex; - align-items:center; - padding:0 10px; - margin-bottom:27px; - gap:10px} -.workspace-avatar,.profile-avatar,.top-avatar { - display:grid; - place-items:center; - background:#efeaff; - color:#6c55da; - font-weight:700} -.workspace-avatar { - width:32px; - height:32px; - border-radius:8px} -.workspace-copy { - flex:1} -.workspace-copy b,.workspace-copy small { - display:block} -.workspace-copy b { - font-size:11px} -.workspace-copy small { - color:#a09eae; - font-size:10px; - margin-top:3px} -.workspace>.el-icon { - color:#aaa8b7; - font-size:13px} -.nav-label { - padding:0 11px 10px; - color:#aaa8b7; - font-size:10px; - letter-spacing:.5px} -.side-menu { - border:0!important} -.side-menu .el-menu-item { - height:43px; - line-height:43px; - border-radius:8px; - margin:2px 0; - color:#777589; - font-size:12px; - padding:0 11px!important} -.side-menu .el-menu-item .el-icon { - font-size:16px; - margin-right:12px} -.side-menu .el-menu-item:hover { - background:#f8f6ff; - color:#6850df} -.side-menu .el-menu-item.is-active { - background:#f1edff; - color:#684fdf; - font-weight:600} -.menu-count { - margin-left:auto; - border-radius:10px; - background:#f0edfa; - color:#8b79df; - padding:2px 7px; - font-size:10px} -.access-label { - margin-top:27px} -.sidebar-bottom { - margin-top:auto} -.help-card { - background:#f8f6ff; - border:1px solid #f0edfa; - border-radius:10px; - padding:13px 13px 14px; - margin:0 2px 17px} -.help-icon { - width:23px; - height:23px; - display:grid; - place-items:center; - background:#ebe5ff; - border-radius:7px; - color:#755de6; - margin-bottom:9px; - font-size:13px} -.help-card b { - font-size:11px} -.help-card p { - font-size:10px; - line-height:1.6; - color:#9290a2; - margin:5px 0 9px} -.help-card a { - font-size:10px; - color:#7258e5; - font-weight:600} -.help-card a span { - margin-left:4px} -.user-profile { - border-top:1px solid #f0eff4; - padding:15px 4px 0; - display:flex; - align-items:center; - gap:9px} -.profile-avatar { - width:33px; - height:33px; - border-radius:50%; - font-size:12px} -.profile-info { - flex:1} -.profile-info b,.profile-info small { - display:block} -.profile-info b { - font-size:11px} -.profile-info small { - font-size:10px; - color:#a09eae; - margin-top:3px} -.user-profile>.el-icon { - color:#aaa8b7; - transform:rotate(90deg)} -.main-area { - margin-left:250px; - min-height:100vh} -.topbar { - height:64px; - background:rgba(255,255,255,.92); - border-bottom:1px solid #eeedf4; - display:flex; - align-items:center; - justify-content:space-between; - padding:0 39px; - position:sticky; - top:0; - z-index:3} -.breadcrumb { - font-size:11px; - color:#aaa8b6} -.breadcrumb .slash { - padding:0 10px; - color:#d4d2dd} -.breadcrumb b { - font-weight:600; - color:#555367} -.top-actions { - display:flex; - align-items:center; - gap:18px} -.environment { - border:1px solid #eeedf2; - border-radius:15px; - padding:6px 10px; - font-size:10px; - color:#777589} -.environment i { - display:inline-block; - width:6px; - height:6px; - border-radius:50%; - background:#62c491; - margin-right:6px} -.icon-button { - position:relative; - color:#777589} -.notification-dot { - width:5px; - height:5px; - border-radius:50%; - background:#ef7d85; - position:absolute; - right:6px; - top:5px; - border:1px solid white} -.top-avatar { - width:30px; - height:30px; - border-radius:50%; - font-size:11px} -.page-content { - max-width:1440px; - margin:0 auto; - padding:35px 40px 46px} -.page-heading { - display:flex; - justify-content:space-between; - align-items:center; - margin-bottom:28px} -.eyebrow { - font-size:9px; - letter-spacing:1.25px; - color:#a5a2b2; - font-weight:600} -.eyebrow span { - color:#a990f0; - margin-left:5px} -.page-heading h1 { - font:700 26px Manrope,'Noto Sans SC',sans-serif; - letter-spacing:-.65px; - margin:8px 0 7px; - color:#29263d} -.page-heading p { - font-size:11px; - color:#9694a7; - margin:0} -.wave { - font-size:20px} -.primary-button { - background:#7157e7!important; - border-color:#7157e7!important; - border-radius:7px!important; - font-size:11px!important; - font-weight:600!important; - height:37px!important; - padding:0 15px!important; - box-shadow:0 4px 10px #7257e822} -.primary-button:hover { - background:#6147d5!important} -.primary-button .el-icon { - margin-right:6px} -.stats-grid { - display:grid; - grid-template-columns:repeat(4,minmax(0,1fr)); - gap:15px; - margin-bottom:18px} -.stat-card,.panel { - background:#fff; - border:1px solid #efedf3; - border-radius:10px} -.stat-card { - padding:16px 17px 13px; - min-height:130px} -.stat-top { - display:flex; - align-items:center; - justify-content:space-between; - color:#858396; - font-size:10px} -.stat-icon { - width:27px; - height:27px; - display:grid; - place-items:center; - border-radius:8px; - font-size:13px} -.lilac { - background:#f0edff; - color:#795fe6} -.blush { - background:#fff0f2; - color:#e98191} -.amber { - background:#fff5e7; - color:#e9aa4b} -.mint { - background:#eaf8f1; - color:#55b987} -.stat-value { - font:700 25px Manrope,sans-serif; - letter-spacing:-.8px; - margin-top:12px; - color:#2c2940} -.stat-value small { - font:500 11px 'Noto Sans SC',sans-serif; - color:#9c99aa; - letter-spacing:0; - margin-left:3px} -.stat-foot { - display:flex; - align-items:center; - gap:6px; - color:#aaa8b6; - font-size:9px; - margin-top:9px} -.positive { - font-size:9px; - color:#49b786; - background:#eef9f4; - padding:3px 5px; - border-radius:4px} -.neutral { - color:#aaa8b6} -.sparkline { - height:24px; - display:flex; - align-items:flex-end; - gap:3px; - margin-left:auto} -.sparkline i { - width:3px; - border-radius:2px 2px 0 0; - opacity:.7} -.purple-line i { - background:#a693f5} -.pink-line i { - background:#ed9bab} -.orange-line i { - background:#f2c17a} -.green-line i { - background:#87d4b0} -.dashboard-grid { - display:grid; - grid-template-columns:1.62fr 1fr; - gap:16px; - margin-bottom:17px} -.panel { - padding:19px 20px} -.panel-heading { - display:flex; - align-items:flex-start; - justify-content:space-between} -.panel-heading h2,.episode-toolbar h2 { - font-size:13px; - margin:0; - color:#302d44; - font-weight:650} -.panel-heading p,.episode-toolbar p { - font-size:10px; - color:#a09eae; - margin:5px 0 0} -.period-select { - width:88px} -.period-select .el-input__wrapper { - box-shadow:0 0 0 1px #efedf3 inset!important} -.period-select .el-input__inner { - font-size:10px} -.chart-summary { - display:flex; - align-items:baseline; - gap:8px; - margin-top:15px} -.chart-summary b { - font:700 20px Manrope,sans-serif; - letter-spacing:-.5px} -.chart-summary>span:not(.positive) { - color:#a09eae; - font-size:10px} -.chart { - display:flex; - margin-top:13px; - height:164px} -.chart-y { - width:39px; - display:flex; - flex-direction:column; - justify-content:space-between; - color:#b2b0bd; - font-size:8px; - padding-bottom:18px} -.chart-main { - position:relative; - flex:1} -.chart-grid { - position:absolute; - inset:0 0 20px; - display:flex; - flex-direction:column; - justify-content:space-between} -.chart-grid i { - border-top:1px dashed #eeedf3} -.chart-svg { - position:absolute; - inset:0 0 20px; - width:100%; - height:calc(100% - 20px); - overflow:visible} -.chart-x { - position:absolute; - bottom:0; - left:0; - right:0; - display:flex; - justify-content:space-between; - color:#b2b0bd; - font-size:8px} -.review-count,.muted-count { - display:inline-grid; - place-items:center; - border-radius:10px; - background:#f2efff; - color:#755bdd; - font:600 9px 'DM Sans',sans-serif; - min-width:18px; - height:18px; - margin-left:4px; - padding:0 5px} -.view-all { - font-size:10px!important; - color:#7963df!important} -.view-all span { - margin-left:5px} -.review-list { - margin-top:17px} -.review-row { - display:flex; - align-items:center; - gap:9px; - padding:11px 0; - border-bottom:1px solid #f3f2f6} -.review-row:last-child { - border-bottom:0} -.review-art { - width:34px; - height:37px; - border-radius:6px; - display:grid; - place-items:center; - font-size:17px} -.art-one { - background:#fff0ed} -.art-two { - background:#f2efff} -.art-three { - background:#eaf6f7} -.review-copy { - flex:1; - min-width:0} -.review-copy b,.review-copy small { - display:block} -.review-copy b { - font-size:10px; - white-space:nowrap; - overflow:hidden; - text-overflow:ellipsis} -.review-copy small { - font-size:8px; - color:#aaa8b6; - margin-top:5px} -.review-row .el-tag,.drama-table .el-tag { - border:0; - font-size:9px; - height:21px} -.recent-panel { - padding-bottom:6px} -.recent-panel .panel-heading { - margin-bottom:15px} -.table-wrap { - overflow-x:auto} -.drama-table { - width:100%; - border-collapse:collapse; - text-align:left; - white-space:nowrap} -.drama-table th { - font-size:9px; - font-weight:500; - color:#aaa8b6; - background:#fafafd; - padding:10px 11px} -.drama-table th:first-child { - border-radius:5px 0 0 5px; - padding-left:12px} -.drama-table th:last-child { - border-radius:0 5px 5px 0} -.drama-table td { - font-size:10px; - color:#767487; - padding:10px 11px; - border-bottom:1px solid #f3f2f6} -.drama-table tr:last-child td { - border-bottom:0} -.drama-cell { - display:flex; - align-items:center; - gap:9px} -.drama-cell b { - font-size:10px; - font-weight:600; - color:#4a475e} -.drama-cover { - width:31px; - height:38px; - border-radius:4px; - display:grid; - place-items:center; - font-size:16px; - flex:none; - box-shadow:inset 0 0 0 1px #00000008} -.rose { - background:linear-gradient(145deg,#f8d6d7,#ffece4)} -.purple { - background:linear-gradient(145deg,#dfd4ff,#f4e4f4)} -.peach { - background:linear-gradient(145deg,#ffd6bd,#fff0d7)} -.blue { - background:linear-gradient(145deg,#d6e7ff,#e4f5fa)} -.gold { - background:linear-gradient(145deg,#f5e3b3,#fff0dc)} -.row-action { - font-size:9px!important; - color:#765ed8!important} -.inner-heading { - margin-bottom:24px} -.library-panel { - padding:0 20px 12px} -.library-toolbar { - display:flex; - align-items:center; - justify-content:space-between; - border-bottom:1px solid #f0eff4; - margin-bottom:0} -.library-toolbar .el-tabs__header,.cover-toolbar .el-tabs__header { - margin:0} -.library-toolbar .el-tabs__nav-wrap:after,.cover-toolbar .el-tabs__nav-wrap:after { - display:none} -.library-toolbar .el-tabs__item,.cover-toolbar .el-tabs__item { - font-size:11px; - color:#9997a7; - height:46px} -.library-toolbar .el-tabs__item.is-active,.cover-toolbar .el-tabs__item.is-active { - color:#7258e2} -.library-toolbar .el-tabs__active-bar,.cover-toolbar .el-tabs__active-bar { - background:#7459e6} -.toolbar-filters { - display:flex; - gap:9px} -.toolbar-filters .el-input { - width:190px} -.toolbar-filters .el-select { - width:120px} -.toolbar-filters .el-input__wrapper,.toolbar-filters .el-select__wrapper { - box-shadow:0 0 0 1px #efedf3 inset!important} -.toolbar-filters .el-input__inner,.toolbar-filters .el-select__placeholder { - font-size:10px} -.full-table th { - padding-top:13px; - padding-bottom:13px} -.full-table td { - padding-top:13px; - padding-bottom:13px} -.full-table .drama-cell small { - display:block; - font-size:9px; - color:#aaa8b6; - margin-top:4px; - max-width:235px; - overflow:hidden; - text-overflow:ellipsis} -.pagination { - display:flex; - align-items:center; - justify-content:space-between; - color:#aaa8b6; - font-size:10px; - padding:15px 2px 3px} -.pagination .el-pagination { - --el-pagination-button-bg-color:#fff; - --el-pagination-hover-color:#7357e8} -.pagination .el-pager li { - font-size:10px} -.empty-cell { - text-align:center!important; - padding:45px!important; - color:#aaa!important} -.episode-panel { - padding:0 23px} -.episode-context { - display:flex; - align-items:center; - padding:20px 0; - border-bottom:1px solid #f0eff4} -.large-cover { - width:48px; - height:58px; - font-size:22px; - border-radius:6px} -.episode-context-copy { - margin-left:12px; - display:flex; - flex-direction:column; - gap:4px} -.episode-context-copy>span { - font-size:9px; - color:#a7a5b3} -.episode-context-copy b { - font-size:13px} -.episode-context-copy small { - font-size:9px; - color:#aaa8b6} -.drama-select { - margin-left:auto; - width:215px} -.drama-select .el-input__wrapper { - box-shadow:0 0 0 1px #efedf3 inset!important} -.drama-select .el-input__inner { - font-size:10px} -.episode-toolbar { - display:flex; - align-items:center; - justify-content:space-between; - padding:20px 0 14px} -.secondary-button { - border-radius:6px!important; - border-color:#eae8f0!important; - color:#6e6c80!important; - font-size:10px!important; - height:31px!important} -.secondary-button .el-icon { - margin-right:5px} -.episode-row { - height:69px; - border:1px solid #efedf4; - border-radius:7px; - display:flex; - align-items:center; - gap:12px; - padding:8px 12px; - margin-bottom:8px} -.drag-handle { - color:#c2c0cb; - font-size:17px; - cursor:grab} -.episode-thumb { - width:62px; - height:45px; - border-radius:5px; - background:linear-gradient(135deg,#29273b,#55516e); - display:grid; - place-items:center; - color:#fff; - font-size:15px} -.episode-title { - flex:1} -.episode-title b,.episode-title small { - display:block} -.episode-title b { - font-size:10px; - color:#49465a} -.episode-title small { - font-size:9px; - color:#aaa8b6; - margin-top:6px} -.episode-title small .el-icon { - vertical-align:middle} -.episode-row .el-tag { - font-size:9px; - border:0} -.episode-actions { - display:flex; - margin-left:8px} -.episode-actions .el-button { - padding:5px; - color:#9997a7} -.episode-actions .danger-action:hover { - color:#e77680} -.episode-footer { - display:flex; - justify-content:space-between; - align-items:center; - color:#aaa8b6; - font-size:9px; - padding:12px 1px} -.cover-toolbar { - display:flex; - align-items:center; - justify-content:space-between; - margin-bottom:17px; - border-bottom:1px solid #eeedf3} -.cover-grid { - display:grid; - grid-template-columns:repeat(5,minmax(130px,1fr)); - gap:15px} -.cover-card,.add-cover { - background:white; - border:1px solid #eeedf3; - border-radius:9px; - overflow:hidden} -.poster-art { - height:235px; - position:relative; - display:flex; - flex-direction:column; - justify-content:flex-end; - padding:17px; - overflow:hidden} -.poster-art.rose { - background:linear-gradient(155deg,#e8bfc5,#8b455e 65%,#36273c)} -.poster-art.purple { - background:linear-gradient(155deg,#beb3e8,#584879 65%,#28263c)} -.poster-art.peach { - background:linear-gradient(155deg,#f4c9a8,#cb7181 65%,#553b56)} -.poster-art.blue { - background:linear-gradient(155deg,#9bc2d7,#426b85 65%,#292c4e)} -.poster-art.gold { - background:linear-gradient(155deg,#efd8a6,#b8754d 65%,#48334b)} -.poster-glow { - position:absolute; - width:150px; - height:150px; - border-radius:50%; - background:#ffffff30; - filter:blur(25px); - top:30px; - left:30px} -.poster-emoji { - position:absolute; - top:45px; - left:50%; - transform:translateX(-50%); - font-size:68px; - filter:drop-shadow(0 6px 6px #0004)} -.poster-copy { - position:relative; - color:white; - text-shadow:0 1px 5px #0008; - z-index:1} -.poster-copy small { - display:block; - font:600 7px 'DM Sans'; - letter-spacing:1.6px; - opacity:.8; - margin-bottom:8px} -.poster-copy b { - display:block; - font-size:15px; - line-height:1.5} -.poster-copy span { - display:block; - font-size:8px; - margin-top:5px; - opacity:.8} -.poster-stamp { - position:absolute; - right:10px; - top:12px; - color:white; - border:1px solid #ffffff8a; - padding:4px 6px; - font-size:8px; - border-radius:3px} -.cover-card-info { - padding:10px; - display:flex; - align-items:center; - justify-content:space-between} -.cover-card-info b,.cover-card-info small { - display:block; - max-width:140px; - overflow:hidden; - text-overflow:ellipsis; - white-space:nowrap} -.cover-card-info b { - font-size:9px} -.cover-card-info small { - font-size:8px; - color:#aaa8b6; - margin-top:4px} -.add-cover { - min-height:276px; - border-style:dashed; - display:flex; - flex-direction:column; - align-items:center; - justify-content:center; - color:#858397; - cursor:pointer} -.add-cover>span { - width:36px; - height:36px; - border-radius:50%; - display:grid; - place-items:center; - background:#f3f0ff; - color:#755ce4; - font-size:17px; - margin-bottom:11px} -.add-cover b { - font-size:10px; - font-weight:600} -.add-cover small { - font-size:8px; - color:#aaa8b6; - margin-top:6px} -.access-layout { - display:grid; - grid-template-columns:1fr 1.48fr; - gap:17px} -.roles-panel,.permissions-panel { - padding:19px} -.role-list { - margin-top:14px; - display:flex; - flex-direction:column; - gap:8px} -.role-card { - width:100%; - border:1px solid #efedf4; - background:#fff; - border-radius:8px; - display:flex; - align-items:center; - text-align:left; - padding:11px; - gap:10px; - cursor:pointer; - font:inherit} -.role-card:hover { - border-color:#d9d1fa} -.role-active { - border-color:#bcb0f2; - background:#faf9ff} -.role-icon { - width:34px; - height:34px; - display:grid; - place-items:center; - border-radius:8px; - font-size:15px} -.role-icon.red { - background:#fff0ef; - color:#e47b79} -.role-icon.purple { - background:#f1edff; - color:#785ee5} -.role-icon.blue { - background:#edf5ff; - color:#6195dc} -.role-icon.green { - background:#eaf8f0; - color:#50ae80} -.role-copy { - flex:1} -.role-copy b,.role-copy small,.role-copy span { - display:block} -.role-copy b { - font-size:10px; - color:#47445a} -.role-copy small { - font-size:8px; - color:#a09eae; - margin-top:4px} -.role-copy span { - font-size:8px; - color:#aaa8b6; - margin-top:6px} -.role-chevron { - font-size:10px; - color:#aaa8b6; - transform:rotate(-90deg)} -.role-badge { - font-size:8px; - color:#7b63df; - background:#f1edff; - border-radius:10px; - padding:4px 7px; - vertical-align:2px; - margin-left:5px; - font-weight:500} -.permission-groups { - display:grid; - grid-template-columns:1fr 1fr; - gap:15px; - margin:23px 0 18px} -.permission-group { - display:flex; - flex-direction:column; - gap:14px} -.permission-group h3 { - font-size:10px; - margin:0 0 2px; - color:#5b586d} -.permission-group h3 .el-icon { - font-size:12px; - vertical-align:middle; - margin-right:5px; - color:#816be2} -.permission-group .el-checkbox { - height:auto; - margin:0} -.permission-group .el-checkbox__label { - font-size:9px; - color:#858397} -.permission-group .el-checkbox__input.is-checked .el-checkbox__inner { - background:#765ce5; - border-color:#765ce5} -.permission-note { - padding:10px; - background:#f8f7fc; - border-radius:6px; - font-size:8px; - color:#9997a8; - display:flex; - align-items:center; - gap:7px} -.permission-note .el-icon { - color:#8a78dc} -.placeholder-panel { - text-align:center; - padding:70px 20px} -.placeholder-icon { - display:grid; - place-items:center; - width:45px; - height:45px; - border-radius:12px; - margin:auto; - background:#f1edff; - color:#765ce5; - font-size:20px} -.placeholder-panel h2 { - font-size:14px; - margin:14px 0 5px} -.placeholder-panel p { - font-size:10px; - color:#a09eae} -.el-dialog { - border-radius:11px!important} -.el-dialog__header { - padding:20px 22px 12px!important; - margin-right:0!important; - border-bottom:1px solid #f0eff4} -.el-dialog__title { - font-size:14px; - font-weight:650} -.el-dialog__body { - padding:15px 22px 5px!important} -.el-dialog__footer { - padding:13px 22px 19px!important; - border-top:1px solid #f2f1f5} -.dialog-subtitle { - font-size:10px; - color:#9997a8; - margin-bottom:16px} -.drama-form .form-row,.app-dialog .form-row { - display:grid; - grid-template-columns:1fr 1fr; - gap:14px} -.app-dialog .el-form-item { - margin-bottom:14px} -.app-dialog .el-form-item__label { - font-size:10px; - color:#666377; - padding-bottom:5px} -.app-dialog .el-input__inner,.app-dialog .el-textarea__inner,.app-dialog .el-select__placeholder { - font-size:10px} -.upload-area { - width:100%} -.upload-area .el-upload,.upload-area .el-upload-dragger { - width:100%} -.upload-area .el-upload-dragger { - height:112px; - padding:14px; - border-radius:7px; - border-color:#e8e5ef; - background:#fcfbfe} -.upload-icon { - font-size:20px; - color:#8a75e5; - margin-bottom:6px} -.upload-area .el-upload__text { - font-size:10px; - color:#777589} -.upload-area .el-upload__text em { - font-style:normal; - color:#7358e2} -.upload-hint { - font-size:8px; - color:#aaa8b6; - margin-top:5px} -.app-dialog .el-input-number { - width:100%} -.app-dialog .el-input-number .el-input__wrapper { - width:100%} -@media(min-width:1500px) { - .page-content { - padding-left:48px; - padding-right:48px} -.stats-grid { - gap:18px} -.stat-card { - min-height:141px; - padding:18px 19px} -.cover-grid { - grid-template-columns:repeat(6,minmax(130px,1fr))} -} -@media(max-width:1200px) { - .sidebar { - width:220px} -.main-area { - margin-left:220px} -.page-content { - padding:30px 26px} -.topbar { - padding:0 26px} -.stats-grid { - gap:10px} -.stat-card { - padding:14px} -.dashboard-grid { - grid-template-columns:1.35fr 1fr; - gap:12px} -.cover-grid { - grid-template-columns:repeat(4,minmax(130px,1fr))} -} diff --git a/src/styles/business/dark.css b/src/styles/business/dark.css new file mode 100644 index 0000000..8d213a9 --- /dev/null +++ b/src/styles/business/dark.css @@ -0,0 +1,130 @@ +html.dark .page-heading h1, +html.dark .panel-heading h2, +html.dark .episode-toolbar h2, +html.dark .stat-value, +html.dark .drama-cell b, +html.dark .brand b, +html.dark .role-card, +html.dark .cover-card-info b { + color: var(--el-text-color-primary); +} + +html.dark .page-heading p, +html.dark .eyebrow, +html.dark .panel-heading p, +html.dark .episode-toolbar p, +html.dark .stat-top, +html.dark .stat-value small, +html.dark .stat-foot, +html.dark .table-result-count { + color: var(--el-text-color-secondary); +} + +html.dark .stat-card, +html.dark .panel, +html.dark .role-card, +html.dark .topbar, +html.dark .sidebar { + background: var(--el-bg-color-overlay); + border-color: var(--el-border-color-light); +} + +html.dark .stat-icon, +html.dark .role-icon, +html.dark .help-icon, +html.dark .workspace-avatar, +html.dark .profile-avatar, +html.dark .top-avatar { + background: rgb(64 158 255 / 16%) !important; + color: var(--el-color-primary) !important; +} + +html.dark .role-active { + background: rgb(64 158 255 / 12%); + border-color: var(--el-color-primary); +} + +html.dark .drama-cover, +html.dark .episode-thumb, +html.dark .review-art, +html.dark .add-cover, +html.dark .upload-area .el-upload-dragger, +html.dark .permission-note { + background: var(--el-fill-color-light) !important; + color: var(--el-text-color-secondary); + border-color: var(--el-border-color); +} + +html.dark .review-row .el-tag, +html.dark .drama-table .el-tag, +html.dark .episode-row .el-tag { + background: var(--el-fill-color) !important; + color: var(--el-text-color-regular) !important; + border-color: var(--el-border-color) !important; +} + +html.dark .drama-table th, +html.dark .drama-table td { + background: transparent; + color: var(--el-text-color-regular); + border-color: var(--el-border-color-lighter); +} + +html.dark .drama-el-table, +html.dark .dashboard-el-table { + --el-table-header-bg-color: var(--el-fill-color-light); + --el-table-tr-bg-color: var(--el-bg-color-overlay); + --el-table-bg-color: var(--el-bg-color-overlay); + --el-table-border-color: var(--el-border-color); + --el-table-row-hover-bg-color: var(--el-fill-color); + --el-table-text-color: var(--el-text-color-regular); + --el-table-header-text-color: var(--el-text-color-secondary); +} + +html.dark .drama-el-table th.el-table__cell, +html.dark .dashboard-el-table th.el-table__cell, +html.dark .drama-el-table td.el-table__cell, +html.dark .dashboard-el-table td.el-table__cell { + color: var(--el-text-color-regular); +} + +html.dark .el-tag.status-badge.is-online { + --el-tag-bg-color: rgba(34, 160, 90, 0.18); + --el-tag-border-color: rgba(103, 194, 58, 0.45); + --el-tag-text-color: #7dcea0; + background: rgba(34, 160, 90, 0.18) !important; + border-color: rgba(103, 194, 58, 0.45) !important; + color: #7dcea0 !important; +} +html.dark .el-tag.status-badge.is-reviewing { + --el-tag-bg-color: rgba(217, 119, 6, 0.18); + --el-tag-border-color: rgba(230, 162, 60, 0.45); + --el-tag-text-color: #f0c27a; + background: rgba(217, 119, 6, 0.18) !important; + border-color: rgba(230, 162, 60, 0.45) !important; + color: #f0c27a !important; +} +html.dark .el-tag.status-badge.is-draft { + --el-tag-bg-color: rgba(144, 147, 153, 0.18); + --el-tag-border-color: rgba(168, 171, 178, 0.4); + --el-tag-text-color: #c0c4cc; + background: rgba(144, 147, 153, 0.18) !important; + border-color: rgba(168, 171, 178, 0.4) !important; + color: #c0c4cc !important; +} +html.dark .el-tag.status-badge.is-pending { + --el-tag-bg-color: rgba(64, 158, 255, 0.18); + --el-tag-border-color: rgba(121, 187, 255, 0.45); + --el-tag-text-color: #8cc5ff; + background: rgba(64, 158, 255, 0.18) !important; + border-color: rgba(121, 187, 255, 0.45) !important; + color: #8cc5ff !important; +} +html.dark .el-tag.status-badge.is-offline { + --el-tag-bg-color: rgba(245, 108, 108, 0.18); + --el-tag-border-color: rgba(245, 108, 108, 0.45); + --el-tag-text-color: #f3a6a6; + background: rgba(245, 108, 108, 0.18) !important; + border-color: rgba(245, 108, 108, 0.45) !important; + color: #f3a6a6 !important; +} diff --git a/src/styles/business/drama-management.css b/src/styles/business/drama-management.css new file mode 100644 index 0000000..981feaa --- /dev/null +++ b/src/styles/business/drama-management.css @@ -0,0 +1,349 @@ +.library-toolbar { + justify-content: flex-start; + gap: 16px; + min-height: 64px; + padding: 12px 0 !important; +} +.drama-filters { + display: flex; + align-items: center; + gap: 10px; + flex: 0 0 auto; +} +.drama-filters > .el-input { + width: 300px !important; + flex: 0 0 300px !important; +} +.drama-filters > .el-select { + width: 160px !important; + flex: 0 0 160px !important; +} +.table-result-count { + margin-left: auto; + color: #858b95; + font-size: 13px; + white-space: nowrap; +} +.table-actions { + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; +} +.table-actions .el-button { + font-size: 13px !important; + font-weight: 400 !important; + padding: 5px 4px !important; +} +.table-actions .danger-action { + color: #a66a6a !important; +} +.table-actions .danger-action:hover { + color: #b54848 !important; +} +.drama-cell .drama-cover, +.drama-cell .drama-cover-image { + width: 44px; + height: 56px; + flex: 0 0 44px; + border-radius: 4px; + object-fit: cover; +} +.drama-cell .drama-cover { + display: grid; + place-content: center; + text-align: center; + font-size: 10px; + line-height: 1.3; + color: #7e8791; + background: #f0f2f4 !important; +} +.dark .table-result-count { + color: #a6aeb9; +} +.dark .drama-cell .drama-cover { + background: #353b45 !important; + color: #b9c1cb; +} +@media (max-width: 800px) { + .drama-filters > .el-input { + width: min(300px, 45vw) !important; + flex-basis: min(300px, 45vw) !important; + } + .drama-filters > .el-select { + width: 150px !important; + flex-basis: 150px !important; + } + .table-result-count { + display: none; + } +} +.drama-el-table, +.dashboard-el-table { + width: 100%; + font-size: 13px; + --el-table-header-bg-color: #f7f8fa; + --el-table-border-color: #eaecf0; + --el-table-row-hover-bg-color: #f8fafc; +} +.drama-el-table .el-table__cell, +.dashboard-el-table .el-table__cell { + padding: 14px 0; +} +.drama-el-table th.el-table__cell, +.dashboard-el-table th.el-table__cell { + font-size: 12px; + font-weight: 600; + color: #737b85; +} +.drama-el-table td.el-table__cell, +.dashboard-el-table td.el-table__cell { + font-size: 13px; + color: #505862; +} +.drama-el-table .cell, +.dashboard-el-table .cell { + line-height: 1.5; +} +.drama-el-table .el-table__inner-wrapper:before, +.dashboard-el-table .el-table__inner-wrapper:before { + height: 0; +} +.dark .drama-el-table, +.dark .dashboard-el-table { + --el-table-header-bg-color: #2b3038; + --el-table-tr-bg-color: #242830; + --el-table-bg-color: #242830; + --el-table-border-color: #3b414b; + --el-table-row-hover-bg-color: #2e3540; + --el-table-text-color: #c0c6cf; + --el-table-header-text-color: #b1b8c2; +} +.dark .drama-el-table td.el-table__cell, +.dark .dashboard-el-table td.el-table__cell { + color: #c0c6cf; +} +.table-actions .el-button { + font-size: 13px !important; + font-weight: 600 !important; + padding: 7px 11px !important; +} +.table-actions .el-button--primary { + --el-button-bg-color: #409eff; + --el-button-border-color: #409eff; + --el-button-hover-bg-color: #79bbff; + --el-button-hover-border-color: #79bbff; +} +.table-actions .el-button--success { + --el-button-bg-color: #4d866a; + --el-button-border-color: #4d866a; + --el-button-hover-bg-color: #3c7358; + --el-button-hover-border-color: #3c7358; +} +.table-actions .el-button--danger { + --el-button-bg-color: #bd5b5b; + --el-button-border-color: #bd5b5b; + --el-button-hover-bg-color: #a94b4b; + --el-button-hover-border-color: #a94b4b; +} +.pagination { + display: flex; + justify-content: flex-end; + align-items: center; + padding: 18px 0 4px !important; + overflow-x: auto; +} +.pagination .el-pagination { + font-size: 13px !important; + --el-pagination-font-size: 13px; + --el-pagination-button-bg-color: #f1f3f6; + --el-pagination-hover-color: #409eff; + --el-pagination-button-color: #4c5561; + gap: 5px; +} +.pagination .el-pagination__sizes { + margin-right: 8px; +} +.pagination .el-select { + width: 112px; +} +.pagination .el-pagination__jump { + margin-left: 8px; + white-space: nowrap; +} +.dark .pagination .el-pagination { + --el-pagination-button-bg-color: #353b45; + --el-pagination-button-color: #c3cad3; +} +.drama-el-table td.el-table__cell, +.drama-el-table th.el-table__cell { + font-size: 14px; +} +.drama-cell b { + font-size: 15px; +} +.drama-cell small { + font-size: 13px; +} +.table-result-count { + font-size: 14px; +} +.table-actions .el-button { + font-size: 14px !important; +} +.library-toolbar .el-input__inner, +.library-toolbar .el-select__placeholder { + font-size: 14px; +} +.pagination .el-pagination { + font-size: 14px !important; + --el-pagination-font-size: 14px; +} +.el-message-box__title, +.el-message-box__message { + font-size: 15px; +} +.el-message-box__btns .el-button { + font-size: 14px; +} +.episode-toolbar { + margin-bottom: 16px; +} +.episode-el-table { + margin-top: 4px; +} +.episode-table-title b, +.episode-table-title small { + display: block; +} +.episode-table-title b { + font-size: 14px; + color: #343a43; +} +.episode-table-title small { + margin-top: 4px; + font-size: 13px; + color: #858b95; +} +.dark .episode-table-title b { + color: #e5e8ed; +} +.dark .episode-table-title small { + color: #a6aeb9; +} +.menu-management-panel { + margin-top: 20px; +} +.menu-management-panel .panel-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} +.menu-management-panel .el-alert { + margin-top: 4px; +} +.menu-management-panel .el-select { + width: 100%; +} +.admin-user-search { + width: min(440px, 65vw); +} +.muted-text { + font-size: 13px; + color: #9298a1; +} +.role-permission-layout { + display: grid; + grid-template-columns: minmax(520px, 1.25fr) minmax(340px, 0.9fr); + gap: 18px; + align-items: start; +} +.role-table-panel, +.permission-tree-panel { + min-width: 0; +} +.role-config-table .el-table__row { + cursor: pointer; +} +.permission-tree-panel .el-tree { + max-height: 68vh; + overflow: auto; + padding: 5px 8px; +} +.permission-tree-panel .el-tree-node__label { + font-size: 14px; +} +.permission-warning { + color: #d66b4f !important; +} +.admin-users-panel { + overflow: hidden; +} +.admin-user-toolbar { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 0 0 16px; +} +.admin-user-toolbar > .el-input { + width: 230px; +} +.admin-user-toolbar > .el-select { + width: 190px; +} +.admin-users-table { + min-width: 1250px; +} +.menu-editor-layout { + display: grid; + grid-template-columns: minmax(270px, 0.7fr) minmax(520px, 1.5fr); + gap: 18px; + align-items: start; +} +.menu-tree-panel, +.menu-form-panel { + min-width: 0; +} +.menu-tree-panel .el-tree { + max-height: 70vh; + overflow: auto; +} +.menu-form-panel .panel-heading { + display: flex; + justify-content: space-between; + align-items: center; +} +.menu-editor-form { + max-width: 680px; + padding: 16px 12px 0; +} +.menu-editor-form .el-input, +.menu-editor-form .el-select { + max-width: 360px; + width: 100%; +} +.menu-editor-form .el-textarea { + max-width: 360px; +} +.menu-editor-form .el-input-number { + width: 180px; +} +.menu-form-footer { + padding: 12px 0 4px 122px; + border-top: 1px solid #eceef0; +} +.dark .menu-form-footer { + border-color: #3b414b; +} +@media (max-width: 1150px) { + .role-permission-layout, + .menu-editor-layout { + grid-template-columns: 1fr; + } + .permission-tree-panel .el-tree, + .menu-tree-panel .el-tree { + max-height: 45vh; + } +} diff --git a/src/styles/business/index.css b/src/styles/business/index.css new file mode 100644 index 0000000..5994ba9 --- /dev/null +++ b/src/styles/business/index.css @@ -0,0 +1,5 @@ +@import './style.css'; +@import './theme.css'; +@import './typography.css'; +@import './drama-management.css'; +@import './dark.css'; diff --git a/src/styles/business/style.css b/src/styles/business/style.css new file mode 100644 index 0000000..471fa01 --- /dev/null +++ b/src/styles/business/style.css @@ -0,0 +1,1369 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;600;700&display=swap'); +:root { + font-family: 'DM Sans', 'Noto Sans SC', sans-serif; + color: var(--el-text-color-primary); + background: var(--el-bg-color-page); + font-synthesis: none; + text-rendering: optimizeLegibility; + font-optical-sizing: auto; + font-weight: 400; + --purple: #409eff; + --muted: #9695a8; + --line: #eeedf3; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + min-width: 980px; + min-height: 100vh; +} +.app-shell { + min-height: 100vh; +} +.sidebar { + width: 250px; + position: fixed; + inset: 0 auto 0 0; + background: #fff; + border-right: 1px solid #eeedf4; + padding: 27px 17px 16px; + display: flex; + flex-direction: column; + z-index: 4; +} +.brand { + display: flex; + align-items: center; + gap: 11px; + padding: 0 9px 28px; +} +.brand-mark { + width: 32px; + height: 32px; + border-radius: 10px; + background: #409eff; + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + transform: rotate(-8deg); +} +.brand-mark span { + width: 4px; + background: white; + border-radius: 4px; + transform: skew(-15deg); +} +.brand-mark span:nth-child(1) { + height: 11px; + opacity: 0.72; +} +.brand-mark span:nth-child(2) { + height: 18px; +} +.brand-mark span:nth-child(3) { + height: 13px; + opacity: 0.85; +} +.brand b { + display: block; + font: + 800 18px Manrope, + sans-serif; + letter-spacing: -0.8px; + color: #242139; +} +.brand small { + font-size: 8px; + letter-spacing: 1.5px; + color: #a3a1b1; + display: block; + margin-top: 1px; +} +.workspace { + height: 58px; + border: 1px solid #efedf4; + border-radius: 10px; + display: flex; + align-items: center; + padding: 0 10px; + margin-bottom: 27px; + gap: 10px; +} +.workspace-avatar, +.profile-avatar, +.top-avatar { + display: grid; + place-items: center; + background: #ecf5ff; + color: #409eff; + font-weight: 700; +} +.workspace-avatar { + width: 32px; + height: 32px; + border-radius: 8px; +} +.workspace-copy { + flex: 1; +} +.workspace-copy b, +.workspace-copy small { + display: block; +} +.workspace-copy b { + font-size: 11px; +} +.workspace-copy small { + color: #a09eae; + font-size: 10px; + margin-top: 3px; +} +.workspace > .el-icon { + color: #aaa8b7; + font-size: 13px; +} +.nav-label { + padding: 0 11px 10px; + color: #aaa8b7; + font-size: 10px; + letter-spacing: 0.5px; +} +.side-menu { + border: 0 !important; +} +.side-menu .el-menu-item { + height: 43px; + line-height: 43px; + border-radius: 8px; + margin: 2px 0; + color: #777589; + font-size: 12px; + padding: 0 11px !important; +} +.side-menu .el-menu-item .el-icon { + font-size: 16px; + margin-right: 12px; +} +.side-menu .el-menu-item:hover { + background: #ecf5ff; + color: #409eff; +} +.side-menu .el-menu-item.is-active { + background: #ecf5ff; + color: #409eff; + font-weight: 600; +} +.menu-count { + margin-left: auto; + border-radius: 10px; + background: #ecf5ff; + color: #79bbff; + padding: 2px 7px; + font-size: 10px; +} +.access-label { + margin-top: 27px; +} +.sidebar-bottom { + margin-top: auto; +} +.help-card { + background: #ecf5ff; + border: 1px solid #ecf5ff; + border-radius: 10px; + padding: 13px 13px 14px; + margin: 0 2px 17px; +} +.help-icon { + width: 23px; + height: 23px; + display: grid; + place-items: center; + background: #d9ecff; + border-radius: 7px; + color: #409eff; + margin-bottom: 9px; + font-size: 13px; +} +.help-card b { + font-size: 11px; +} +.help-card p { + font-size: 10px; + line-height: 1.6; + color: #9290a2; + margin: 5px 0 9px; +} +.help-card a { + font-size: 10px; + color: #409eff; + font-weight: 600; +} +.help-card a span { + margin-left: 4px; +} +.user-profile { + border-top: 1px solid #f0eff4; + padding: 15px 4px 0; + display: flex; + align-items: center; + gap: 9px; +} +.profile-avatar { + width: 33px; + height: 33px; + border-radius: 50%; + font-size: 12px; +} +.profile-info { + flex: 1; +} +.profile-info b, +.profile-info small { + display: block; +} +.profile-info b { + font-size: 11px; +} +.profile-info small { + font-size: 10px; + color: #a09eae; + margin-top: 3px; +} +.user-profile > .el-icon { + color: #aaa8b7; + transform: rotate(90deg); +} +.main-area { + margin-left: 250px; + min-height: 100vh; +} +.topbar { + height: 64px; + background: rgba(255, 255, 255, 0.92); + border-bottom: 1px solid #eeedf4; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 39px; + position: sticky; + top: 0; + z-index: 3; +} +.breadcrumb { + font-size: 11px; + color: #aaa8b6; +} +.breadcrumb .slash { + padding: 0 10px; + color: #d4d2dd; +} +.breadcrumb b { + font-weight: 600; + color: #555367; +} +.top-actions { + display: flex; + align-items: center; + gap: 18px; +} +.environment { + border: 1px solid #eeedf2; + border-radius: 15px; + padding: 6px 10px; + font-size: 10px; + color: #777589; +} +.environment i { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: #62c491; + margin-right: 6px; +} +.icon-button { + position: relative; + color: #777589; +} +.notification-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: #ef7d85; + position: absolute; + right: 6px; + top: 5px; + border: 1px solid white; +} +.top-avatar { + width: 30px; + height: 30px; + border-radius: 50%; + font-size: 11px; +} +.page-content { + max-width: 1440px; + margin: 0 auto; + padding: 35px 40px 46px; +} +.page-heading { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 28px; +} +.eyebrow { + font-size: 9px; + letter-spacing: 1.25px; + color: #a5a2b2; + font-weight: 600; +} +.eyebrow span { + color: #79bbff; + margin-left: 5px; +} +.page-heading h1 { + font: + 700 26px Manrope, + 'Noto Sans SC', + sans-serif; + letter-spacing: -0.65px; + margin: 8px 0 7px; + color: #29263d; +} +.page-heading p { + font-size: 11px; + color: #9694a7; + margin: 0; +} +.wave { + font-size: 20px; +} +.primary-button { + background: #409eff !important; + border-color: #409eff !important; + border-radius: 7px !important; + font-size: 11px !important; + font-weight: 600 !important; + height: 37px !important; + padding: 0 15px !important; + box-shadow: 0 4px 10px #409eff22; +} +.primary-button:hover { + background: #337ecc !important; +} +.primary-button .el-icon { + margin-right: 6px; +} +.stats-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 15px; + margin-bottom: 18px; +} +.stat-card, +.panel { + background: #fff; + border: 1px solid #efedf3; + border-radius: 10px; +} +.stat-card { + padding: 16px 17px 13px; + min-height: 130px; +} +.stat-top { + display: flex; + align-items: center; + justify-content: space-between; + color: #858396; + font-size: 10px; +} +.stat-icon { + width: 27px; + height: 27px; + display: grid; + place-items: center; + border-radius: 8px; + font-size: 13px; +} +.lilac { + background: #ecf5ff; + color: #409eff; +} +.blush { + background: #fff0f2; + color: #e98191; +} +.amber { + background: #fff5e7; + color: #e9aa4b; +} +.mint { + background: #eaf8f1; + color: #55b987; +} +.stat-value { + font: + 700 25px Manrope, + sans-serif; + letter-spacing: -0.8px; + margin-top: 12px; + color: #2c2940; +} +.stat-value small { + font: + 500 11px 'Noto Sans SC', + sans-serif; + color: #9c99aa; + letter-spacing: 0; + margin-left: 3px; +} +.stat-foot { + display: flex; + align-items: center; + gap: 6px; + color: #aaa8b6; + font-size: 9px; + margin-top: 9px; +} +.positive { + font-size: 9px; + color: #49b786; + background: #eef9f4; + padding: 3px 5px; + border-radius: 4px; +} +.neutral { + color: #aaa8b6; +} +.sparkline { + height: 24px; + display: flex; + align-items: flex-end; + gap: 3px; + margin-left: auto; +} +.sparkline i { + width: 3px; + border-radius: 2px 2px 0 0; + opacity: 0.7; +} +.purple-line i { + background: #a693f5; +} +.pink-line i { + background: #ed9bab; +} +.orange-line i { + background: #f2c17a; +} +.green-line i { + background: #87d4b0; +} +.dashboard-grid { + display: grid; + grid-template-columns: 1.62fr 1fr; + gap: 16px; + margin-bottom: 17px; +} +.panel { + padding: 19px 20px; +} +.panel-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; +} +.panel-heading h2, +.episode-toolbar h2 { + font-size: 13px; + margin: 0; + color: #302d44; + font-weight: 650; +} +.panel-heading p, +.episode-toolbar p { + font-size: 10px; + color: #a09eae; + margin: 5px 0 0; +} +.period-select { + width: 88px; +} +.period-select .el-input__wrapper { + box-shadow: 0 0 0 1px #efedf3 inset !important; +} +.period-select .el-input__inner { + font-size: 10px; +} +.chart-summary { + display: flex; + align-items: baseline; + gap: 8px; + margin-top: 15px; +} +.chart-summary b { + font: + 700 20px Manrope, + sans-serif; + letter-spacing: -0.5px; +} +.chart-summary > span:not(.positive) { + color: #a09eae; + font-size: 10px; +} +.chart { + display: flex; + margin-top: 13px; + height: 164px; +} +.chart-y { + width: 39px; + display: flex; + flex-direction: column; + justify-content: space-between; + color: #b2b0bd; + font-size: 8px; + padding-bottom: 18px; +} +.chart-main { + position: relative; + flex: 1; +} +.chart-grid { + position: absolute; + inset: 0 0 20px; + display: flex; + flex-direction: column; + justify-content: space-between; +} +.chart-grid i { + border-top: 1px dashed #eeedf3; +} +.chart-svg { + position: absolute; + inset: 0 0 20px; + width: 100%; + height: calc(100% - 20px); + overflow: visible; +} +.chart-x { + position: absolute; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: space-between; + color: #b2b0bd; + font-size: 8px; +} +.review-count, +.muted-count { + display: inline-grid; + place-items: center; + border-radius: 10px; + background: #ecf5ff; + color: #409eff; + font: + 600 9px 'DM Sans', + sans-serif; + min-width: 18px; + height: 18px; + margin-left: 4px; + padding: 0 5px; +} +.view-all { + font-size: 10px !important; + color: #409eff !important; +} +.view-all span { + margin-left: 5px; +} +.review-list { + margin-top: 17px; +} +.review-row { + display: flex; + align-items: center; + gap: 9px; + padding: 11px 0; + border-bottom: 1px solid #f3f2f6; +} +.review-row:last-child { + border-bottom: 0; +} +.review-art { + width: 34px; + height: 37px; + border-radius: 6px; + display: grid; + place-items: center; + font-size: 17px; +} +.art-one { + background: #fff0ed; +} +.art-two { + background: #ecf5ff; +} +.art-three { + background: #eaf6f7; +} +.review-copy { + flex: 1; + min-width: 0; +} +.review-copy b, +.review-copy small { + display: block; +} +.review-copy b { + font-size: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.review-copy small { + font-size: 8px; + color: #aaa8b6; + margin-top: 5px; +} +.review-row .el-tag, +.drama-table .el-tag { + border: 0; + font-size: 9px; + height: 21px; +} +.recent-panel { + padding-bottom: 6px; +} +.recent-panel .panel-heading { + margin-bottom: 15px; +} +.table-wrap { + overflow-x: auto; +} +.drama-table { + width: 100%; + border-collapse: collapse; + text-align: left; + white-space: nowrap; +} +.drama-table th { + font-size: 9px; + font-weight: 500; + color: #aaa8b6; + background: #fafafd; + padding: 10px 11px; +} +.drama-table th:first-child { + border-radius: 5px 0 0 5px; + padding-left: 12px; +} +.drama-table th:last-child { + border-radius: 0 5px 5px 0; +} +.drama-table td { + font-size: 10px; + color: #767487; + padding: 10px 11px; + border-bottom: 1px solid #f3f2f6; +} +.drama-table tr:last-child td { + border-bottom: 0; +} +.drama-cell { + display: flex; + align-items: center; + gap: 9px; +} +.drama-cell b { + font-size: 10px; + font-weight: 600; + color: #4a475e; +} +.drama-cover { + width: 31px; + height: 38px; + border-radius: 4px; + display: grid; + place-items: center; + font-size: 16px; + flex: none; + box-shadow: inset 0 0 0 1px #00000008; +} +.rose { + background: linear-gradient(145deg, #f8d6d7, #ffece4); +} +.purple { + background: linear-gradient(145deg, #d9ecff, #ecf5ff); +} +.peach { + background: linear-gradient(145deg, #ffd6bd, #fff0d7); +} +.blue { + background: linear-gradient(145deg, #d6e7ff, #e4f5fa); +} +.gold { + background: linear-gradient(145deg, #f5e3b3, #fff0dc); +} +.row-action { + font-size: 9px !important; + color: #409eff !important; +} +.inner-heading { + margin-bottom: 24px; +} +.library-panel { + padding: 0 20px 12px; +} +.library-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #f0eff4; + margin-bottom: 0; +} +.library-toolbar .el-tabs__header, +.cover-toolbar .el-tabs__header { + margin: 0; +} +.library-toolbar .el-tabs__nav-wrap:after, +.cover-toolbar .el-tabs__nav-wrap:after { + display: none; +} +.library-toolbar .el-tabs__item, +.cover-toolbar .el-tabs__item { + font-size: 11px; + color: #9997a7; + height: 46px; +} +.library-toolbar .el-tabs__item.is-active, +.cover-toolbar .el-tabs__item.is-active { + color: #409eff; +} +.library-toolbar .el-tabs__active-bar, +.cover-toolbar .el-tabs__active-bar { + background: #409eff; +} +.toolbar-filters { + display: flex; + gap: 9px; +} +.toolbar-filters .el-input { + width: 190px; +} +.toolbar-filters .el-select { + width: 120px; +} +.toolbar-filters .el-input__wrapper, +.toolbar-filters .el-select__wrapper { + box-shadow: 0 0 0 1px #efedf3 inset !important; +} +.toolbar-filters .el-input__inner, +.toolbar-filters .el-select__placeholder { + font-size: 10px; +} +.full-table th { + padding-top: 13px; + padding-bottom: 13px; +} +.full-table td { + padding-top: 13px; + padding-bottom: 13px; +} +.full-table .drama-cell small { + display: block; + font-size: 9px; + color: #aaa8b6; + margin-top: 4px; + max-width: 235px; + overflow: hidden; + text-overflow: ellipsis; +} +.pagination { + display: flex; + align-items: center; + justify-content: space-between; + color: #aaa8b6; + font-size: 10px; + padding: 15px 2px 3px; +} +.pagination .el-pagination { + --el-pagination-button-bg-color: #fff; + --el-pagination-hover-color: #409eff; +} +.pagination .el-pager li { + font-size: 10px; +} +.empty-cell { + text-align: center !important; + padding: 45px !important; + color: #aaa !important; +} +.episode-panel { + padding: 0 23px; +} +.episode-context { + display: flex; + align-items: center; + padding: 20px 0; + border-bottom: 1px solid #f0eff4; +} +.large-cover { + width: 48px; + height: 58px; + font-size: 22px; + border-radius: 6px; +} +.episode-context-copy { + margin-left: 12px; + display: flex; + flex-direction: column; + gap: 4px; +} +.episode-context-copy > span { + font-size: 9px; + color: #a7a5b3; +} +.episode-context-copy b { + font-size: 13px; +} +.episode-context-copy small { + font-size: 9px; + color: #aaa8b6; +} +.drama-select { + margin-left: auto; + width: 215px; +} +.drama-select .el-input__wrapper { + box-shadow: 0 0 0 1px #efedf3 inset !important; +} +.drama-select .el-input__inner { + font-size: 10px; +} +.episode-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 0 14px; +} +.secondary-button { + border-radius: 6px !important; + border-color: #eae8f0 !important; + color: #6e6c80 !important; + font-size: 10px !important; + height: 31px !important; +} +.secondary-button .el-icon { + margin-right: 5px; +} +.episode-row { + height: 69px; + border: 1px solid #efedf4; + border-radius: 7px; + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + margin-bottom: 8px; +} +.drag-handle { + color: #c2c0cb; + font-size: 17px; + cursor: grab; +} +.episode-thumb { + width: 62px; + height: 45px; + border-radius: 5px; + background: linear-gradient(135deg, #29273b, #55516e); + display: grid; + place-items: center; + color: #fff; + font-size: 15px; +} +.episode-title { + flex: 1; +} +.episode-title b, +.episode-title small { + display: block; +} +.episode-title b { + font-size: 10px; + color: #49465a; +} +.episode-title small { + font-size: 9px; + color: #aaa8b6; + margin-top: 6px; +} +.episode-title small .el-icon { + vertical-align: middle; +} +.episode-row .el-tag { + font-size: 9px; + border: 0; +} +.episode-actions { + display: flex; + margin-left: 8px; +} +.episode-actions .el-button { + padding: 5px; + color: #9997a7; +} +.episode-actions .danger-action:hover { + color: #e77680; +} +.episode-footer { + display: flex; + justify-content: space-between; + align-items: center; + color: #aaa8b6; + font-size: 9px; + padding: 12px 1px; +} +.cover-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 17px; + border-bottom: 1px solid #eeedf3; +} +.cover-grid { + display: grid; + grid-template-columns: repeat(5, minmax(130px, 1fr)); + gap: 15px; +} +.cover-card, +.add-cover { + background: white; + border: 1px solid #eeedf3; + border-radius: 9px; + overflow: hidden; +} +.poster-art { + height: 235px; + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: 17px; + overflow: hidden; +} +.poster-art.rose { + background: linear-gradient(155deg, #e8bfc5, #8b455e 65%, #36273c); +} +.poster-art.purple { + background: linear-gradient(155deg, #beb3e8, #584879 65%, #28263c); +} +.poster-art.peach { + background: linear-gradient(155deg, #f4c9a8, #cb7181 65%, #553b56); +} +.poster-art.blue { + background: linear-gradient(155deg, #9bc2d7, #426b85 65%, #292c4e); +} +.poster-art.gold { + background: linear-gradient(155deg, #efd8a6, #b8754d 65%, #48334b); +} +.poster-glow { + position: absolute; + width: 150px; + height: 150px; + border-radius: 50%; + background: #ffffff30; + filter: blur(25px); + top: 30px; + left: 30px; +} +.poster-emoji { + position: absolute; + top: 45px; + left: 50%; + transform: translateX(-50%); + font-size: 68px; + filter: drop-shadow(0 6px 6px #0004); +} +.poster-copy { + position: relative; + color: white; + text-shadow: 0 1px 5px #0008; + z-index: 1; +} +.poster-copy small { + display: block; + font: 600 7px 'DM Sans'; + letter-spacing: 1.6px; + opacity: 0.8; + margin-bottom: 8px; +} +.poster-copy b { + display: block; + font-size: 15px; + line-height: 1.5; +} +.poster-copy span { + display: block; + font-size: 8px; + margin-top: 5px; + opacity: 0.8; +} +.poster-stamp { + position: absolute; + right: 10px; + top: 12px; + color: white; + border: 1px solid #ffffff8a; + padding: 4px 6px; + font-size: 8px; + border-radius: 3px; +} +.cover-card-info { + padding: 10px; + display: flex; + align-items: center; + justify-content: space-between; +} +.cover-card-info b, +.cover-card-info small { + display: block; + max-width: 140px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.cover-card-info b { + font-size: 9px; +} +.cover-card-info small { + font-size: 8px; + color: #aaa8b6; + margin-top: 4px; +} +.add-cover { + min-height: 276px; + border-style: dashed; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #858397; + cursor: pointer; +} +.add-cover > span { + width: 36px; + height: 36px; + border-radius: 50%; + display: grid; + place-items: center; + background: #ecf5ff; + color: #409eff; + font-size: 17px; + margin-bottom: 11px; +} +.add-cover b { + font-size: 10px; + font-weight: 600; +} +.add-cover small { + font-size: 8px; + color: #aaa8b6; + margin-top: 6px; +} +.access-layout { + display: grid; + grid-template-columns: 1fr 1.48fr; + gap: 17px; +} +.roles-panel, +.permissions-panel { + padding: 19px; +} +.role-list { + margin-top: 14px; + display: flex; + flex-direction: column; + gap: 8px; +} +.role-card { + width: 100%; + border: 1px solid #efedf4; + background: #fff; + border-radius: 8px; + display: flex; + align-items: center; + text-align: left; + padding: 11px; + gap: 10px; + cursor: pointer; + font: inherit; +} +.role-card:hover { + border-color: #c6e2ff; +} +.role-active { + border-color: #a0cfff; + background: #f5faff; +} +.role-icon { + width: 34px; + height: 34px; + display: grid; + place-items: center; + border-radius: 8px; + font-size: 15px; +} +.role-icon.red { + background: #fff0ef; + color: #e47b79; +} +.role-icon.purple { + background: #ecf5ff; + color: #409eff; +} +.role-icon.blue { + background: #edf5ff; + color: #409eff; +} +.role-icon.green { + background: #eaf8f0; + color: #50ae80; +} +.role-copy { + flex: 1; +} +.role-copy b, +.role-copy small, +.role-copy span { + display: block; +} +.role-copy b { + font-size: 10px; + color: #47445a; +} +.role-copy small { + font-size: 8px; + color: #a09eae; + margin-top: 4px; +} +.role-copy span { + font-size: 8px; + color: #aaa8b6; + margin-top: 6px; +} +.role-chevron { + font-size: 10px; + color: #aaa8b6; + transform: rotate(-90deg); +} +.role-badge { + font-size: 8px; + color: #409eff; + background: #ecf5ff; + border-radius: 10px; + padding: 4px 7px; + vertical-align: 2px; + margin-left: 5px; + font-weight: 500; +} +.permission-groups { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin: 23px 0 18px; +} +.permission-group { + display: flex; + flex-direction: column; + gap: 14px; +} +.permission-group h3 { + font-size: 10px; + margin: 0 0 2px; + color: #5b586d; +} +.permission-group h3 .el-icon { + font-size: 12px; + vertical-align: middle; + margin-right: 5px; + color: #409eff; +} +.permission-group .el-checkbox { + height: auto; + margin: 0; +} +.permission-group .el-checkbox__label { + font-size: 9px; + color: #858397; +} +.permission-group .el-checkbox__input.is-checked .el-checkbox__inner { + background: #409eff; + border-color: #409eff; +} +.permission-note { + padding: 10px; + background: #f8f7fc; + border-radius: 6px; + font-size: 8px; + color: #9997a8; + display: flex; + align-items: center; + gap: 7px; +} +.permission-note .el-icon { + color: #79bbff; +} +.placeholder-panel { + text-align: center; + padding: 70px 20px; +} +.placeholder-icon { + display: grid; + place-items: center; + width: 45px; + height: 45px; + border-radius: 12px; + margin: auto; + background: #ecf5ff; + color: #409eff; + font-size: 20px; +} +.placeholder-panel h2 { + font-size: 14px; + margin: 14px 0 5px; +} +.placeholder-panel p { + font-size: 10px; + color: #a09eae; +} +.el-dialog { + border-radius: 11px !important; +} +.el-dialog__header { + padding: 20px 22px 12px !important; + margin-right: 0 !important; + border-bottom: 1px solid #f0eff4; +} +.el-dialog__title { + font-size: 14px; + font-weight: 650; +} +.el-dialog__body { + padding: 15px 22px 5px !important; +} +.el-dialog__footer { + padding: 13px 22px 19px !important; + border-top: 1px solid #f2f1f5; +} +.dialog-subtitle { + font-size: 10px; + color: #9997a8; + margin-bottom: 16px; +} +.drama-form .form-row, +.app-dialog .form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} +.app-dialog .el-form-item { + margin-bottom: 14px; +} +.app-dialog .el-form-item__label { + font-size: 10px; + color: #666377; + padding-bottom: 5px; +} +.app-dialog .el-input__inner, +.app-dialog .el-textarea__inner, +.app-dialog .el-select__placeholder { + font-size: 10px; +} +.upload-area { + width: 100%; +} +.upload-area .el-upload, +.upload-area .el-upload-dragger { + width: 100%; +} +.upload-area .el-upload-dragger { + height: 112px; + padding: 14px; + border-radius: 7px; + border-color: #e8e5ef; + background: #fcfbfe; +} +.upload-icon { + font-size: 20px; + color: #409eff; + margin-bottom: 6px; +} +.upload-area .el-upload__text { + font-size: 10px; + color: #777589; +} +.upload-area .el-upload__text em { + font-style: normal; + color: #409eff; +} +.upload-hint { + font-size: 8px; + color: #aaa8b6; + margin-top: 5px; +} +.app-dialog .el-input-number { + width: 100%; +} +.app-dialog .el-input-number .el-input__wrapper { + width: 100%; +} +@media (min-width: 1500px) { + .page-content { + padding-left: 48px; + padding-right: 48px; + } + .stats-grid { + gap: 18px; + } + .stat-card { + min-height: 141px; + padding: 18px 19px; + } + .cover-grid { + grid-template-columns: repeat(6, minmax(130px, 1fr)); + } +} +@media (max-width: 1200px) { + .sidebar { + width: 220px; + } + .main-area { + margin-left: 220px; + } + .page-content { + padding: 30px 26px; + } + .topbar { + padding: 0 26px; + } + .stats-grid { + gap: 10px; + } + .stat-card { + padding: 14px; + } + .dashboard-grid { + grid-template-columns: 1.35fr 1fr; + gap: 12px; + } + .cover-grid { + grid-template-columns: repeat(4, minmax(130px, 1fr)); + } +} diff --git a/src/styles/business/theme.css b/src/styles/business/theme.css new file mode 100644 index 0000000..96439a3 --- /dev/null +++ b/src/styles/business/theme.css @@ -0,0 +1,395 @@ +:root { + font-family: Arial, 'Noto Sans SC', sans-serif; + color: var(--el-text-color-primary); + background: var(--el-bg-color-page); + --el-color-primary: #409eff; + --el-color-primary-light-3: #79bbff; + --el-color-primary-light-5: #a0cfff; + --el-color-primary-light-7: #c6e2ff; + --el-color-primary-light-8: #d9ecff; + --el-color-primary-light-9: #ecf5ff; + --el-color-primary-dark-2: #337ecc; + --purple: var(--el-color-primary); + --muted: #909399; + --line: #e4e7ed; +} +body { + background: var(--el-bg-color-page); + color: var(--el-text-color-primary); +} +.sidebar { + background: #fff; + border-right-color: #e7e9ec; + padding-top: 22px; +} +.brand { + padding-bottom: 23px; +} +.brand-mark { + background: #409eff; + border-radius: 7px; + transform: none; +} +.brand b { + font: + 700 16px Arial, + sans-serif; + letter-spacing: -0.4px; + color: #29313b; +} +.brand small { + letter-spacing: 0.9px; +} +.workspace { + border-radius: 5px; + border-color: #e5e7eb; + margin-bottom: 22px; +} +.workspace-avatar, +.profile-avatar, +.top-avatar { + background: #ecf5ff; + color: #409eff; + border-radius: 5px; +} +.nav-label { + color: #9298a1; + font-size: 10px; +} +.side-menu .el-menu-item { + border-radius: 5px; + color: #616873; +} +.side-menu .el-menu-item:hover { + background: #ecf5ff; + color: #409eff; +} +.side-menu .el-menu-item.is-active { + background: #ecf5ff; + color: #409eff; +} +.menu-count { + background: #eef1f4; + color: #747d88; +} +.help-card { + background: #f7f8fa; + border-color: #eceef0; + border-radius: 5px; +} +.help-icon { + background: #ecf5ff; + color: #409eff; + border-radius: 5px; +} +.help-card a { + color: #409eff; +} +.topbar { + background: #fff; +} +.environment { + border-radius: 4px; +} +.page-content { + padding-top: 30px; +} +.eyebrow { + letter-spacing: 0.7px; + color: #9298a1; +} +.page-heading h1 { + font: + 600 23px Arial, + 'Noto Sans SC', + sans-serif; + letter-spacing: 0; + color: #2f343c; +} +.page-heading p { + color: #818791; +} +.primary-button { + background: #409eff !important; + border-color: #409eff !important; + border-radius: 5px !important; + box-shadow: none !important; +} +.primary-button:hover { + background: #79bbff !important; + border-color: #79bbff !important; +} +.stats-grid { + gap: 12px; +} +.stat-card, +.panel { + border-color: #e6e8eb; + border-radius: 6px; + box-shadow: none; +} +.stat-card { + min-height: 116px; + padding: 14px 16px; +} +.stat-top { + color: #727983; +} +.stat-icon { + border-radius: 5px; + background: #ecf5ff !important; + color: #409eff !important; +} +.stat-value { + font-family: Arial, sans-serif; + color: #303640; +} +.stat-foot .positive { + color: #4d866a; + background: #eff5f1; +} +.sparkline { + opacity: 0.42; +} +.positive { + color: #4d866a; + background: #eff5f1; +} +.review-count, +.muted-count { + background: #eef1f4; + color: #61738a; +} +.chart-svg path:first-of-type { + fill: #d9ecff; +} +.chart-svg path:last-of-type { + stroke: #409eff; +} +.view-all, +.row-action { + color: #409eff !important; +} +.review-art { + background: #f1f3f5 !important; + border-radius: 5px; + font-size: 0; +} +.review-art:after { + content: '—'; + font-size: 14px; + color: #818a95; +} +.review-row .el-tag, +.drama-table .el-tag, +.episode-row .el-tag { + border-radius: 4px; + background: #f0f2f4 !important; + color: #606b78 !important; +} +.drama-cover { + border-radius: 4px; + box-shadow: none; + font-size: 14px; + background: #eceff2 !important; +} +.drama-cover.rose, +.drama-cover.purple, +.drama-cover.peach, +.drama-cover.blue, +.drama-cover.gold { + background: #eceff2 !important; +} +.drama-table th { + background: #f7f8fa; + color: #7d848e; +} +.drama-table td { + color: #656c76; +} +.drama-cell b { + color: #343a43; +} +.library-panel { + padding-left: 18px; + padding-right: 18px; +} +.library-toolbar .el-tabs__item, +.cover-toolbar .el-tabs__item { + font-size: 11px; +} +.library-toolbar .el-tabs__item.is-active, +.cover-toolbar .el-tabs__item.is-active { + color: #409eff; +} +.library-toolbar .el-tabs__active-bar, +.cover-toolbar .el-tabs__active-bar { + background: #409eff; +} +.toolbar-filters .el-input__wrapper, +.toolbar-filters .el-select__wrapper, +.drama-select .el-input__wrapper { + border-radius: 5px; +} +.episode-panel { + padding-left: 20px; + padding-right: 20px; +} +.episode-row { + border-color: #e7e9ec; + border-radius: 5px; +} +.episode-thumb { + border-radius: 4px; + background: #e9ecef; + color: #7d8792; +} +.episode-thumb .el-icon { + color: #606a75; +} +.episode-actions .el-button { + color: #767e88; +} +.cover-grid { + grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); +} +.cover-card, +.add-cover { + border-color: #e6e8eb; + border-radius: 5px; +} +.poster-art { + background: #e9ecef !important; + filter: grayscale(0.72); +} +.poster-art.rose, +.poster-art.purple, +.poster-art.peach, +.poster-art.blue, +.poster-art.gold { + background: #e9ecef !important; +} +.poster-glow, +.poster-stamp { + display: none; +} +.poster-emoji { + font-size: 48px; + filter: grayscale(1); + opacity: 0.55; +} +.poster-copy { + text-shadow: none; + color: #303640; +} +.poster-copy small, +.poster-copy span { + color: #606873; + opacity: 1; +} +.cover-card-info b { + color: #383e46; +} +.add-cover { + min-height: 276px; + background: #fafbfc; + color: #656d77; +} +.add-cover > span { + border-radius: 5px; + background: #edf0f3; + color: #68798d; +} +.role-card { + border-radius: 5px; + border-color: #e7e9ec; +} +.role-active { + border-color: #a0cfff; + background: #ecf5ff; +} +.role-icon { + border-radius: 5px; + background: #ecf5ff !important; + color: #409eff !important; +} +.role-badge { + border-radius: 4px; + background: #eef1f4; + color: #65758a; +} +.permission-group h3 .el-icon { + color: #409eff; +} +.permission-group .el-checkbox__input.is-checked .el-checkbox__inner { + background: #409eff; + border-color: #409eff; +} +.permission-note { + background: #f6f7f8; + border-radius: 4px; +} +.el-dialog { + border-radius: 6px !important; +} +.el-dialog__header, +.el-dialog__footer { + border-color: #e8eaed; +} +.upload-area .el-upload-dragger { + border-radius: 5px; + background: #fafbfc; +} +.upload-icon, +.upload-area .el-upload__text em { + color: #409eff; +} +.el-tabs__item, +.el-input__inner, +.el-select__placeholder { + font-family: Arial, 'Noto Sans SC', sans-serif; +} +.el-tag.status-badge { + border-radius: 4px; + font-weight: 500; +} +.el-tag.status-badge.is-online { + --el-tag-bg-color: #e8f8ef; + --el-tag-border-color: #b7ebc9; + --el-tag-text-color: #178a4a; + background: #e8f8ef !important; + border-color: #b7ebc9 !important; + color: #178a4a !important; +} +.el-tag.status-badge.is-reviewing { + --el-tag-bg-color: #fff4e5; + --el-tag-border-color: #f8d7a8; + --el-tag-text-color: #d97706; + background: #fff4e5 !important; + border-color: #f8d7a8 !important; + color: #d97706 !important; +} +.el-tag.status-badge.is-draft { + --el-tag-bg-color: #f2f3f5; + --el-tag-border-color: #e0e2e6; + --el-tag-text-color: #6b7280; + background: #f2f3f5 !important; + border-color: #e0e2e6 !important; + color: #6b7280 !important; +} +.el-tag.status-badge.is-pending { + --el-tag-bg-color: #ecf5ff; + --el-tag-border-color: #c6e2ff; + --el-tag-text-color: #2b7de9; + background: #ecf5ff !important; + border-color: #c6e2ff !important; + color: #2b7de9 !important; +} +.el-tag.status-badge.is-offline { + --el-tag-bg-color: #fdecec; + --el-tag-border-color: #f8c9c9; + --el-tag-text-color: #d45656; + background: #fdecec !important; + border-color: #f8c9c9 !important; + color: #d45656 !important; +} diff --git a/src/styles/business/typography.css b/src/styles/business/typography.css new file mode 100644 index 0000000..04e9a71 --- /dev/null +++ b/src/styles/business/typography.css @@ -0,0 +1,11 @@ +.stat-top > .stat-icon { + width: 30px; + height: 30px; + flex: 0 0 30px; + display: grid; + place-items: center; + border-radius: 5px; +} +.stat-top > .stat-icon .el-icon { + font-size: 16px; +} diff --git a/src/styles/index.less b/src/styles/index.less new file mode 100644 index 0000000..6825d70 --- /dev/null +++ b/src/styles/index.less @@ -0,0 +1,7 @@ +@import './var.css'; +@import 'element-plus/theme-chalk/dark/css-vars.css'; + +// 解决抽屉弹出时,body宽度变化的问题 +.el-popup-parent--hidden { + width: 100% !important; +} diff --git a/src/styles/var.css b/src/styles/var.css new file mode 100644 index 0000000..15e206e --- /dev/null +++ b/src/styles/var.css @@ -0,0 +1,61 @@ +:root { + --login-bg-color: #293146; + + /* left menu start */ + --left-menu-max-width: 200px; + + --left-menu-min-width: 64px; + + --left-menu-bg-color: #001529; + + --left-menu-bg-light-color: #0f2438; + + --left-menu-bg-active-color: var(--el-color-primary); + + --left-menu-text-color: #bfcbd9; + + --left-menu-text-active-color: #fff; + + --left-menu-collapse-bg-active-color: var(--el-color-primary); + /* left menu end */ + + /* logo start */ + --logo-height: 50px; + + --logo-title-text-color: #fff; + /* logo end */ + + /* header start */ + --top-header-bg-color: '#fff'; + + --top-header-text-color: 'inherit'; + + --top-header-hover-color: #f6f6f6; + + --top-tool-height: var(--logo-height); + + --top-tool-p-x: 0; + + --tags-view-height: 35px; + /* header start */ + + /* tab menu start */ + --tab-menu-max-width: 80px; + + --tab-menu-min-width: 30px; + + --tab-menu-collapse-height: 36px; + /* tab menu end */ + + --app-content-padding: 20px; + + --app-content-bg-color: #f5f7f9; + + --app-footer-height: 0px; + + --transition-time-02: 0.2s; +} + +.dark { + --app-content-bg-color: var(--el-bg-color); +} diff --git a/src/styles/variables.module.less b/src/styles/variables.module.less new file mode 100644 index 0000000..e97028d --- /dev/null +++ b/src/styles/variables.module.less @@ -0,0 +1,10 @@ +// 命名空间 +@namespace: v; +// el命名空间 +@elNamespace: el; + +// 导出变量 +:export { + namespace: @namespace; + elNamespace: @elNamespace; +} diff --git a/src/theme.css b/src/theme.css deleted file mode 100644 index 1edb604..0000000 --- a/src/theme.css +++ /dev/null @@ -1,231 +0,0 @@ -:root { - font-family:Arial,'Noto Sans SC',sans-serif; - color:#30343b; - background:#f5f6f8; - --purple:#4b76b8; - --muted:#858b95; - --line:#e8eaed} -body { - background:#f5f6f8} -.sidebar { - background:#fff; - border-right-color:#e7e9ec; - padding-top:22px} -.brand { - padding-bottom:23px} -.brand-mark { - background:#456fae; - border-radius:7px; - transform:none} -.brand b { - font:700 16px Arial,sans-serif; - letter-spacing:-.4px; - color:#29313b} -.brand small { - letter-spacing:.9px} -.workspace { - border-radius:5px; - border-color:#e5e7eb; - margin-bottom:22px} -.workspace-avatar,.profile-avatar,.top-avatar { - background:#edf1f6; - color:#4b6689; - border-radius:5px} -.nav-label { - color:#9298a1; - font-size:10px} -.side-menu .el-menu-item { - border-radius:5px; - color:#616873} -.side-menu .el-menu-item:hover { - background:#f5f7f9; - color:#355f97} -.side-menu .el-menu-item.is-active { - background:#edf2f8; - color:#355f97} -.menu-count { - background:#eef1f4; - color:#747d88} -.help-card { - background:#f7f8fa; - border-color:#eceef0; - border-radius:5px} -.help-icon { - background:#e9edf2; - color:#617b9c; - border-radius:5px} -.help-card a { - color:#416a9f} -.topbar { - background:#fff} -.environment { - border-radius:4px} -.page-content { - padding-top:30px} -.eyebrow { - letter-spacing:.7px; - color:#9298a1} -.page-heading h1 { - font:600 23px Arial,'Noto Sans SC',sans-serif; - letter-spacing:0; - color:#2f343c} -.page-heading p { - color:#818791} -.primary-button { - background:#456fae!important; - border-color:#456fae!important; - border-radius:5px!important; - box-shadow:none!important} -.primary-button:hover { - background:#385f97!important} -.stats-grid { - gap:12px} -.stat-card,.panel { - border-color:#e6e8eb; - border-radius:6px; - box-shadow:none} -.stat-card { - min-height:116px; - padding:14px 16px} -.stat-top { - color:#727983} -.stat-icon { - border-radius:5px; - background:#f0f2f4!important; - color:#697584!important} -.stat-value { - font-family:Arial,sans-serif; - color:#303640} -.stat-foot .positive { - color:#4d866a; - background:#eff5f1} -.sparkline { - opacity:.42} -.positive { - color:#4d866a; - background:#eff5f1} -.review-count,.muted-count { - background:#eef1f4; - color:#61738a} -.chart-svg path:first-of-type { - fill:#dce5f0} -.chart-svg path:last-of-type { - stroke:#587eae} -.view-all,.row-action { - color:#456fae!important} -.review-art { - background:#f1f3f5!important; - border-radius:5px; - font-size:0} -.review-art:after { - content:'—'; - font-size:14px; - color:#818a95} -.review-row .el-tag,.drama-table .el-tag,.episode-row .el-tag { - border-radius:4px; - background:#f0f2f4!important; - color:#606b78!important} -.drama-cover { - border-radius:4px; - box-shadow:none; - font-size:14px; - background:#eceff2!important} -.drama-cover.rose,.drama-cover.purple,.drama-cover.peach,.drama-cover.blue,.drama-cover.gold { - background:#eceff2!important} -.drama-table th { - background:#f7f8fa; - color:#7d848e} -.drama-table td { - color:#656c76} -.drama-cell b { - color:#343a43} -.library-panel { - padding-left:18px; - padding-right:18px} -.library-toolbar .el-tabs__item,.cover-toolbar .el-tabs__item { - font-size:11px} -.library-toolbar .el-tabs__item.is-active,.cover-toolbar .el-tabs__item.is-active { - color:#456fae} -.library-toolbar .el-tabs__active-bar,.cover-toolbar .el-tabs__active-bar { - background:#456fae} -.toolbar-filters .el-input__wrapper,.toolbar-filters .el-select__wrapper,.drama-select .el-input__wrapper { - border-radius:5px} -.episode-panel { - padding-left:20px; - padding-right:20px} -.episode-row { - border-color:#e7e9ec; - border-radius:5px} -.episode-thumb { - border-radius:4px; - background:#e9ecef; - color:#7d8792} -.episode-thumb .el-icon { - color:#606a75} -.episode-actions .el-button { - color:#767e88} -.cover-grid { - grid-template-columns:repeat(auto-fill,minmax(145px,1fr))} -.cover-card,.add-cover { - border-color:#e6e8eb; - border-radius:5px} -.poster-art { - background:#e9ecef!important; - filter:grayscale(.72)} -.poster-art.rose,.poster-art.purple,.poster-art.peach,.poster-art.blue,.poster-art.gold { - background:#e9ecef!important} -.poster-glow,.poster-stamp { - display:none} -.poster-emoji { - font-size:48px; - filter:grayscale(1); - opacity:.55} -.poster-copy { - text-shadow:none; - color:#303640} -.poster-copy small,.poster-copy span { - color:#606873; - opacity:1} -.cover-card-info b { - color:#383e46} -.add-cover { - min-height:276px; - background:#fafbfc; - color:#656d77} -.add-cover>span { - border-radius:5px; - background:#edf0f3; - color:#68798d} -.role-card { - border-radius:5px; - border-color:#e7e9ec} -.role-active { - border-color:#aebed1; - background:#f7f9fb} -.role-icon { - border-radius:5px; - background:#eef1f4!important; - color:#65758a!important} -.role-badge { - border-radius:4px; - background:#eef1f4; - color:#65758a} -.permission-group h3 .el-icon { - color:#617b9b} -.permission-group .el-checkbox__input.is-checked .el-checkbox__inner { - background:#456fae; - border-color:#456fae} -.permission-note { - background:#f6f7f8; - border-radius:4px} -.el-dialog { - border-radius:6px!important} -.el-dialog__header,.el-dialog__footer { - border-color:#e8eaed} -.upload-area .el-upload-dragger { - border-radius:5px; - background:#fafbfc} -.upload-icon,.upload-area .el-upload__text em { - color:#456fae} -.el-tabs__item,.el-input__inner,.el-select__placeholder { - font-family:Arial,'Noto Sans SC',sans-serif} diff --git a/src/typography.css b/src/typography.css deleted file mode 100644 index 14c5f96..0000000 --- a/src/typography.css +++ /dev/null @@ -1,9 +0,0 @@ -.stat-top>.stat-icon { - width:30px; - height:30px; - flex:0 0 30px; - display:grid; - place-items:center; - border-radius:5px} -.stat-top>.stat-icon .el-icon { - font-size:16px} diff --git a/src/utils/color.ts b/src/utils/color.ts new file mode 100644 index 0000000..387ffc6 --- /dev/null +++ b/src/utils/color.ts @@ -0,0 +1,172 @@ +/** + * 判断是否 十六进制颜色值. + * 输入形式可为 #fff000 #f00 + * + * @param String color 十六进制颜色值 + * @return Boolean + */ +export const isHexColor = (color: string) => { + const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/ + return reg.test(color) +} + +/** + * RGB 颜色值转换为 十六进制颜色值. + * r, g, 和 b 需要在 [0, 255] 范围内 + * + * @return String 类似#ff00ff + * @param r + * @param g + * @param b + */ +export const rgbToHex = (r: number, g: number, b: number) => { + // tslint:disable-next-line:no-bitwise + const hex = ((r << 16) | (g << 8) | b).toString(16) + return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex +} + +/** + * Transform a HEX color to its RGB representation + * @param {string} hex The color to transform + * @returns The RGB representation of the passed color + */ +export const hexToRGB = (hex: string, opacity?: number) => { + let sHex = hex.toLowerCase() + if (isHexColor(hex)) { + if (sHex.length === 4) { + let sColorNew = '#' + for (let i = 1; i < 4; i += 1) { + sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1)) + } + sHex = sColorNew + } + const sColorChange: number[] = [] + for (let i = 1; i < 7; i += 2) { + sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2))) + } + return opacity + ? 'RGBA(' + sColorChange.join(',') + ',' + opacity + ')' + : 'RGB(' + sColorChange.join(',') + ')' + } + return sHex +} + +export const colorIsDark = (color: string) => { + if (!isHexColor(color)) return + const [r, g, b] = hexToRGB(color) + .replace(/(?:\(|\)|rgb|RGB)*/g, '') + .split(',') + .map((item) => Number(item)) + return r * 0.299 + g * 0.578 + b * 0.114 < 192 +} + +/** + * Darkens a HEX color given the passed percentage + * @param {string} color The color to process + * @param {number} amount The amount to change the color by + * @returns {string} The HEX representation of the processed color + */ +export const darken = (color: string, amount: number) => { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color + amount = Math.trunc((255 * amount) / 100) + return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight( + color.substring(2, 4), + amount + )}${subtractLight(color.substring(4, 6), amount)}` +} + +/** + * Lightens a 6 char HEX color according to the passed percentage + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed color represented as HEX + */ +export const lighten = (color: string, amount: number) => { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color + amount = Math.trunc((255 * amount) / 100) + return `#${addLight(color.substring(0, 2), amount)}${addLight( + color.substring(2, 4), + amount + )}${addLight(color.substring(4, 6), amount)}` +} + +/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */ +/** + * Sums the passed percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +const addLight = (color: string, amount: number) => { + const cc = parseInt(color, 16) + amount + const c = cc > 255 ? 255 : cc + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` +} + +/** + * Calculates luminance of an rgb color + * @param {number} r red + * @param {number} g green + * @param {number} b blue + */ +const luminanace = (r: number, g: number, b: number) => { + const a = [r, g, b].map((v) => { + v /= 255 + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) + }) + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722 +} + +/** + * Calculates contrast between two rgb colors + * @param {string} rgb1 rgb color 1 + * @param {string} rgb2 rgb color 2 + */ +const contrast = (rgb1: string[], rgb2: number[]) => { + return ( + (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) / + (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05) + ) +} + +/** + * Determines what the best text color is (black or white) based con the contrast with the background + * @param hexColor - Last selected color by the user + */ +export const calculateBestTextColor = (hexColor: string) => { + const rgbColor = hexToRGB(hexColor.substring(1)) + const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]) + + return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF' +} + +/** + * Subtracts the indicated percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +const subtractLight = (color: string, amount: number) => { + const cc = parseInt(color, 16) - amount + const c = cc < 0 ? 0 : cc + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` +} + +/** + * Mixes two colors. + * + * @param {string} color1 - The first color, should be a 6-digit hexadecimal color code starting with `#`. + * @param {string} color2 - The second color, should be a 6-digit hexadecimal color code starting with `#`. + * @param {number} [weight=0.5] - The weight of color1 in the mix, should be a number between 0 and 1, where 0 represents 100% of color2, and 1 represents 100% of color1. + * @returns {string} The mixed color, a 6-digit hexadecimal color code starting with `#`. + */ +export const mix = (color1: string, color2: string, weight: number = 0.5): string => { + let color = '#' + for (let i = 0; i <= 2; i++) { + const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16) + const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16) + const c = Math.round(c1 * weight + c2 * (1 - weight)) + color += c.toString(16).padStart(2, '0') + } + return color +} diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts new file mode 100644 index 0000000..36b87e3 --- /dev/null +++ b/src/utils/dateUtil.ts @@ -0,0 +1,17 @@ +/** + * Independent time operation tool to facilitate subsequent switch to dayjs + */ +import dayjs from 'dayjs' + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' +const DATE_FORMAT = 'YYYY-MM-DD' + +export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string { + return dayjs(date).format(format) +} + +export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string { + return dayjs(date).format(format) +} + +export const dateUtil = dayjs diff --git a/src/utils/domUtils.ts b/src/utils/domUtils.ts new file mode 100644 index 0000000..dbc1989 --- /dev/null +++ b/src/utils/domUtils.ts @@ -0,0 +1,289 @@ +import { isServer } from './is' +const ieVersion = isServer ? 0 : Number((document as any).documentMode) +const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g +const MOZ_HACK_REGEXP = /^moz([A-Z])/ + +export interface ViewportOffsetResult { + left: number + top: number + right: number + bottom: number + rightIncludeBody: number + bottomIncludeBody: number +} + +/* istanbul ignore next */ +const trim = function (string: string) { + return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '') +} + +/* istanbul ignore next */ +const camelCase = function (name: string) { + return name + .replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) { + return offset ? letter.toUpperCase() : letter + }) + .replace(MOZ_HACK_REGEXP, 'Moz$1') +} + +/* istanbul ignore next */ +export function hasClass(el: Element, cls: string) { + if (!el || !cls) return false + if (cls.indexOf(' ') !== -1) { + throw new Error('className should not contain space.') + } + if (el.classList) { + return el.classList.contains(cls) + } else { + return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1 + } +} + +/* istanbul ignore next */ +export function addClass(el: Element, cls: string) { + if (!el) return + let curClass = el.className + const classes = (cls || '').split(' ') + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i] + if (!clsName) continue + + if (el.classList) { + el.classList.add(clsName) + } else if (!hasClass(el, clsName)) { + curClass += ' ' + clsName + } + } + if (!el.classList) { + el.className = curClass + } +} + +/* istanbul ignore next */ +export function removeClass(el: Element, cls: string) { + if (!el || !cls) return + const classes = cls.split(' ') + let curClass = ' ' + el.className + ' ' + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i] + if (!clsName) continue + + if (el.classList) { + el.classList.remove(clsName) + } else if (hasClass(el, clsName)) { + curClass = curClass.replace(' ' + clsName + ' ', ' ') + } + } + if (!el.classList) { + el.className = trim(curClass) + } +} + +export function getBoundingClientRect(element: Element): DOMRect | number { + if (!element || !element.getBoundingClientRect) { + return 0 + } + return element.getBoundingClientRect() +} + +/** + * 获取当前元素的left、top偏移 + * left:元素最左侧距离文档左侧的距离 + * top:元素最顶端距离文档顶端的距离 + * right:元素最右侧距离文档右侧的距离 + * bottom:元素最底端距离文档底端的距离 + * rightIncludeBody:元素最左侧距离文档右侧的距离 + * bottomIncludeBody:元素最底端距离文档最底部的距离 + * + * @description: + */ +export function getViewportOffset(element: Element): ViewportOffsetResult { + const doc = document.documentElement + + const docScrollLeft = doc.scrollLeft + const docScrollTop = doc.scrollTop + const docClientLeft = doc.clientLeft + const docClientTop = doc.clientTop + + const pageXOffset = window.pageXOffset + const pageYOffset = window.pageYOffset + + const box = getBoundingClientRect(element) + + const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect + + const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0) + const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0) + const offsetLeft = retLeft + pageXOffset + const offsetTop = rectTop + pageYOffset + + const left = offsetLeft - scrollLeft + const top = offsetTop - scrollTop + + const clientWidth = window.document.documentElement.clientWidth + const clientHeight = window.document.documentElement.clientHeight + return { + left: left, + top: top, + right: clientWidth - rectWidth - left, + bottom: clientHeight - rectHeight - top, + rightIncludeBody: clientWidth - left, + bottomIncludeBody: clientHeight - top + } +} + +/* istanbul ignore next */ +export const on = function ( + element: HTMLElement | Document | Window, + event: string, + handler: EventListenerOrEventListenerObject +): void { + if (element && event && handler) { + element.addEventListener(event, handler, false) + } +} + +/* istanbul ignore next */ +export const off = function ( + element: HTMLElement | Document | Window, + event: string, + handler: any +): void { + if (element && event && handler) { + element.removeEventListener(event, handler, false) + } +} + +/* istanbul ignore next */ +export const once = function (el: HTMLElement, event: string, fn: EventListener): void { + const listener = function (this: any, ...args: unknown[]) { + if (fn) { + // @ts-ignore + fn.apply(this, args) + } + off(el, event, listener) + } + on(el, event, listener) +} + +/* istanbul ignore next */ +export const getStyle = + ieVersion < 9 + ? function (element: Element | any, styleName: string) { + if (isServer) return + if (!element || !styleName) return null + styleName = camelCase(styleName) + if (styleName === 'float') { + styleName = 'styleFloat' + } + try { + switch (styleName) { + case 'opacity': + try { + return element.filters.item('alpha').opacity / 100 + } catch (e) { + return 1.0 + } + default: + return element.style[styleName] || element.currentStyle + ? element.currentStyle[styleName] + : null + } + } catch (e) { + return element.style[styleName] + } + } + : function (element: Element | any, styleName: string) { + if (isServer) return + if (!element || !styleName) return null + styleName = camelCase(styleName) + if (styleName === 'float') { + styleName = 'cssFloat' + } + try { + const computed = (document as any).defaultView.getComputedStyle(element, '') + return element.style[styleName] || computed ? computed[styleName] : null + } catch (e) { + return element.style[styleName] + } + } + +/* istanbul ignore next */ +export function setStyle(element: Element | any, styleName: any, value: any) { + if (!element || !styleName) return + + if (typeof styleName === 'object') { + for (const prop in styleName) { + if (Object.prototype.hasOwnProperty.call(styleName, prop)) { + setStyle(element, prop, styleName[prop]) + } + } + } else { + styleName = camelCase(styleName) + if (styleName === 'opacity' && ieVersion < 9) { + element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')' + } else { + element.style[styleName] = value + } + } +} + +/* istanbul ignore next */ +export const isScroll = (el: Element, vertical: any) => { + if (isServer) return + + const determinedDirection = vertical !== null || vertical !== undefined + const overflow = determinedDirection + ? vertical + ? getStyle(el, 'overflow-y') + : getStyle(el, 'overflow-x') + : getStyle(el, 'overflow') + + return overflow.match(/(scroll|auto)/) +} + +/* istanbul ignore next */ +export const getScrollContainer = (el: Element, vertical?: any) => { + if (isServer) return + + let parent: any = el + while (parent) { + if ([window, document, document.documentElement].includes(parent)) { + return window + } + if (isScroll(parent, vertical)) { + return parent + } + parent = parent.parentNode + } + + return parent +} + +/* istanbul ignore next */ +export const isInContainer = (el: Element, container: any) => { + if (isServer || !el || !container) return false + + const elRect = el.getBoundingClientRect() + let containerRect + + if ([window, document, document.documentElement, null, undefined].includes(container)) { + containerRect = { + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + left: 0 + } + } else { + containerRect = container.getBoundingClientRect() + } + + return ( + elRect.top < containerRect.bottom && + elRect.bottom > containerRect.top && + elRect.right > containerRect.left && + elRect.left < containerRect.right + ) +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..083f3d0 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,139 @@ +// import type { Plugin } from 'vue' + +/** + * + * @param component 需要注册的组件 + * @param alias 组件别名 + * @returns any + */ +export const withInstall = (component: T, alias?: string) => { + const comp = component as any + comp.install = (app: any) => { + app.component(comp.name || comp.displayName, component) + if (alias) { + app.config.globalProperties[alias] = component + } + } + return component as T & Plugin +} + +/** + * @param str 需要转下划线的驼峰字符串 + * @returns 字符串下划线 + */ +export const humpToUnderline = (str: string): string => { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +/** + * @param str 需要转驼峰的下划线字符串 + * @returns 字符串驼峰 + */ +export const underlineToHump = (str: string): string => { + if (!str) return '' + return str.replace(/\-(\w)/g, (_, letter: string) => { + return letter.toUpperCase() + }) +} + +/** + * 驼峰转横杠 + */ +export const humpToDash = (str: string): string => { + return str.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +export const setCssVar = (prop: string, val: any, dom = document.documentElement) => { + dom.style.setProperty(prop, val) +} + +export const getCssVar = (prop: string, dom = document.documentElement) => { + return getComputedStyle(dom).getPropertyValue(prop) +} + +/** + * 查找数组对象的某个下标 + * @param {Array} ary 查找的数组 + * @param {Functon} fn 判断的方法 + */ +// eslint-disable-next-line +export const findIndex = (ary: Array , fn: Fn): number => { + if (ary.findIndex) { + return ary.findIndex(fn) + } + let index = -1 + ary.some((item: T, i: number, ary: Array ) => { + const ret: T = fn(item, i, ary) + if (ret) { + index = i + return ret + } + }) + return index +} + +export const trim = (str: string) => { + return str.replace(/(^\s*)|(\s*$)/g, '') +} + +/** + * @param {Date | number | string} time 需要转换的时间 + * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss + */ +export function formatTime(time: Date | number | string, fmt: string) { + if (!time) return '' + else { + const date = new Date(time) + const o = { + 'M+': date.getMonth() + 1, + 'd+': date.getDate(), + 'H+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds(), + 'q+': Math.floor((date.getMonth() + 3) / 3), + S: date.getMilliseconds() + } + if (/(y+)/.test(fmt)) { + fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) + } + for (const k in o) { + if (new RegExp('(' + k + ')').test(fmt)) { + fmt = fmt.replace( + RegExp.$1, + RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length) + ) + } + } + return fmt + } +} + +/** + * 生成随机字符串 + */ +export function toAnyString() { + const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => { + const r: number = (Math.random() * 16) | 0 + const v: number = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString() + }) + return str +} + +/** + * 首字母大写 + */ +export function firstUpperCase(str: string) { + return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()) +} + +/** + * 把对象转为formData + */ +export function objToFormData(obj: Recordable) { + const formData = new FormData() + Object.keys(obj).forEach((key) => { + formData.append(key, obj[key]) + }) + return formData +} diff --git a/src/utils/is.ts b/src/utils/is.ts new file mode 100644 index 0000000..8ac2e50 --- /dev/null +++ b/src/utils/is.ts @@ -0,0 +1,114 @@ +// copy to vben-admin + +const toString = Object.prototype.toString + +export const is = (val: unknown, type: string) => { + return toString.call(val) === `[object ${type}]` +} + +export const isDef = (val?: T): val is T => { + return typeof val !== 'undefined' +} + +export const isUnDef = (val?: T): val is T => { + return !isDef(val) +} + +export const isObject = (val: any): val is Record => { + return val !== null && is(val, 'Object') +} + +export const isEmpty = (val: T): val is T => { + if (isArray(val) || isString(val)) { + return val.length === 0 + } + + if (val instanceof Map || val instanceof Set) { + return val.size === 0 + } + + if (isObject(val)) { + return Object.keys(val).length === 0 + } + + return false +} + +export const isDate = (val: unknown): val is Date => { + return is(val, 'Date') +} + +export const isNull = (val: unknown): val is null => { + return val === null +} + +export const isNullAndUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) && isNull(val) +} + +export const isNullOrUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) || isNull(val) +} + +export const isNumber = (val: unknown): val is number => { + return is(val, 'Number') +} + +export const isPromise = (val: unknown): val is Promise => { + return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch) +} + +export const isString = (val: unknown): val is string => { + return is(val, 'String') +} + +export const isFunction = (val: unknown): val is Function => { + return typeof val === 'function' +} + +export const isBoolean = (val: unknown): val is boolean => { + return is(val, 'Boolean') +} + +export const isRegExp = (val: unknown): val is RegExp => { + return is(val, 'RegExp') +} + +export const isArray = (val: any): val is Array