├── .env.development ├── src ├── context │ ├── index.ts │ └── demo.ts ├── locales │ ├── index.ts │ ├── lang │ │ ├── index.ts │ │ ├── zh-cn.ts │ │ └── en.ts │ └── i18n.ts ├── service │ ├── index.ts │ ├── api │ │ ├── electronAPI.ts │ │ ├── index.ts │ │ ├── management.adapter.ts │ │ ├── management.ts │ │ └── auth.ts │ └── request │ │ ├── index.ts │ │ └── helpers.ts ├── utils │ ├── form │ │ └── index.ts │ ├── storage │ │ ├── index.ts │ │ ├── session.ts │ │ └── local.ts │ ├── service │ │ ├── index.ts │ │ ├── msg.ts │ │ ├── handler.ts │ │ └── transform.ts │ ├── common │ │ ├── index.ts │ │ ├── pattern.ts │ │ └── number.ts │ ├── index.ts │ ├── router │ │ ├── regexp.ts │ │ ├── index.ts │ │ ├── helpers.ts │ │ ├── module.ts │ │ ├── cache.ts │ │ ├── auth.ts │ │ └── component.ts │ ├── crypto │ │ └── index.ts │ └── logger.ts ├── constants │ ├── index.ts │ └── business.ts ├── router │ ├── helpers │ │ ├── index.ts │ │ └── scroll.ts │ ├── modules │ │ ├── index.ts │ │ ├── dashboard.ts │ │ ├── mock.ts │ │ └── management.ts │ ├── guard │ │ ├── index.ts │ │ ├── dynamic.ts │ │ └── permission.ts │ ├── index.ts │ └── routes │ │ └── index.ts ├── styles │ ├── scss │ │ ├── global.scss │ │ └── scrollbar.scss │ └── css │ │ ├── global.css │ │ ├── scrollbar.css │ │ └── transition.css ├── hooks │ ├── business │ │ ├── useTable.ts │ │ ├── index.ts │ │ ├── useCountDown.ts │ │ └── useSmsCode.ts │ ├── index.ts │ └── common │ │ ├── index.ts │ │ ├── useLoading.ts │ │ ├── useLoadingEmpty.ts │ │ ├── useContext.ts │ │ ├── useBoolean.ts │ │ └── useReload.ts ├── settings │ ├── index.ts │ └── color.ts ├── plugins │ ├── index.ts │ ├── assets.ts │ └── userWorker.ts ├── config │ ├── index.ts │ ├── map-sdk.ts │ ├── regexp.ts │ └── service.ts ├── enum │ ├── index.ts │ ├── business.ts │ ├── common.ts │ └── system.ts ├── store │ ├── subscribe │ │ ├── app.ts │ │ └── index.ts │ ├── modules │ │ ├── index.ts │ │ ├── auth │ │ │ └── helpers.ts │ │ ├── setup-store │ │ │ └── index.ts │ │ ├── app │ │ │ └── index.ts │ │ └── tab │ │ │ └── helpers.ts │ ├── index.ts │ └── plugins │ │ └── index.ts ├── assets │ ├── fonts │ │ ├── aguazyuan-bold.ttf │ │ ├── aguazyuan-light.ttf │ │ └── aguazyuan-regular.ttf │ └── svg-icon │ │ ├── activity.svg │ │ ├── copy.svg │ │ ├── chrome.svg │ │ ├── heart.svg │ │ ├── at-sign.svg │ │ ├── wind.svg │ │ ├── cast.svg │ │ ├── custom-icon.svg │ │ ├── logo.svg │ │ └── logo-fill.svg ├── layouts │ ├── common │ │ ├── GlobalSearch │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── SearchFooter.vue │ │ │ │ └── SearchResult.vue │ │ │ └── index.vue │ │ ├── GlobalTab │ │ │ ├── components │ │ │ │ ├── TabDetail │ │ │ │ │ └── components │ │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── ReloadButton │ │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ ├── GlobalSider │ │ │ ├── components │ │ │ │ ├── VerticalSider │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── VerticalMenu.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── index.ts │ │ │ │ └── VerticalMixSider │ │ │ │ │ └── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── MixMenuCollapse.vue │ │ │ │ │ └── MixMenuDetail.vue │ │ │ └── index.vue │ │ ├── SettingDrawer │ │ │ ├── components │ │ │ │ ├── LayoutMode │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── LayoutCheckbox.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── ThemeColorSelect │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── ColorCheckbox.vue │ │ │ │ │ │ └── ColorModal.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── SettingMenu │ │ │ │ │ └── index.vue │ │ │ │ ├── index.ts │ │ │ │ ├── DrawerButton │ │ │ │ │ └── index.vue │ │ │ │ ├── DarkMode │ │ │ │ │ └── index.vue │ │ │ │ ├── PageView │ │ │ │ │ └── index.vue │ │ │ │ └── ThemeConfig │ │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ ├── GlobalFooter │ │ │ └── index.vue │ │ ├── GlobalBackTop │ │ │ └── index.vue │ │ ├── GlobalHeader │ │ │ ├── components │ │ │ │ ├── ThemeMode.vue │ │ │ │ ├── SettingButton.vue │ │ │ │ ├── MenuCollapse.vue │ │ │ │ ├── GithubSite.vue │ │ │ │ ├── FullScreen.vue │ │ │ │ ├── index.ts │ │ │ │ ├── HeaderMenu.vue │ │ │ │ ├── UserAvatar.vue │ │ │ │ ├── MessageList.vue │ │ │ │ └── GlobalBreadcrumb.vue │ │ │ └── index.vue │ │ ├── index.ts │ │ ├── GlobalLogo │ │ │ └── index.vue │ │ └── GlobalContent │ │ │ └── index.vue │ ├── index.ts │ ├── BlankLayout │ │ └── index.vue │ └── BasicLayout │ │ └── index.vue ├── views │ ├── dashboard │ │ ├── analysis │ │ │ ├── components │ │ │ │ ├── DataCard │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── GradientBg.vue │ │ │ │ │ └── index.vue │ │ │ │ └── index.ts │ │ │ └── index.vue │ │ └── workbench │ │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── WorkbenchMain │ │ │ │ └── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ShortcutsCard.vue │ │ │ │ │ └── TechnologyCard.vue │ │ │ └── WorkbenchHeader │ │ │ │ └── index.vue │ │ │ └── index.vue │ ├── management │ │ ├── role │ │ │ └── index.vue │ │ ├── auth │ │ │ └── index.vue │ │ ├── route │ │ │ └── index.vue │ │ └── user │ │ │ └── components │ │ │ └── ColumnSetting.vue │ ├── _builtin │ │ ├── 403 │ │ │ └── index.vue │ │ ├── 404 │ │ │ └── index.vue │ │ ├── 500 │ │ │ └── index.vue │ │ ├── not-found │ │ │ └── index.vue │ │ └── login │ │ │ └── components │ │ │ ├── LoginBg │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── CornerTop.vue │ │ │ │ └── CornerBottom.vue │ │ │ └── index.vue │ │ │ ├── PwdLogin │ │ │ └── components │ │ │ │ ├── index.ts │ │ │ │ ├── OtherLogin.vue │ │ │ │ └── OtherAccount.vue │ │ │ ├── index.ts │ │ │ └── BindWechat │ │ │ └── index.vue │ └── index.ts ├── typings │ ├── naive-ui.d.ts │ ├── router.d.ts │ ├── package.d.ts │ ├── expose.d.ts │ ├── renderer.d.ts │ ├── global.d.ts │ ├── storage.d.ts │ ├── utils.d.ts │ ├── business.d.ts │ ├── api.d.ts │ ├── page-route.d.ts │ └── env.d.ts ├── composables │ ├── index.ts │ ├── events.ts │ ├── system.ts │ └── icon.ts ├── components │ ├── custom │ │ ├── GithubLink.vue │ │ ├── WebSiteLink.vue │ │ ├── ImageVerify.vue │ │ ├── SvgIcon.vue │ │ ├── MonacoEditor.vue │ │ ├── BetterScroll.vue │ │ └── IconSelect.vue │ ├── common │ │ ├── SystemLogo.vue │ │ ├── DarkModeContainer.vue │ │ ├── ExceptionBase.vue │ │ ├── DarkModeSwitch.vue │ │ ├── NaiveProvider.vue │ │ ├── AppLoading.vue │ │ └── HoverContainer.vue │ └── business │ │ └── LoginAgreement.vue ├── directives │ ├── index.ts │ ├── network.ts │ ├── permission.ts │ └── login.ts ├── main.ts └── App.vue ├── mock ├── model │ ├── index.ts │ └── auth.ts ├── api │ ├── index.ts │ ├── route.ts │ └── management.ts └── index.ts ├── docs ├── add-api.png ├── apilist.png ├── add-api1.png ├── edit-api.png ├── save-api.png ├── test-api.png ├── add-project.png ├── add-project1.png ├── add-project2.png ├── add-project3.png ├── add-project-err.png ├── test-api-result.png ├── test-api-result1.png └── readme.md ├── public ├── icon.icns ├── favicon.ico ├── node.svg ├── vite.svg └── favicon.svg ├── .prettierignore ├── .eslintignore ├── .npmrc ├── .env.production ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── linter.yml └── ISSUE_TEMPLATE │ └── bug-report.yaml ├── electron ├── config │ ├── defaultConfig.ts │ └── index.ts ├── electron-env.d.ts └── utils │ ├── mock │ └── types.d.ts │ └── appDir.ts ├── .editorconfig ├── .vscode ├── launch.json └── extensions.json ├── .gitattributes ├── index.html ├── scripts └── logo.ts ├── .env ├── .gitignore ├── changelogithub.config.json ├── tsconfig.json ├── .env-config.ts ├── electron-builder.json5 └── LICENSE /.env.development: -------------------------------------------------------------------------------- 1 | VITE_HTTP_PROXY=Y 2 | -------------------------------------------------------------------------------- /src/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './demo'; 2 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n'; 2 | -------------------------------------------------------------------------------- /src/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | -------------------------------------------------------------------------------- /src/utils/form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rule'; 2 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './business'; 2 | -------------------------------------------------------------------------------- /src/router/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './scroll'; 2 | -------------------------------------------------------------------------------- /src/styles/scss/global.scss: -------------------------------------------------------------------------------- 1 | @import './scrollbar.scss'; 2 | -------------------------------------------------------------------------------- /src/hooks/business/useTable.ts: -------------------------------------------------------------------------------- 1 | export function useTable() {} 2 | -------------------------------------------------------------------------------- /mock/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | export * from './route'; 3 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './business'; 3 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme'; 2 | export * from './color'; 3 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './local'; 2 | export * from './session'; 3 | -------------------------------------------------------------------------------- /docs/add-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/add-api.png -------------------------------------------------------------------------------- /docs/apilist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/apilist.png -------------------------------------------------------------------------------- /public/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/public/icon.icns -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import setupAssets from './assets'; 2 | 3 | export { setupAssets }; 4 | -------------------------------------------------------------------------------- /docs/add-api1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/add-api1.png -------------------------------------------------------------------------------- /docs/edit-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/edit-api.png -------------------------------------------------------------------------------- /docs/save-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/save-api.png -------------------------------------------------------------------------------- /docs/test-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/test-api.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /docs/add-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/add-project.png -------------------------------------------------------------------------------- /docs/add-project1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/add-project1.png -------------------------------------------------------------------------------- /docs/add-project2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/add-project2.png -------------------------------------------------------------------------------- /docs/add-project3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/add-project3.png -------------------------------------------------------------------------------- /src/service/api/electronAPI.ts: -------------------------------------------------------------------------------- 1 | export const { getAppConfig, ipcRenderer } = window.electronAPI; 2 | -------------------------------------------------------------------------------- /docs/add-project-err.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/add-project-err.png -------------------------------------------------------------------------------- /docs/test-api-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/test-api-result.png -------------------------------------------------------------------------------- /docs/test-api-result1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/docs/test-api-result1.png -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './service'; 2 | export * from './regexp'; 3 | export * from './map-sdk'; 4 | -------------------------------------------------------------------------------- /src/enum/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './system'; 3 | export * from './business'; 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore all HTML files: 2 | *.html 3 | electron-builder.json5 4 | .github/ 5 | *.md 6 | *.png 7 | -------------------------------------------------------------------------------- /src/store/subscribe/app.ts: -------------------------------------------------------------------------------- 1 | /** 订阅app store */ 2 | export default function subscribeAppStore() { 3 | // 4 | } 5 | -------------------------------------------------------------------------------- /src/service/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | export * from './management'; 3 | export * from './electronAPI'; 4 | -------------------------------------------------------------------------------- /src/utils/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transform'; 2 | export * from './error'; 3 | export * from './handler'; 4 | -------------------------------------------------------------------------------- /src/assets/fonts/aguazyuan-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/src/assets/fonts/aguazyuan-bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/aguazyuan-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/src/assets/fonts/aguazyuan-light.ttf -------------------------------------------------------------------------------- /src/layouts/common/GlobalSearch/components/index.ts: -------------------------------------------------------------------------------- 1 | import SearchModal from './SearchModal.vue'; 2 | 3 | export { SearchModal }; 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.env-config.ts 2 | components.d.ts 3 | router-page.d.ts 4 | *.svg 5 | dist-electron/ 6 | dist/ 7 | *.md 8 | .github/ 9 | -------------------------------------------------------------------------------- /src/assets/fonts/aguazyuan-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixin59/electron-mock-api/HEAD/src/assets/fonts/aguazyuan-regular.ttf -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false 4 | auto-install-peers=true 5 | -------------------------------------------------------------------------------- /src/utils/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typeof'; 2 | export * from './color'; 3 | export * from './number'; 4 | export * from './pattern'; 5 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/DataCard/components/index.ts: -------------------------------------------------------------------------------- 1 | import GradientBg from './GradientBg.vue'; 2 | 3 | export { GradientBg }; 4 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalTab/components/TabDetail/components/index.ts: -------------------------------------------------------------------------------- 1 | import ContextMenu from './ContextMenu.vue'; 2 | 3 | export { ContextMenu }; 4 | -------------------------------------------------------------------------------- /src/typings/naive-ui.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NaiveUI { 2 | type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning'; 3 | } 4 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSider/components/VerticalSider/components/index.ts: -------------------------------------------------------------------------------- 1 | import VerticalMenu from './VerticalMenu.vue'; 2 | 3 | export { VerticalMenu }; 4 | -------------------------------------------------------------------------------- /src/views/management/role/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_VISUALIZER=N 2 | 3 | VITE_COMPRESS=N 4 | 5 | # gzip | brotliCompress | deflate | deflateRaw 6 | VITE_COMPRESS_TYPE=gzip 7 | 8 | VITE_PWA=N 9 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/LayoutMode/components/index.ts: -------------------------------------------------------------------------------- 1 | import LayoutCheckbox from './LayoutCheckbox.vue'; 2 | 3 | export { LayoutCheckbox }; 4 | -------------------------------------------------------------------------------- /src/styles/css/global.css: -------------------------------------------------------------------------------- 1 | @import './transition.css'; 2 | @import './reset.css'; 3 | @import './scrollbar.css'; 4 | 5 | html, body, #app { 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/views/management/auth/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/management/route/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/typings/router.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router'; 2 | 3 | declare module 'vue-router' { 4 | interface RouteMeta extends AuthRoute.RouteMeta {} 5 | } 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pull Request 详情 2 | 3 | 请根据实际使用情况修改以下信息。 4 | 5 | ## 版本信息 6 | 7 | ## 解决了哪些问题 8 | 9 | ## 是否关闭了某个 Issue 10 | 11 | Closes # 12 | -------------------------------------------------------------------------------- /src/views/_builtin/403/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/_builtin/404/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/_builtin/500/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/_builtin/not-found/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mock/api/index.ts: -------------------------------------------------------------------------------- 1 | import auth from './auth'; 2 | import route from './route'; 3 | import management from './management'; 4 | 5 | export default [...auth, ...route, ...management]; 6 | -------------------------------------------------------------------------------- /src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | const BasicLayout = () => import('./BasicLayout/index.vue'); 2 | const BlankLayout = () => import('./BlankLayout/index.vue'); 3 | 4 | export { BasicLayout, BlankLayout }; 5 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './storage'; 3 | export * from './service'; 4 | export * from './router'; 5 | export * from './form'; 6 | export * from './logger'; 7 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './system'; 2 | export * from './router'; 3 | export * from './layout'; 4 | export * from './events'; 5 | export * from './echarts'; 6 | export * from './icon'; 7 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalTab/components/index.ts: -------------------------------------------------------------------------------- 1 | import TabDetail from './TabDetail/index.vue'; 2 | import ReloadButton from './ReloadButton/index.vue'; 3 | 4 | export { TabDetail, ReloadButton }; 5 | -------------------------------------------------------------------------------- /src/views/_builtin/login/components/LoginBg/components/index.ts: -------------------------------------------------------------------------------- 1 | import CornerTop from './CornerTop.vue'; 2 | import CornerBottom from './CornerBottom.vue'; 3 | 4 | export { CornerTop, CornerBottom }; 5 | -------------------------------------------------------------------------------- /src/views/_builtin/login/components/PwdLogin/components/index.ts: -------------------------------------------------------------------------------- 1 | import OtherLogin from './OtherLogin.vue'; 2 | import OtherAccount from './OtherAccount.vue'; 3 | 4 | export { OtherLogin, OtherAccount }; 5 | -------------------------------------------------------------------------------- /electron/config/defaultConfig.ts: -------------------------------------------------------------------------------- 1 | const defaultConfigs = Object.freeze({ 2 | appName: 'vite-admin' 3 | }); 4 | 5 | export type tDefaultConfigs = typeof defaultConfigs; 6 | 7 | export default defaultConfigs; 8 | -------------------------------------------------------------------------------- /mock/index.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'; 2 | import api from './api'; 3 | 4 | export function setupMockServer() { 5 | createProdMockServer(api); 6 | } 7 | -------------------------------------------------------------------------------- /src/views/dashboard/workbench/components/index.ts: -------------------------------------------------------------------------------- 1 | import WorkbenchHeader from './WorkbenchHeader/index.vue'; 2 | import WorkbenchMain from './WorkbenchMain/index.vue'; 3 | 4 | export { WorkbenchHeader, WorkbenchMain }; 5 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSider/components/index.ts: -------------------------------------------------------------------------------- 1 | import VerticalSider from './VerticalSider/index.vue'; 2 | import VerticalMixSider from './VerticalMixSider/index.vue'; 3 | 4 | export { VerticalSider, VerticalMixSider }; 5 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/ThemeColorSelect/components/index.ts: -------------------------------------------------------------------------------- 1 | import ColorCheckbox from './ColorCheckbox.vue'; 2 | import ColorModal from './ColorModal.vue'; 3 | 4 | export { ColorCheckbox, ColorModal }; 5 | -------------------------------------------------------------------------------- /src/hooks/business/index.ts: -------------------------------------------------------------------------------- 1 | import useCountDown from './useCountDown'; 2 | import useSmsCode from './useSmsCode'; 3 | import useImageVerify from './useImageVerify'; 4 | 5 | export { useCountDown, useSmsCode, useImageVerify }; 6 | -------------------------------------------------------------------------------- /src/views/dashboard/workbench/components/WorkbenchMain/components/index.ts: -------------------------------------------------------------------------------- 1 | import TechnologyCard from './TechnologyCard.vue'; 2 | import ShortcutsCard from './ShortcutsCard.vue'; 3 | 4 | export { TechnologyCard, ShortcutsCard }; 5 | -------------------------------------------------------------------------------- /src/assets/svg-icon/activity.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/router/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { handleModuleRoutes } from '@/utils'; 2 | 3 | const modules = import.meta.glob('./**/*.ts', { eager: true }) as AuthRoute.RouteModule; 4 | 5 | export const routes = handleModuleRoutes(modules); 6 | -------------------------------------------------------------------------------- /src/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app'; 2 | export * from './theme'; 3 | export * from './auth'; 4 | export * from './tab'; 5 | export * from './route'; 6 | export * from './setup-store'; 7 | export * from './mock-project'; 8 | -------------------------------------------------------------------------------- /src/store/subscribe/index.ts: -------------------------------------------------------------------------------- 1 | import subscribeAppStore from './app'; 2 | import subscribeThemeStore from './theme'; 3 | 4 | /** 订阅状态 */ 5 | export function subscribeStore() { 6 | subscribeAppStore(); 7 | subscribeThemeStore(); 8 | } 9 | -------------------------------------------------------------------------------- /src/locales/lang/index.ts: -------------------------------------------------------------------------------- 1 | import zhCN from './zh-cn'; 2 | import en from './en'; 3 | 4 | const locales = { 5 | 'zh-CN': zhCN, 6 | en 7 | }; 8 | 9 | export type LocaleKey = keyof typeof locales; 10 | 11 | export default locales; 12 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/index.ts: -------------------------------------------------------------------------------- 1 | import TopChart from './TopChart/index.vue'; 2 | import DataCard from './DataCard/index.vue'; 3 | import BottomPart from './BottomPart/index.vue'; 4 | 5 | export { TopChart, DataCard, BottomPart }; 6 | -------------------------------------------------------------------------------- /src/utils/router/regexp.ts: -------------------------------------------------------------------------------- 1 | /** 获取登录页面模块的动态路由的正则 */ 2 | export function getLoginModuleRegExp() { 3 | const modules: EnumType.LoginModuleKey[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat']; 4 | return modules.join('|'); 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /src/utils/router/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './helpers'; 3 | export * from './cache'; 4 | export * from './auth'; 5 | export * from './menu'; 6 | export * from './breadcrumb'; 7 | export * from './regexp'; 8 | export * from './transform'; 9 | -------------------------------------------------------------------------------- /src/typings/package.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare namespace BMap { 5 | class Map extends BMapGL.Map {} 6 | class Point extends BMapGL.Point {} 7 | } 8 | 9 | declare const TMap: any; 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Vue debugger", 8 | "url": "http://localhost:3200", 9 | "webRoot": "${workspaceFolder}" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSider/components/VerticalMixSider/components/index.ts: -------------------------------------------------------------------------------- 1 | import MixMenuDetail from './MixMenuDetail.vue'; 2 | import MixMenuDrawer from './MixMenuDrawer.vue'; 3 | import MixMenuCollapse from './MixMenuCollapse.vue'; 4 | 5 | export { MixMenuDetail, MixMenuDrawer, MixMenuCollapse }; 6 | -------------------------------------------------------------------------------- /src/typings/expose.d.ts: -------------------------------------------------------------------------------- 1 | /** vue 的defineExpose导出的类型 */ 2 | declare namespace Expose { 3 | interface BetterScroll { 4 | instance: import('@better-scroll/core').BScrollInstance; 5 | } 6 | 7 | interface ImageVerify { 8 | /** 获取图片验证码 */ 9 | getImgCode(): void; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/svg-icon/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: 'true'; 6 | DIST_ELECTRON: string; 7 | DIST: string; 8 | /** /dist/ or /public/ */ 9 | PUBLIC: string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/svg-icon/chrome.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/hooks/common/index.ts: -------------------------------------------------------------------------------- 1 | import useContext from './useContext'; 2 | import useBoolean from './useBoolean'; 3 | import useLoading from './useLoading'; 4 | import useLoadingEmpty from './useLoadingEmpty'; 5 | import useReload from './useReload'; 6 | 7 | export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload }; 8 | -------------------------------------------------------------------------------- /src/utils/common/pattern.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 策略模式 3 | * @param actions 每一种可能执行的操作 4 | */ 5 | export function exeStrategyActions(actions: Common.StrategyAction[]) { 6 | actions.some(item => { 7 | const [flag, action] = item; 8 | if (flag) { 9 | action(); 10 | } 11 | return flag; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/assets/svg-icon/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/hooks/common/useLoading.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from './useBoolean'; 2 | 3 | export default function useLoading(initValue = false) { 4 | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue); 5 | 6 | return { 7 | loading, 8 | startLoading, 9 | endLoading 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalFooter/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/dashboard/workbench/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/svg-icon/at-sign.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/enum/business.ts: -------------------------------------------------------------------------------- 1 | /** 用户角色 */ 2 | export enum EnumUserRole { 3 | super = '超级管理员', 4 | admin = '管理员', 5 | user = '普通用户' 6 | } 7 | 8 | /** 登录模块 */ 9 | export enum EnumLoginModule { 10 | 'pwd-login' = '账密登录', 11 | 'code-login' = '手机验证码登录', 12 | 'register' = '注册', 13 | 'reset-pwd' = '重置密码', 14 | 'bind-wechat' = '微信绑定' 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/svg-icon/wind.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/plugins/assets.ts: -------------------------------------------------------------------------------- 1 | import 'uno.css'; 2 | import 'swiper/css'; 3 | import 'swiper/css/navigation'; 4 | import 'swiper/css/pagination'; 5 | import 'virtual:svg-icons-register'; 6 | import '../styles/css/global.css'; 7 | 8 | /** import static assets: css, js , font and so on. - [引入静态资源,css、js和字体文件等] */ 9 | export default function setupAssets() {} 10 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/svg-icon/cast.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/typings/renderer.d.ts: -------------------------------------------------------------------------------- 1 | import type { IpcRenderer } from 'electron'; 2 | import type { tDefaultConfigs } from '../../electron/config/defaultConfig'; 3 | export interface IElectronAPI { 4 | getAppConfig: () => Promise; 5 | ipcRenderer: IpcRenderer; 6 | } 7 | 8 | declare global { 9 | interface Window { 10 | electronAPI: IElectronAPI; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/service/api/management.adapter.ts: -------------------------------------------------------------------------------- 1 | export function adapterOfFetchUserList(data: ApiUserManagement.User[] | null): UserManagement.User[] { 2 | if (!data) return []; 3 | 4 | return data.map((item, index) => { 5 | const user: UserManagement.User = { 6 | index: index + 1, 7 | key: item.id, 8 | ...item 9 | }; 10 | 11 | return user; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | "*.vue" eol=lf 2 | "*.js" eol=lf 3 | "*.ts" eol=lf 4 | "*.jsx" eol=lf 5 | "*.tsx" eol=lf 6 | "*.cjs" eol=lf 7 | "*.cts" eol=lf 8 | "*.mjs" eol=lf 9 | "*.mts" eol=lf 10 | "*.json" eol=lf 11 | "*.html" eol=lf 12 | "*.css" eol=lf 13 | "*.less" eol=lf 14 | "*.scss" eol=lf 15 | "*.sass" eol=lf 16 | "*.styl" eol=lf 17 | "*.md" eol=lf 18 | -------------------------------------------------------------------------------- /src/components/custom/GithubLink.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /src/service/api/management.ts: -------------------------------------------------------------------------------- 1 | import { adapter } from '@/utils'; 2 | import { mockRequest } from '../request'; 3 | import { adapterOfFetchUserList } from './management.adapter'; 4 | 5 | /** 获取用户列表 */ 6 | export const fetchUserList = async () => { 7 | const data = await mockRequest.post('/getAllUserList'); 8 | return adapter(adapterOfFetchUserList, data); 9 | }; 10 | -------------------------------------------------------------------------------- /src/views/_builtin/login/components/index.ts: -------------------------------------------------------------------------------- 1 | import LoginBg from './LoginBg/index.vue'; 2 | import PwdLogin from './PwdLogin/index.vue'; 3 | import CodeLogin from './CodeLogin/index.vue'; 4 | import Register from './Register/index.vue'; 5 | import ResetPwd from './ResetPwd/index.vue'; 6 | import BindWechat from './BindWechat/index.vue'; 7 | 8 | export { LoginBg, PwdLogin, CodeLogin, Register, ResetPwd, BindWechat }; 9 | -------------------------------------------------------------------------------- /src/components/common/SystemLogo.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import { resetSetupStore } from './plugins'; 4 | 5 | /** setup vue store plugin: pinia. - [安装vue状态管理插件:pinia] */ 6 | export function setupStore(app: App) { 7 | const store = createPinia(); 8 | store.use(resetSetupStore); 9 | 10 | app.use(store); 11 | } 12 | 13 | export * from './modules'; 14 | export * from './subscribe'; 15 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import setupNetworkDirective from './network'; 3 | import setupLoginDirective from './login'; 4 | import setupPermissionDirective from './permission'; 5 | 6 | /** setup custom vue directives. - [安装自定义的vue指令] */ 7 | export function setupDirectives(app: App) { 8 | setupNetworkDirective(app); 9 | setupLoginDirective(app); 10 | setupPermissionDirective(app); 11 | } 12 | -------------------------------------------------------------------------------- /src/composables/events.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from '@vueuse/core'; 2 | import { useTabStore, useThemeStore } from '@/store'; 3 | 4 | /** 全局事件 */ 5 | export function useGlobalEvents() { 6 | const theme = useThemeStore(); 7 | const tab = useTabStore(); 8 | 9 | /** 页面离开时缓存多页签数据 */ 10 | useEventListener(window, 'beforeunload', () => { 11 | theme.cacheThemeSettings(); 12 | tab.cacheTabRoutes(); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalBackTop/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/SettingMenu/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/config/map-sdk.ts: -------------------------------------------------------------------------------- 1 | /** 百度地图sdk地址 */ 2 | export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`; 3 | 4 | /** 高德地图sdk地址 */ 5 | export const GAODE_MAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d'; 6 | 7 | /** 腾讯地图sdk地址 */ 8 | export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7'; 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= appName %> 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/hooks/common/useLoadingEmpty.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from './useBoolean'; 2 | 3 | export default function useLoadingEmpty(initLoading = false, initEmpty = false) { 4 | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initLoading); 5 | const { bool: empty, setBool: setEmpty } = useBoolean(initEmpty); 6 | 7 | return { 8 | loading, 9 | startLoading, 10 | endLoading, 11 | empty, 12 | setEmpty 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/locales/lang/zh-cn.ts: -------------------------------------------------------------------------------- 1 | import type { LocaleMessages } from 'vue-i18n'; 2 | 3 | const locale: LocaleMessages = { 4 | message: { 5 | system: { 6 | title: 'mock管理系统' 7 | }, 8 | routes: { 9 | dashboard: { 10 | dashboard: '仪表盘', 11 | analysis: '分析页', 12 | workbench: '工作台' 13 | }, 14 | about: { 15 | about: '关于' 16 | } 17 | } 18 | } 19 | }; 20 | 21 | export default locale; 22 | -------------------------------------------------------------------------------- /src/views/_builtin/login/components/PwdLogin/components/OtherLogin.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /scripts/logo.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'fs/promises'; 2 | import themeSettings from '../src/settings/theme.json'; 3 | 4 | async function updateFavicon(svgPath: string, color: string) { 5 | const svgStr = await readFile(svgPath, 'utf-8'); 6 | 7 | const svgStrWithColor = svgStr.replace(/currentColor/g, color); 8 | 9 | await writeFile('./public/favicon.svg', svgStrWithColor); 10 | } 11 | 12 | updateFavicon('./src/assets/svg-icon/logo.svg', themeSettings.themeColor); 13 | -------------------------------------------------------------------------------- /src/components/custom/WebSiteLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/locales/lang/en.ts: -------------------------------------------------------------------------------- 1 | import type { LocaleMessages } from 'vue-i18n'; 2 | 3 | const locale: LocaleMessages = { 4 | message: { 5 | system: { 6 | title: 'MockAdmin' 7 | }, 8 | routes: { 9 | dashboard: { 10 | dashboard: 'Dashboard', 11 | analysis: 'Analysis', 12 | workbench: 'Workbench' 13 | }, 14 | about: { 15 | about: 'About' 16 | } 17 | } 18 | } 19 | }; 20 | 21 | export default locale; 22 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=/ 2 | 3 | VITE_APP_NAME=MockAdmin 4 | 5 | VITE_APP_TITLE=mock管理系统 6 | 7 | VITE_APP_DESC=MockAdmin是一个Mock管理系统 8 | 9 | # 权限路由模式: static | dynamic 10 | VITE_AUTH_ROUTE_MODE=static 11 | 12 | # 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页 13 | VITE_ROUTE_HOME_PATH=/dashboard/analysis 14 | 15 | # iconify图标作为组件的前缀 16 | VITE_ICON_PREFFIX=icon 17 | 18 | # 本地SVG图标作为组件的前缀, 请注意一定要包含 VITE_ICON_PREFFIX 19 | # 格式 {VITE_ICON_PREFFIX}-{本地图标集合名称} 20 | VITE_ICON_LOCAL_PREFFIX=icon-local 21 | -------------------------------------------------------------------------------- /src/styles/scss/scrollbar.scss: -------------------------------------------------------------------------------- 1 | @mixin scrollbar($size:8px, $color:#d9d9d9) { 2 | &::-webkit-scrollbar-thumb { 3 | background-color: $color; 4 | border-radius: $size; 5 | } 6 | &::-webkit-scrollbar-thumb:hover { 7 | background-color: $color; 8 | border-radius: $size; 9 | } 10 | &::-webkit-scrollbar { 11 | width: $size; 12 | height: $size; 13 | } 14 | &::-webkit-scrollbar-track-piece { 15 | background-color: rgba(0, 0, 0, 0); 16 | border-radius: 0; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalHeader/components/ThemeMode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/index.ts: -------------------------------------------------------------------------------- 1 | import DrawerButton from './DrawerButton/index.vue'; 2 | import DarkMode from './DarkMode/index.vue'; 3 | import LayoutMode from './LayoutMode/index.vue'; 4 | import ThemeColorSelect from './ThemeColorSelect/index.vue'; 5 | import PageFunc from './PageFunc/index.vue'; 6 | import PageView from './PageView/index.vue'; 7 | import ThemeConfig from './ThemeConfig/index.vue'; 8 | 9 | export { DrawerButton, DarkMode, LayoutMode, ThemeColorSelect, PageFunc, PageView, ThemeConfig }; 10 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $loadingBar?: import('naive-ui').LoadingBarProviderInst; 3 | $dialog?: import('naive-ui').DialogProviderInst; 4 | $message?: import('naive-ui').MessageProviderInst; 5 | $notification?: import('naive-ui').NotificationProviderInst; 6 | } 7 | 8 | /** 通用类型 */ 9 | declare namespace Common { 10 | /** 11 | * 策略模式 12 | * [状态, 为true时执行的回调函数] 13 | */ 14 | type StrategyAction = [boolean, () => void]; 15 | } 16 | 17 | /** 构建时间 */ 18 | declare const PROJECT_BUILD_TIME: string; 19 | -------------------------------------------------------------------------------- /src/locales/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { createI18n } from 'vue-i18n'; 3 | import messages from './lang'; 4 | import type { LocaleKey } from './lang'; 5 | 6 | const i18n = createI18n({ 7 | locale: 'zh-CN', 8 | fallbackLocale: 'en', 9 | messages 10 | }); 11 | 12 | export function setupI18n(app: App) { 13 | app.use(i18n); 14 | } 15 | 16 | export function t(key: string) { 17 | return i18n.global.t(key); 18 | } 19 | 20 | export function setLocale(locale: LocaleKey) { 21 | i18n.global.locale = locale; 22 | } 23 | -------------------------------------------------------------------------------- /src/settings/color.ts: -------------------------------------------------------------------------------- 1 | import colorJson from './color.json'; 2 | 3 | interface TraditionColorDetail { 4 | label: string; 5 | color: string; 6 | } 7 | interface TraditionColor { 8 | label: string; 9 | data: TraditionColorDetail[]; 10 | } 11 | 12 | /** 中国传统颜色 */ 13 | export const traditionColors = colorJson as TraditionColor[]; 14 | 15 | export function isInTraditionColors(color: string) { 16 | return traditionColors.some(item => { 17 | const flag = item.data.some(v => v.color === color); 18 | return flag; 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/common/useContext.ts: -------------------------------------------------------------------------------- 1 | import { inject, provide } from 'vue'; 2 | import type { InjectionKey } from 'vue'; 3 | 4 | /** 创建共享上下文状态 */ 5 | export default function useContext(contextName = 'context') { 6 | const injectKey: InjectionKey = Symbol(contextName); 7 | 8 | function useProvide(context: T) { 9 | provide(injectKey, context); 10 | return context; 11 | } 12 | 13 | function useInject() { 14 | return inject(injectKey) as T; 15 | } 16 | 17 | return { 18 | useProvide, 19 | useInject 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/assets/svg-icon/custom-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/router/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取所有固定路由的名称集合 3 | * @param routes - 固定路由 4 | */ 5 | export function getConstantRouteNames(routes: AuthRoute.Route[]) { 6 | return routes.map(route => getConstantRouteName(route)).flat(1); 7 | } 8 | 9 | /** 10 | * 获取所有固定路由的名称集合 11 | * @param route - 固定路由 12 | */ 13 | function getConstantRouteName(route: AuthRoute.Route) { 14 | const names = [route.name]; 15 | if (route.children?.length) { 16 | names.push(...route.children!.map(item => getConstantRouteName(item)).flat(1)); 17 | } 18 | return names; 19 | } 20 | -------------------------------------------------------------------------------- /src/typings/storage.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace StorageInterface { 2 | /** localStorage的存储数据的类型 */ 3 | interface Session { 4 | demoKey: string; 5 | } 6 | 7 | /** localStorage的存储数据的类型 */ 8 | interface Local { 9 | /** 主题颜色 */ 10 | themeColor: string; 11 | /** 用户token */ 12 | token: string; 13 | /** 用户刷新token */ 14 | refreshToken: string; 15 | /** 用户信息 */ 16 | userInfo: Auth.UserInfo; 17 | /** 主题配置 */ 18 | themeSettings: Theme.Setting; 19 | /** 多页签路由信息 */ 20 | multiTabRoutes: App.GlobalTabRoute[]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSider/components/VerticalMixSider/components/MixMenuCollapse.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/common/DarkModeContainer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/service/request/index.ts: -------------------------------------------------------------------------------- 1 | import { getServiceEnvConfig } from '~/.env-config'; 2 | import { createRequest } from './request'; 3 | 4 | const { url, urlPattern, secondUrl, secondUrlPattern } = getServiceEnvConfig(import.meta.env as ImportMetaEnv); 5 | 6 | const isHttpProxy = import.meta.env.VITE_HTTP_PROXY === 'Y'; 7 | 8 | export const request = createRequest({ baseURL: isHttpProxy ? urlPattern : url }); 9 | 10 | export const secondRequest = createRequest({ baseURL: isHttpProxy ? secondUrlPattern : secondUrl }); 11 | 12 | export const mockRequest = createRequest({ baseURL: '/mock' }); 13 | -------------------------------------------------------------------------------- /src/layouts/common/index.ts: -------------------------------------------------------------------------------- 1 | import SettingDrawer from './SettingDrawer/index.vue'; 2 | import GlobalHeader from './GlobalHeader/index.vue'; 3 | import GlobalTab from './GlobalTab/index.vue'; 4 | import GlobalSider from './GlobalSider/index.vue'; 5 | import GlobalContent from './GlobalContent/index.vue'; 6 | import GlobalFooter from './GlobalFooter/index.vue'; 7 | import GlobalLogo from './GlobalLogo/index.vue'; 8 | import GlobalBackTop from './GlobalBackTop/index.vue'; 9 | 10 | export { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter, GlobalLogo, GlobalBackTop }; 11 | -------------------------------------------------------------------------------- /src/hooks/common/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | /** 4 | * boolean组合式函数 5 | * @param initValue 初始值 6 | */ 7 | export default function useBoolean(initValue = false) { 8 | const bool = ref(initValue); 9 | 10 | function setBool(value: boolean) { 11 | bool.value = value; 12 | } 13 | function setTrue() { 14 | setBool(true); 15 | } 16 | function setFalse() { 17 | setBool(false); 18 | } 19 | function toggle() { 20 | setBool(!bool.value); 21 | } 22 | 23 | return { 24 | bool, 25 | setBool, 26 | setTrue, 27 | setFalse, 28 | toggle 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalHeader/components/SettingButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | dist.zip 15 | coverage 16 | *.local 17 | stats.html 18 | dist-electron 19 | release 20 | 21 | /cypress/videos/ 22 | /cypress/screenshots/ 23 | 24 | # Editor directories and files 25 | .vscode/* 26 | !.vscode/extensions.json 27 | !.vscode/launch.json 28 | !.vscode/settings.json 29 | .idea 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw? 35 | 36 | /src/typings/components.d.ts 37 | package-lock.json 38 | yarn.lock 39 | -------------------------------------------------------------------------------- /src/store/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { PiniaPluginContext } from 'pinia'; 2 | import { cloneDeep } from 'lodash-es'; 3 | 4 | /** 5 | * setup语法的重置状态插件 6 | * @param context 7 | * @description 请将用setup语法的状态id写入到setupSyntaxIds 8 | */ 9 | export function resetSetupStore(context: PiniaPluginContext) { 10 | const setupSyntaxIds = ['setup-store']; 11 | 12 | if (setupSyntaxIds.includes(context.store.$id)) { 13 | const { $state } = context.store; 14 | 15 | const defaultStore = cloneDeep($state); 16 | 17 | context.store.$reset = () => { 18 | context.store.$patch(defaultStore); 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalHeader/components/MenuCollapse.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/hooks/common/useReload.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue'; 2 | import useBoolean from './useBoolean'; 3 | 4 | /** 重载 */ 5 | export default function useReload() { 6 | // 重载的标志 7 | const { bool: reloadFlag, setTrue, setFalse } = useBoolean(true); 8 | 9 | /** 10 | * 触发重载 11 | * @param duration - 延迟时间(ms) 12 | */ 13 | async function handleReload(duration = 0) { 14 | setFalse(); 15 | await nextTick(); 16 | 17 | if (duration > 0) { 18 | setTimeout(() => { 19 | setTrue(); 20 | }, duration); 21 | } 22 | } 23 | 24 | return { 25 | reloadFlag, 26 | handleReload 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/store/modules/auth/helpers.ts: -------------------------------------------------------------------------------- 1 | import { localStg } from '@/utils'; 2 | 3 | /** 获取token */ 4 | export function getToken() { 5 | return localStg.get('token') || ''; 6 | } 7 | 8 | /** 获取用户信息 */ 9 | export function getUserInfo() { 10 | const emptyInfo: Auth.UserInfo = { 11 | userId: '', 12 | userName: '', 13 | userRole: 'user' 14 | }; 15 | const userInfo: Auth.UserInfo = localStg.get('userInfo') || emptyInfo; 16 | 17 | return userInfo; 18 | } 19 | 20 | /** 去除用户相关缓存 */ 21 | export function clearAuthStorage() { 22 | localStg.remove('token'); 23 | localStg.remove('refreshToken'); 24 | localStg.remove('userInfo'); 25 | } 26 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalHeader/components/GithubSite.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router'; 2 | import { useTitle } from '@vueuse/core'; 3 | import { createPermissionGuard } from './permission'; 4 | 5 | /** 6 | * 路由守卫函数 7 | * @param router - 路由实例 8 | */ 9 | export function createRouterGuard(router: Router) { 10 | router.beforeEach(async (to, from, next) => { 11 | // 开始 loadingBar 12 | window.$loadingBar?.start(); 13 | // 页面跳转权限处理 14 | await createPermissionGuard(to, from, next); 15 | }); 16 | router.afterEach(to => { 17 | // 设置document title 18 | useTitle(to.meta.title); 19 | // 结束 loadingBar 20 | window.$loadingBar?.finish(); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /changelogithub.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "feat": { "title": "🚀 Features" }, 4 | "perf": { "title": "🔥 Performance" }, 5 | "fix": { "title": "🩹 Fixes" }, 6 | "refactor": { "title": "💅 Refactors" }, 7 | "docs": { "title": "📖 Documentation" }, 8 | "types": { "title": "🌊 Types" }, 9 | "chore": { "title": "🏡 Chore" }, 10 | "test": { "title": "🧪 Tests" }, 11 | "style": { "title": "🎨 Styles" }, 12 | "ci": { "title": "🤖 CI" } 13 | }, 14 | "scopeMap": {}, 15 | "titles": { 16 | "breakingChanges": "🚨 Breaking Changes" 17 | }, 18 | "contributors": true, 19 | "capitalize": true, 20 | "group": true 21 | } 22 | -------------------------------------------------------------------------------- /src/store/modules/setup-store/index.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import { defineStore } from 'pinia'; 3 | import { useBoolean } from '@/hooks'; 4 | 5 | export const useSetupStore = defineStore('setup-store', () => { 6 | const { bool: visible, setTrue: show, setFalse: hide } = useBoolean(); 7 | 8 | interface Config { 9 | name: string; 10 | } 11 | 12 | const config = reactive({ name: 'config' }); 13 | 14 | /** 设置配置 */ 15 | function setConfig(conf: Partial) { 16 | Object.assign(config, conf); 17 | } 18 | 19 | return { 20 | visible, 21 | show, 22 | hide, 23 | config, 24 | setConfig 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSider/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js'; 2 | 3 | const CryptoSecret = '__CryptoJS_Secret__'; 4 | 5 | /** 6 | * 加密数据 7 | * @param data - 数据 8 | */ 9 | export function encrypto(data: any) { 10 | const newData = JSON.stringify(data); 11 | return CryptoJS.AES.encrypt(newData, CryptoSecret).toString(); 12 | } 13 | 14 | /** 15 | * 解密数据 16 | * @param cipherText - 密文 17 | */ 18 | export function decrypto(cipherText: string) { 19 | const bytes = CryptoJS.AES.decrypt(cipherText, CryptoSecret); 20 | const originalText = bytes.toString(CryptoJS.enc.Utf8); 21 | if (originalText) { 22 | return JSON.parse(originalText); 23 | } 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalHeader/components/FullScreen.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalHeader/components/index.ts: -------------------------------------------------------------------------------- 1 | import MenuCollapse from './MenuCollapse.vue'; 2 | import GlobalBreadcrumb from './GlobalBreadcrumb.vue'; 3 | import HeaderMenu from './HeaderMenu.vue'; 4 | import GithubSite from './GithubSite.vue'; 5 | import FullScreen from './FullScreen.vue'; 6 | import ThemeMode from './ThemeMode.vue'; 7 | import UserAvatar from './UserAvatar.vue'; 8 | import SystemMessage from './SystemMessage.vue'; 9 | import SettingButton from './SettingButton.vue'; 10 | 11 | export { 12 | MenuCollapse, 13 | GlobalBreadcrumb, 14 | HeaderMenu, 15 | GithubSite, 16 | FullScreen, 17 | ThemeMode, 18 | UserAvatar, 19 | SystemMessage, 20 | SettingButton 21 | }; 22 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/DataCard/components/GradientBg.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "jsx": "preserve", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "paths": { 17 | "~/*": ["./*"], 18 | "@/*": ["./src/*"] 19 | }, 20 | "types": ["vite/client", "node", "unplugin-icons/types/vue", "naive-ui/volar", "unplugin-vue-macros/macros-global"] 21 | }, 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /src/enum/common.ts: -------------------------------------------------------------------------------- 1 | /** http请求头的content-type类型 */ 2 | export enum EnumContentType { 3 | json = 'application/json', 4 | formUrlencoded = 'application/x-www-form-urlencoded', 5 | formData = 'multipart/form-data' 6 | } 7 | 8 | /** 数据类型 */ 9 | export enum EnumDataType { 10 | number = '[object Number]', 11 | string = '[object String]', 12 | boolean = '[object Boolean]', 13 | null = '[object Null]', 14 | undefined = '[object Undefined]', 15 | object = '[object Object]', 16 | array = '[object Array]', 17 | function = '[object Function]', 18 | date = '[object Date]', 19 | regexp = '[object RegExp]', 20 | promise = '[object Promise]', 21 | set = '[object Set]', 22 | map = '[object Map]', 23 | file = '[object File]' 24 | } 25 | -------------------------------------------------------------------------------- /src/views/dashboard/workbench/components/WorkbenchMain/components/ShortcutsCard.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/LayoutMode/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Code 3 | 4 | permissions: 5 | contents: write 6 | 7 | on: 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | lint: 13 | name: Lint All Code 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Lint Code Base 23 | uses: github/super-linter@v4 24 | env: 25 | VALIDATE_ALL_CODEBASE: false 26 | DEFAULT_BRANCH: main 27 | # To change branch master or main 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | FILTER_REGEX_EXCLUDE: (docs|.github) 30 | VALIDATE_MARKDOWN: false 31 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/DrawerButton/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSearch/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/utils/router/module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 权限路由排序 3 | * @param routes - 权限路由 4 | */ 5 | export function sortRoutes(routes: AuthRoute.Route[]) { 6 | return routes 7 | .sort((next, pre) => Number(next.meta?.order) - Number(pre.meta?.order)) 8 | .map(i => { 9 | if (i.children) sortRoutes(i.children); 10 | return i; 11 | }); 12 | } 13 | 14 | /** 15 | * 处理全部导入的路由模块 16 | * @param modules - 路由模块 17 | */ 18 | export function handleModuleRoutes(modules: AuthRoute.RouteModule) { 19 | const routes: AuthRoute.Route[] = []; 20 | 21 | Object.keys(modules).forEach(key => { 22 | const item = modules[key].default; 23 | if (item) { 24 | routes.push(item); 25 | } else { 26 | window.console.error(`路由模块解析出错: key = ${key}`); 27 | } 28 | }); 29 | 30 | return sortRoutes(routes); 31 | } 32 | -------------------------------------------------------------------------------- /src/enum/system.ts: -------------------------------------------------------------------------------- 1 | /** 布局组件的名称 */ 2 | export enum EnumLayoutComponentName { 3 | basic = 'basic-layout', 4 | blank = 'blank-layout' 5 | } 6 | 7 | /** 布局模式 */ 8 | export enum EnumThemeLayoutMode { 9 | 'vertical' = '左侧菜单模式', 10 | 'horizontal' = '顶部菜单模式', 11 | 'vertical-mix' = '左侧菜单混合模式', 12 | 'horizontal-mix' = '顶部菜单混合模式' 13 | } 14 | 15 | /** 多页签风格 */ 16 | export enum EnumThemeTabMode { 17 | 'chrome' = '谷歌风格', 18 | 'button' = '按钮风格' 19 | } 20 | 21 | /** 水平模式的菜单位置 */ 22 | export enum EnumThemeHorizontalMenuPosition { 23 | 'flex-start' = '居左', 24 | 'center' = '居中', 25 | 'flex-end' = '居右' 26 | } 27 | 28 | /** 过渡动画类型 */ 29 | export enum EnumThemeAnimateMode { 30 | 'zoom-fade' = '渐变', 31 | 'zoom-out' = '闪现', 32 | 'fade-slide' = '滑动', 33 | 'fade' = '消退', 34 | 'fade-bottom' = '底部消退', 35 | 'fade-scale' = '缩放消退' 36 | } 37 | -------------------------------------------------------------------------------- /mock/api/route.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from 'vite-plugin-mock'; 2 | import { routeModel, userModel } from '../model'; 3 | 4 | const apis: MockMethod[] = [ 5 | { 6 | url: '/mock/getUserRoutes', 7 | method: 'post', 8 | response: (options: Service.MockOption): Service.MockServiceResult => { 9 | const { userId = undefined } = options.body; 10 | 11 | const routeHomeName: AuthRoute.LastDegreeRouteKey = 'dashboard_analysis'; 12 | 13 | const role = userModel.find(item => item.userId === userId)?.userRole || 'user'; 14 | 15 | const filterRoutes = routeModel[role]; 16 | 17 | return { 18 | code: 200, 19 | message: 'ok', 20 | data: { 21 | routes: filterRoutes, 22 | home: routeHomeName 23 | } 24 | }; 25 | } 26 | } 27 | ]; 28 | 29 | export default apis; 30 | -------------------------------------------------------------------------------- /src/router/modules/dashboard.ts: -------------------------------------------------------------------------------- 1 | const dashboard: AuthRoute.Route = { 2 | name: 'dashboard', 3 | path: '/dashboard', 4 | component: 'basic', 5 | children: [ 6 | { 7 | name: 'dashboard_analysis', 8 | path: '/dashboard/analysis', 9 | component: 'self', 10 | meta: { 11 | title: '分析页', 12 | requiresAuth: true, 13 | icon: 'icon-park-outline:analysis' 14 | } 15 | }, 16 | { 17 | name: 'dashboard_workbench', 18 | path: '/dashboard/workbench', 19 | component: 'self', 20 | meta: { 21 | title: '工作台', 22 | requiresAuth: true, 23 | icon: 'icon-park-outline:workbench' 24 | } 25 | } 26 | ], 27 | meta: { 28 | title: '仪表盘', 29 | icon: 'mdi:monitor-dashboard', 30 | order: 1 31 | } 32 | }; 33 | 34 | export default dashboard; 35 | -------------------------------------------------------------------------------- /src/config/regexp.ts: -------------------------------------------------------------------------------- 1 | /** 手机号码正则 */ 2 | export const REGEXP_PHONE = 3 | /^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/; 4 | 5 | /** 邮箱正则 */ 6 | export const REGEXP_EMAIL = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; 7 | 8 | /** 密码正则(密码为6-18位数字/字符/符号的组合) */ 9 | export const REGEXP_PWD = 10 | /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){6,18}$/; 11 | 12 | /** 6位数字验证码正则 */ 13 | export const REGEXP_CODE_SIX = /^\d{6}$/; 14 | 15 | /** 4位数字验证码正则 */ 16 | export const REGEXP_CODE_FOUR = /^\d{4}$/; 17 | 18 | /** url链接正则 */ 19 | export const REGEXP_URL = 20 | /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 21 | -------------------------------------------------------------------------------- /src/router/modules/mock.ts: -------------------------------------------------------------------------------- 1 | const functionRoute: AuthRoute.Route = { 2 | name: 'mock', 3 | path: '/mock', 4 | component: 'basic', 5 | children: [ 6 | { 7 | name: 'mock_projects', 8 | path: '/mock/projects', 9 | component: 'self', 10 | meta: { 11 | title: '接口管理', 12 | requiresAuth: true, 13 | icon: 'nonicons:interface-16' 14 | } 15 | }, 16 | { 17 | name: 'mock_project-detail', 18 | path: '/mock/project-detail', 19 | component: 'self', 20 | meta: { 21 | title: '项目详情', 22 | requiresAuth: true, 23 | hide: true, 24 | icon: 'nonicons:interface-16' 25 | } 26 | } 27 | ], 28 | meta: { 29 | title: 'MOCK管理', 30 | icon: 'icon-park-outline:all-application', 31 | order: 6 32 | } 33 | }; 34 | 35 | export default functionRoute; 36 | -------------------------------------------------------------------------------- /src/service/request/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig } from 'axios'; 2 | import { useAuthStore } from '@/store'; 3 | import { localStg } from '@/utils'; 4 | import { fetchUpdateToken } from '../api'; 5 | 6 | /** 7 | * 刷新token 8 | * @param axiosConfig - token失效时的请求配置 9 | */ 10 | export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) { 11 | const { resetAuthStore } = useAuthStore(); 12 | const refreshToken = localStg.get('refreshToken') || ''; 13 | const { data } = await fetchUpdateToken(refreshToken); 14 | if (data) { 15 | localStg.set('token', data.token); 16 | localStg.set('refreshToken', data.refreshToken); 17 | 18 | const config = { ...axiosConfig }; 19 | if (config.headers) { 20 | config.headers.Authorization = data.token; 21 | } 22 | return config; 23 | } 24 | 25 | resetAuthStore(); 26 | return null; 27 | } 28 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # 使用说明 2 | 3 | ## 添加项目 4 | ![新建项目](./add-project.png) 5 | ![新建项目](./add-project1.png) 6 | ![新建项目](./add-project2.png) 7 | 8 | ## 查看项目详情 9 | ![项目详情](./add-project3.png) 10 | 11 | ## 添加接口 12 | ![添加接口](./add-api.png) 13 | ![添加接口](./add-api1.png) 14 | 15 | ## 编辑接口 16 | ### 接口返回结果内容编辑 17 | 1. 普通json数据 18 | ```json 19 | { 20 | "code": 200, 21 | "msg": "test", 22 | "data": {"name": "张三","age": 20} 23 | } 24 | ``` 25 | 2. [mockjs语法规则json数据](https://github.com/nuysoft/Mock/wiki/Syntax-Specification) 26 | 27 | ```json 28 | { 29 | "code": 200, 30 | "msg": "test", 31 | "data": { 32 | "list|1-10": [ 33 | { 34 | "id|+1": 1 35 | } 36 | ] 37 | } 38 | } 39 | ``` 40 | ![添加接口](./edit-api.png) 41 | ![添加接口](./save-api.png) 42 | 43 | ## 接口预览与测试 44 | ![预览接口信息](./test-api.png) 45 | 46 | ### 错误响应 47 | ![错误响应](./test-api-result.png) 48 | ![正确响应](./test-api-result1.png) 49 | -------------------------------------------------------------------------------- /src/components/common/ExceptionBase.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "afzalsayed96.icones", 4 | "antfu.iconify", 5 | "antfu.unocss", 6 | "christian-kohler.path-intellisense", 7 | "dbaeumer.vscode-eslint", 8 | "eamodio.gitlens", 9 | "editorconfig.editorconfig", 10 | "esbenp.prettier-vscode", 11 | "formulahendry.auto-close-tag", 12 | "formulahendry.auto-complete-tag", 13 | "formulahendry.auto-rename-tag", 14 | "kisstkondoros.vscode-gutter-preview", 15 | "lokalise.i18n-ally", 16 | "mariusalchimavicius.json-to-ts", 17 | "mhutchie.git-graph", 18 | "mikestead.dotenv", 19 | "naumovs.color-highlight", 20 | "pkief.material-icon-theme", 21 | "sdras.vue-vscode-snippets", 22 | "steoates.autoimport", 23 | "vue.volar", 24 | "vue.vscode-typescript-vue-plugin", 25 | "whtouche.vscode-js-console-utils", 26 | "zhuangtongfa.material-theme" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/directives/network.ts: -------------------------------------------------------------------------------- 1 | import type { App, Directive } from 'vue'; 2 | import { NETWORK_ERROR_MSG } from '@/config'; 3 | 4 | export default function setupNetworkDirective(app: App) { 5 | function listenerHandler(event: MouseEvent) { 6 | const hasNetwork = window.navigator.onLine; 7 | if (!hasNetwork) { 8 | window.$message?.error(NETWORK_ERROR_MSG); 9 | event.stopPropagation(); 10 | } 11 | } 12 | 13 | const networkDirective: Directive = { 14 | mounted(el: HTMLElement, binding) { 15 | if (binding.value === false) return; 16 | el.addEventListener('click', listenerHandler, { capture: true }); 17 | }, 18 | unmounted(el: HTMLElement, binding) { 19 | if (binding.value === false) return; 20 | el.removeEventListener('click', listenerHandler); 21 | } 22 | }; 23 | 24 | app.directive('network', networkDirective); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/router/cache.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router'; 2 | 3 | /** 4 | * 获取缓存的路由对应组件的名称 5 | * @param routes - 转换后的vue路由 6 | */ 7 | export function getCacheRoutes(routes: RouteRecordRaw[]) { 8 | const cacheNames: string[] = []; 9 | routes.forEach(route => { 10 | // 只需要获取二级路由的缓存的组件名 11 | if (hasChildren(route)) { 12 | (route.children as RouteRecordRaw[]).forEach(item => { 13 | if (isKeepAlive(item)) { 14 | cacheNames.push(item.name as string); 15 | } 16 | }); 17 | } 18 | }); 19 | return cacheNames; 20 | } 21 | 22 | /** 23 | * 路由是否缓存 24 | * @param route 25 | */ 26 | function isKeepAlive(route: RouteRecordRaw) { 27 | return Boolean(route?.meta?.keepAlive); 28 | } 29 | /** 30 | * 是否有二级路由 31 | * @param route 32 | */ 33 | function hasChildren(route: RouteRecordRaw) { 34 | return Boolean(route.children && route.children.length); 35 | } 36 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/ThemeColorSelect/components/ColorCheckbox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/directives/permission.ts: -------------------------------------------------------------------------------- 1 | import type { App, Directive } from 'vue'; 2 | import { usePermission } from '@/composables'; 3 | 4 | export default function setupPermissionDirective(app: App) { 5 | const { hasPermission } = usePermission(); 6 | 7 | function updateElVisible(el: HTMLElement, permission: Auth.RoleType | Auth.RoleType[]) { 8 | if (!permission) { 9 | throw new Error(`need roles: like v-permission="'admin'", v-permission="['admin', 'test]"`); 10 | } 11 | if (!hasPermission(permission)) { 12 | el.parentElement?.removeChild(el); 13 | } 14 | } 15 | 16 | const permissionDirective: Directive = { 17 | mounted(el, binding) { 18 | updateElVisible(el, binding.value); 19 | }, 20 | beforeUpdate(el, binding) { 21 | updateElVisible(el, binding.value); 22 | } 23 | }; 24 | 25 | app.directive('permission', permissionDirective); 26 | } 27 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSider/components/VerticalSider/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import AppLoading from './components/common/AppLoading.vue'; 4 | import { setupDirectives } from './directives'; 5 | import { setupRouter } from './router'; 6 | import { setupAssets } from './plugins'; 7 | import { setupStore } from './store'; 8 | import { setupI18n } from './locales'; 9 | import './plugins/userWorker'; 10 | 11 | async function setupApp() { 12 | // import assets: js、css 13 | setupAssets(); 14 | 15 | // app loading 16 | const appLoading = createApp(AppLoading); 17 | 18 | appLoading.mount('#appLoading'); 19 | 20 | const app = createApp(App); 21 | 22 | // store plugin: pinia 23 | setupStore(app); 24 | 25 | // vue custom directives 26 | setupDirectives(app); 27 | 28 | // vue router 29 | await setupRouter(app); 30 | 31 | setupI18n(app); 32 | 33 | // mount app 34 | app.mount('#app'); 35 | } 36 | 37 | setupApp(); 38 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSearch/components/SearchFooter.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /src/directives/login.ts: -------------------------------------------------------------------------------- 1 | import type { App, Directive } from 'vue'; 2 | import { useAuthStore } from '@/store'; 3 | import { useRouterPush } from '@/composables'; 4 | 5 | export default function setupLoginDirective(app: App) { 6 | const auth = useAuthStore(); 7 | const { toLogin } = useRouterPush(false); 8 | function listenerHandler(event: MouseEvent) { 9 | if (!auth.isLogin) { 10 | event.stopPropagation(); 11 | toLogin(); 12 | } 13 | } 14 | 15 | const loginDirective: Directive = { 16 | mounted(el: HTMLElement, binding) { 17 | if (binding.value === false) return; 18 | el.addEventListener('click', listenerHandler, { capture: true }); 19 | }, 20 | unmounted(el: HTMLElement, binding) { 21 | if (binding.value === false) return; 22 | el.removeEventListener('click', listenerHandler); 23 | } 24 | }; 25 | 26 | app.directive('login', loginDirective); 27 | } 28 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalLogo/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/typings/utils.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace TypeUtil { 2 | type Noop = (...args: any) => any; 3 | 4 | type UnionInclude = K extends keyof T ? true : false; 5 | 6 | type GetFunArgs = F extends (...args: infer P) => any ? P : never; 7 | 8 | type Writable = { [K in keyof T]: T[K] }; 9 | 10 | type FirstOfArray = T extends [infer First, ...infer _Rest] ? First : never; 11 | 12 | type LastOfArray = T extends [...infer _Rest, infer Last] ? Last : never; 13 | 14 | // union to tuple 15 | type Union2IntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer R) => void 16 | ? R 17 | : never; 18 | type GetUnionLast = Union2IntersectionFn extends () => infer I ? I : never; 19 | 20 | type UnionToTuple = [T] extends [never] 21 | ? R 22 | : UnionToTuple>, [GetUnionLast, ...R]>; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/service/msg.ts: -------------------------------------------------------------------------------- 1 | import { ERROR_MSG_DURATION, NO_ERROR_MSG_CODE } from '@/config'; 2 | 3 | /** 错误消息栈,防止同一错误同时出现 */ 4 | const errorMsgStack = new Map([]); 5 | 6 | function addErrorMsg(error: Service.RequestError) { 7 | errorMsgStack.set(error.code, error.msg); 8 | } 9 | function removeErrorMsg(error: Service.RequestError) { 10 | errorMsgStack.delete(error.code); 11 | } 12 | function hasErrorMsg(error: Service.RequestError) { 13 | return errorMsgStack.has(error.code); 14 | } 15 | 16 | /** 17 | * 显示错误信息 18 | * @param error 19 | */ 20 | export function showErrorMsg(error: Service.RequestError) { 21 | if (!error.msg || NO_ERROR_MSG_CODE.includes(error.code) || hasErrorMsg(error)) return; 22 | 23 | addErrorMsg(error); 24 | window.console.warn(error.code, error.msg); 25 | window.$message?.error(error.msg, { duration: ERROR_MSG_DURATION }); 26 | setTimeout(() => { 27 | removeErrorMsg(error); 28 | }, ERROR_MSG_DURATION); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/custom/ImageVerify.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/utils/common/number.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 根据数字获取对应的汉字 3 | * @param num - 数字(0-10) 4 | */ 5 | export function getHanByNumber(num: number) { 6 | const HAN_STR = '零一二三四五六七八九十'; 7 | return HAN_STR.charAt(num); 8 | } 9 | 10 | /** 11 | * 将总秒数转换成 分:秒 12 | * @param seconds - 秒 13 | */ 14 | export function transformToTimeCountDown(seconds: number) { 15 | const SECONDS_A_MINUTE = 60; 16 | function fillZero(num: number) { 17 | return num.toString().padStart(2, '0'); 18 | } 19 | const minuteNum = Math.floor(seconds / SECONDS_A_MINUTE); 20 | const minute = fillZero(minuteNum); 21 | const second = fillZero(seconds - minuteNum * SECONDS_A_MINUTE); 22 | return `${minute}: ${second}`; 23 | } 24 | 25 | /** 26 | * 获取指定整数范围内的随机整数 27 | * @param start - 开始范围 28 | * @param end - 结束范围 29 | */ 30 | export function getRandomInteger(end: number, start = 0) { 31 | const range = end - start; 32 | const random = Math.floor(Math.random() * range + start); 33 | return random; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/common/DarkModeSwitch.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /mock/api/management.ts: -------------------------------------------------------------------------------- 1 | import { mock } from 'mockjs'; 2 | import type { MockMethod } from 'vite-plugin-mock'; 3 | 4 | const apis: MockMethod[] = [ 5 | { 6 | url: '/mock/getAllUserList', 7 | method: 'post', 8 | response: (): Service.MockServiceResult => { 9 | const data = mock({ 10 | 'list|1000': [ 11 | { 12 | id: '@id', 13 | userName: '@cname', 14 | 'age|18-56': 56, 15 | 'gender|1': ['0', '1', null], 16 | phone: 17 | /^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/, 18 | 'email|1': ['@email("qq.com")', null], 19 | 'userStatus|1': ['1', '2', '3', '4', null] 20 | } 21 | ] 22 | }); 23 | 24 | return { 25 | code: 200, 26 | message: 'ok', 27 | data: data.list 28 | }; 29 | } 30 | } 31 | ]; 32 | 33 | export default apis; 34 | -------------------------------------------------------------------------------- /.env-config.ts: -------------------------------------------------------------------------------- 1 | /** 请求服务的环境配置 */ 2 | type ServiceEnv = Record; 3 | 4 | /** 不同请求服务的环境配置 */ 5 | const serviceEnv: ServiceEnv = { 6 | dev: { 7 | url: 'http://localhost:8080', 8 | urlPattern: '/url-pattern', 9 | secondUrl: 'http://localhost:8081', 10 | secondUrlPattern: '/second-url-pattern' 11 | }, 12 | test: { 13 | url: 'http://localhost:8080', 14 | urlPattern: '/url-pattern', 15 | secondUrl: 'http://localhost:8081', 16 | secondUrlPattern: '/second-url-pattern' 17 | }, 18 | prod: { 19 | url: 'http://localhost:8080', 20 | urlPattern: '/url-pattern', 21 | secondUrl: 'http://localhost:8081', 22 | secondUrlPattern: '/second-url-pattern' 23 | } 24 | }; 25 | 26 | /** 27 | * 获取当前环境模式下的请求服务的配置 28 | * @param env 环境 29 | */ 30 | export function getServiceEnvConfig(env: ImportMetaEnv) { 31 | const { VITE_SERVICE_ENV = 'dev' } = env; 32 | 33 | const config = serviceEnv[VITE_SERVICE_ENV]; 34 | 35 | return config; 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/router/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 根据用户权限过滤路由 3 | * @param routes - 权限路由 4 | * @param permission - 权限 5 | */ 6 | export function filterAuthRoutesByUserPermission(routes: AuthRoute.Route[], permission: Auth.RoleType) { 7 | return routes.map(route => filterAuthRouteByUserPermission(route, permission)).flat(1); 8 | } 9 | 10 | /** 11 | * 根据用户权限过滤单个路由 12 | * @param route - 单个权限路由 13 | * @param permission - 权限 14 | */ 15 | function filterAuthRouteByUserPermission(route: AuthRoute.Route, permission: Auth.RoleType): AuthRoute.Route[] { 16 | const filterRoute = { ...route }; 17 | const hasPermission = 18 | !route.meta.permissions || permission === 'super' || route.meta.permissions.includes(permission); 19 | 20 | if (filterRoute.children) { 21 | const filterChildren = filterRoute.children.map(item => filterAuthRouteByUserPermission(item, permission)).flat(1); 22 | Object.assign(filterRoute, { children: filterChildren }); 23 | } 24 | return hasPermission ? [filterRoute] : []; 25 | } 26 | -------------------------------------------------------------------------------- /src/typings/business.d.ts: -------------------------------------------------------------------------------- 1 | /** 用户相关模块 */ 2 | declare namespace Auth { 3 | /** 4 | * 用户角色类型(前端静态路由用角色类型进行路由权限的控制) 5 | * - super: 超级管理员(该权限具有所有路由数据) 6 | * - admin: 管理员 7 | * - user: 用户 8 | * - custom: 自定义角色 9 | */ 10 | type RoleType = keyof typeof import('@/enum').EnumUserRole; 11 | 12 | /** 用户信息 */ 13 | interface UserInfo { 14 | /** 用户id */ 15 | userId: string; 16 | /** 用户名 */ 17 | userName: string; 18 | /** 用户角色类型 */ 19 | userRole: RoleType; 20 | } 21 | } 22 | 23 | declare namespace UserManagement { 24 | interface User extends ApiUserManagement.User { 25 | /** 序号 */ 26 | index: number; 27 | /** 表格的key(id) */ 28 | key: string; 29 | } 30 | 31 | /** 32 | * 用户性别 33 | * - 0: 女 34 | * - 1: 男 35 | */ 36 | type GenderKey = NonNullable; 37 | 38 | /** 39 | * 用户状态 40 | * - 1: 启用 41 | * - 2: 禁用 42 | * - 3: 冻结 43 | * - 4: 软删除 44 | */ 45 | type UserStatusKey = NonNullable; 46 | } 47 | -------------------------------------------------------------------------------- /src/views/_builtin/login/components/LoginBg/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /mock/model/auth.ts: -------------------------------------------------------------------------------- 1 | interface UserModel extends Auth.UserInfo { 2 | token: string; 3 | refreshToken: string; 4 | password: string; 5 | } 6 | 7 | export const userModel: UserModel[] = [ 8 | { 9 | token: '__TOKEN_SOYBEAN__', 10 | refreshToken: '__REFRESH_TOKEN_SOYBEAN__', 11 | userId: '0', 12 | userName: 'admin', 13 | userRole: 'super', 14 | password: 'admin123' 15 | }, 16 | { 17 | token: '__TOKEN_SUPER__', 18 | refreshToken: '__REFRESH_TOKEN_SUPER__', 19 | userId: '1', 20 | userName: 'Super', 21 | userRole: 'super', 22 | password: 'super123' 23 | }, 24 | { 25 | token: '__TOKEN_ADMIN__', 26 | refreshToken: '__REFRESH_TOKEN_ADMIN__', 27 | userId: '2', 28 | userName: 'Admin', 29 | userRole: 'admin', 30 | password: 'admin123' 31 | }, 32 | { 33 | token: '__TOKEN_USER01__', 34 | refreshToken: '__REFRESH_TOKEN_USER01__', 35 | userId: '3', 36 | userName: 'User01', 37 | userRole: 'user', 38 | password: 'user01123' 39 | } 40 | ]; 41 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalContent/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /electron-builder.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://www.electron.build/configuration/configuration 3 | */ 4 | { 5 | "appId": "YourAppID", 6 | "asar": true, 7 | "icon": "public/favicon.ico", 8 | "directories": { 9 | "output": "release" 10 | }, 11 | "files": [ 12 | "dist-electron", 13 | "dist" 14 | ], 15 | "mac": { 16 | "artifactName": "${productName}_${version}.${ext}", 17 | "target": [ 18 | "dmg" 19 | ] 20 | }, 21 | "win": { 22 | "target": [ 23 | { 24 | "target": "nsis", 25 | "arch": [ 26 | "x64" 27 | ] 28 | } 29 | ], 30 | "artifactName": "${productName}_${version}.${ext}" 31 | }, 32 | "linux": { 33 | "icon": "public/icon.icns", 34 | "target": [ 35 | { 36 | "target": "deb", 37 | "arch": [ 38 | "x64" 39 | ] 40 | } 41 | ], 42 | "artifactName": "${productName}_${version}.${ext}", 43 | "category": "SmartAssembly" 44 | }, 45 | "nsis": { 46 | "oneClick": false, 47 | "perMachine": false, 48 | "allowToChangeInstallationDirectory": true, 49 | "deleteAppDataOnUninstall": false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/router/helpers/scroll.ts: -------------------------------------------------------------------------------- 1 | import type { RouterScrollBehavior } from 'vue-router'; 2 | import { useTabStore } from '@/store'; 3 | 4 | export const scrollBehavior: RouterScrollBehavior = (to, from) => { 5 | return new Promise(resolve => { 6 | const tab = useTabStore(); 7 | 8 | if (to.hash) { 9 | const el = document.querySelector(to.hash); 10 | if (el) { 11 | resolve({ 12 | el, 13 | behavior: 'smooth' 14 | }); 15 | } 16 | } 17 | 18 | const { left, top } = tab.getTabScrollPosition(to.path); 19 | const scrollPosition = { 20 | left, 21 | top 22 | }; 23 | const { scrollLeft, scrollTop } = document.documentElement; 24 | 25 | const isFromCached = Boolean(from.meta.keepAlive); 26 | if (isFromCached) { 27 | tab.recordTabScrollPosition(from.path, { left: scrollLeft, top: scrollTop }); 28 | } 29 | 30 | const duration = !scrollPosition.left && !scrollPosition.top ? 0 : 350; 31 | 32 | setTimeout(() => { 33 | resolve(scrollPosition); 34 | }, duration); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/common/NaiveProvider.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 38 | 39 | -------------------------------------------------------------------------------- /src/typings/api.d.ts: -------------------------------------------------------------------------------- 1 | // 后端接口返回的数据类型 2 | 3 | /** 后端返回的用户权益相关类型 */ 4 | declare namespace ApiAuth { 5 | /** 返回的token和刷新token */ 6 | interface Token { 7 | token: string; 8 | refreshToken: string; 9 | } 10 | /** 返回的用户信息 */ 11 | type UserInfo = Auth.UserInfo; 12 | } 13 | 14 | /** 后端返回的路由相关类型 */ 15 | declare namespace ApiRoute { 16 | /** 后端返回的路由数据类型 */ 17 | interface Route { 18 | /** 动态路由 */ 19 | routes: AuthRoute.Route[]; 20 | /** 路由首页对应的key */ 21 | home: AuthRoute.AllRouteKey; 22 | } 23 | } 24 | 25 | declare namespace ApiUserManagement { 26 | interface User { 27 | /** 用户id */ 28 | id: string; 29 | /** 用户名 */ 30 | userName: string | null; 31 | /** 用户年龄 */ 32 | age: number | null; 33 | /** 34 | * 用户性别 35 | * - 0: 女 36 | * - 1: 男 37 | */ 38 | gender: '0' | '1' | null; 39 | /** 用户手机号码 */ 40 | phone: string; 41 | /** 用户邮箱 */ 42 | email: string | null; 43 | /** 44 | * 用户状态 45 | * - 1: 启用 46 | * - 2: 禁用 47 | * - 3: 冻结 48 | * - 4: 软删除 49 | */ 50 | userStatus: '1' | '2' | '3' | '4' | null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 lixin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/service/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { mockRequest } from '../request'; 2 | 3 | /** 4 | * 获取验证码 5 | * @param phone - 手机号 6 | * @returns - 返回boolean值表示是否发送成功 7 | */ 8 | export function fetchSmsCode(phone: string) { 9 | return mockRequest.post('/getSmsCode', { phone }); 10 | } 11 | 12 | /** 13 | * 登录 14 | * @param userName - 用户名 15 | * @param password - 密码 16 | */ 17 | export function fetchLogin(userName: string, password: string) { 18 | return mockRequest.post('/login', { userName, password }); 19 | } 20 | 21 | /** 获取用户信息 */ 22 | export function fetchUserInfo() { 23 | return mockRequest.get('/getUserInfo'); 24 | } 25 | 26 | /** 27 | * 获取用户路由数据 28 | * @param userId - 用户id 29 | * @description 后端根据用户id查询到对应的角色类型,并将路由筛选出对应角色的路由数据返回前端 30 | */ 31 | export function fetchUserRoutes(userId: string) { 32 | return mockRequest.post('/getUserRoutes', { userId }); 33 | } 34 | 35 | /** 36 | * 刷新token 37 | * @param refreshToken 38 | */ 39 | export function fetchUpdateToken(refreshToken: string) { 40 | return mockRequest.post('/updateToken', { refreshToken }); 41 | } 42 | -------------------------------------------------------------------------------- /src/context/demo.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | import type { Ref } from 'vue'; 3 | import { useContext } from '@/hooks'; 4 | 5 | interface DemoContext { 6 | counts: Ref; 7 | setCounts: (count: number) => void; 8 | } 9 | 10 | const { useProvide: useDemoProvide, useInject: useDemoInject } = useContext(); 11 | 12 | export function useDemoContext() { 13 | const counts = ref(0); 14 | 15 | function setCounts(count: number) { 16 | counts.value = count; 17 | } 18 | 19 | const demoContext: DemoContext = { 20 | counts, 21 | setCounts 22 | }; 23 | 24 | function useProvide() { 25 | return useDemoProvide(demoContext); 26 | } 27 | 28 | return { 29 | useProvide, 30 | useInject: useDemoInject 31 | }; 32 | } 33 | 34 | // 示例用法: A.vue为父组件, B.vue为子孙组件 C.vue为子孙组件 35 | // A.vue 36 | // import { useDemoContext } from '@/context'; 37 | // const { useProvide } = useDemoContext(); 38 | // const { counts, setCounts } = useProvide(); 39 | 40 | // B.vue 和 C.vue : 共享状态 counts 41 | // import { useDemoContext } from '@/context'; 42 | // const { useInject } = useDemoContext(); 43 | // const { counts, setCounts } = useInject(); 44 | -------------------------------------------------------------------------------- /public/node.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalTab/components/ReloadButton/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/utils/service/handler.ts: -------------------------------------------------------------------------------- 1 | /** 统一失败和成功的请求结果的数据类型 */ 2 | export async function handleServiceResult(error: Service.RequestError | null, data: any) { 3 | if (error) { 4 | const fail: Service.FailedResult = { 5 | error, 6 | data: null 7 | }; 8 | return fail; 9 | } 10 | const success: Service.SuccessResult = { 11 | error: null, 12 | data 13 | }; 14 | return success; 15 | } 16 | 17 | /** 请求结果的适配器:用于接收适配器函数和请求结果 */ 18 | export function adapter( 19 | adapterFun: T, 20 | ...args: Service.MultiRequestResult> 21 | ): Service.RequestResult> { 22 | let result: Service.RequestResult | undefined; 23 | 24 | const hasError = args.some(item => { 25 | const flag = Boolean(item.error); 26 | if (flag) { 27 | result = { 28 | error: item.error, 29 | data: null 30 | }; 31 | } 32 | return flag; 33 | }); 34 | 35 | if (!hasError) { 36 | const adapterFunArgs = args.map(item => item.data); 37 | result = { 38 | error: null, 39 | data: adapterFun(...adapterFunArgs) 40 | }; 41 | } 42 | 43 | return result!; 44 | } 45 | -------------------------------------------------------------------------------- /src/views/_builtin/login/components/PwdLogin/components/OtherAccount.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/constants/business.ts: -------------------------------------------------------------------------------- 1 | /** 用户性别 */ 2 | export const genderLabels: Record = { 3 | 0: '女', 4 | 1: '男' 5 | }; 6 | 7 | export const genderOptions: { value: UserManagement.GenderKey; label: string }[] = [ 8 | { value: '0', label: genderLabels['0'] }, 9 | { value: '1', label: genderLabels['1'] } 10 | ]; 11 | 12 | /** 用户状态 */ 13 | export const userStatusLabels: Record = { 14 | 1: '启用', 15 | 2: '禁用', 16 | 3: '冻结', 17 | 4: '软删除' 18 | }; 19 | 20 | export const userStatusOptions: { value: UserManagement.UserStatusKey; label: string }[] = [ 21 | { value: '1', label: userStatusLabels['1'] }, 22 | { value: '2', label: userStatusLabels['2'] }, 23 | { value: '3', label: userStatusLabels['3'] }, 24 | { value: '4', label: userStatusLabels['4'] } 25 | ]; 26 | 27 | /** 请求方法标签状态map */ 28 | export const methodTagMap: Record = { 29 | get: 'success', 30 | post: 'info', 31 | delete: 'error', 32 | put: 'warning', 33 | head: 'primary', 34 | link: 'primary', 35 | options: 'primary', 36 | patch: 'primary', 37 | unlink: 'default' 38 | }; 39 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'; 3 | import { transformRouteNameToRoutePath } from '@/utils'; 4 | import { transformAuthRouteToVueRoutes } from '@/utils/router/transform'; 5 | import { constantRoutes } from './routes'; 6 | import { scrollBehavior } from './helpers'; 7 | import { createRouterGuard } from './guard'; 8 | 9 | const { VITE_HASH_ROUTE = 'N', VITE_BASE_URL } = import.meta.env; 10 | 11 | export const router = createRouter({ 12 | history: VITE_HASH_ROUTE === 'Y' ? createWebHashHistory(VITE_BASE_URL) : createWebHistory(VITE_BASE_URL), 13 | routes: transformAuthRouteToVueRoutes(constantRoutes), 14 | scrollBehavior 15 | }); 16 | 17 | /** setup vue router. - [安装vue路由] */ 18 | export async function setupRouter(app: App) { 19 | app.use(router); 20 | createRouterGuard(router); 21 | await router.isReady(); 22 | } 23 | 24 | /** 路由名称 */ 25 | export const routeName = (key: AuthRoute.AllRouteKey) => key; 26 | /** 路由路径 */ 27 | export const routePath = (key: Exclude) => transformRouteNameToRoutePath(key); 28 | 29 | export * from './routes'; 30 | export * from './modules'; 31 | -------------------------------------------------------------------------------- /src/views/dashboard/workbench/components/WorkbenchMain/components/TechnologyCard.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/styles/css/scrollbar.css: -------------------------------------------------------------------------------- 1 | html { 2 | scrollbar-width: thin; 3 | scrollbar-color: #e1e1e1 transparent; 4 | } 5 | 6 | /*---滚动条默认显示样式--*/ 7 | html::-webkit-scrollbar-thumb { 8 | background-color: #e1e1e1; 9 | border-radius: 8px; 10 | } 11 | /*---鼠标点击滚动条显示样式--*/ 12 | html::-webkit-scrollbar-thumb:hover { 13 | background-color: #e1e1e1; 14 | border-radius: 8px; 15 | } 16 | /*---滚动条大小--*/ 17 | html::-webkit-scrollbar { 18 | width: 8px; 19 | height: 8px; 20 | } 21 | /*---滚动框背景样式--*/ 22 | html::-webkit-scrollbar-track-piece { 23 | background-color: rgba(0, 0, 0, 0); 24 | border-radius: 0; 25 | } 26 | 27 | 28 | html.dark { 29 | scrollbar-width: thin; 30 | scrollbar-color: #555 transparent; 31 | } 32 | 33 | /*---滚动条默认显示样式--*/ 34 | html.dark::-webkit-scrollbar-thumb { 35 | background-color: #555; 36 | border-radius: 8px; 37 | } 38 | /*---鼠标点击滚动条显示样式--*/ 39 | html.dark::-webkit-scrollbar-thumb:hover { 40 | background-color: #555; 41 | border-radius: 8px; 42 | } 43 | /*---滚动条大小--*/ 44 | html.dark::-webkit-scrollbar { 45 | width: 8px; 46 | height: 8px; 47 | } 48 | 49 | /*---滚动框背景样式--*/ 50 | html.dark::-webkit-scrollbar-track-piece { 51 | background-color: rgba(0, 0, 0, 0); 52 | border-radius: 0; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/business/LoginAgreement.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/views/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteComponent } from 'vue-router'; 2 | 3 | export const views: Record< 4 | PageRoute.LastDegreeRouteKey, 5 | RouteComponent | (() => Promise<{ default: RouteComponent }>) 6 | > = { 7 | 403: () => import('./_builtin/403/index.vue'), 8 | 404: () => import('./_builtin/404/index.vue'), 9 | 500: () => import('./_builtin/500/index.vue'), 10 | 'constant-page': () => import('./_builtin/constant-page/index.vue'), 11 | login: () => import('./_builtin/login/index.vue'), 12 | 'not-found': () => import('./_builtin/not-found/index.vue'), 13 | dashboard_analysis: () => import('./dashboard/analysis/index.vue'), 14 | dashboard_workbench: () => import('./dashboard/workbench/index.vue'), 15 | management_auth: () => import('./management/auth/index.vue'), 16 | management_role: () => import('./management/role/index.vue'), 17 | management_route: () => import('./management/route/index.vue'), 18 | management_user: () => import('./management/user/index.vue'), 19 | 'mock_api-detail': () => import('./mock/api-detail/index.vue'), 20 | 'mock_api-list': () => import('./mock/api-list/index.vue'), 21 | 'mock_project-detail': () => import('./mock/project-detail/index.vue'), 22 | mock_projects: () => import('./mock/projects/index.vue') 23 | }; 24 | -------------------------------------------------------------------------------- /src/hooks/business/useCountDown.ts: -------------------------------------------------------------------------------- 1 | import { computed, onScopeDispose, ref } from 'vue'; 2 | import { useBoolean } from '../common'; 3 | 4 | /** 5 | * 倒计时 6 | * @param second - 倒计时的时间(s) 7 | */ 8 | export default function useCountDown(second: number) { 9 | if (second <= 0 && second % 1 !== 0) { 10 | throw new Error('倒计时的时间应该为一个正整数!'); 11 | } 12 | const { bool: isComplete, setTrue, setFalse } = useBoolean(false); 13 | 14 | const counts = ref(0); 15 | const isCounting = computed(() => Boolean(counts.value)); 16 | 17 | let intervalId: any; 18 | 19 | /** 20 | * 开始计时 21 | * @param updateSecond - 更改初时传入的倒计时时间 22 | */ 23 | function start(updateSecond: number = second) { 24 | if (!counts.value) { 25 | setFalse(); 26 | counts.value = updateSecond; 27 | intervalId = setInterval(() => { 28 | counts.value -= 1; 29 | if (counts.value <= 0) { 30 | clearInterval(intervalId); 31 | setTrue(); 32 | } 33 | }, 1000); 34 | } 35 | } 36 | 37 | /** 38 | * 停止计时 39 | */ 40 | function stop() { 41 | intervalId = clearInterval(intervalId); 42 | counts.value = 0; 43 | } 44 | 45 | onScopeDispose(stop); 46 | 47 | return { 48 | counts, 49 | isCounting, 50 | start, 51 | stop, 52 | isComplete 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/composables/system.ts: -------------------------------------------------------------------------------- 1 | import UAParser from 'ua-parser-js'; 2 | import { useAuthStore } from '@/store'; 3 | import { isArray, isString } from '@/utils'; 4 | 5 | interface AppInfo { 6 | /** 项目名称 */ 7 | name: string; 8 | /** 项目标题 */ 9 | title: string; 10 | /** 项目描述 */ 11 | desc: string; 12 | } 13 | 14 | /** 项目信息 */ 15 | export function useAppInfo(): AppInfo { 16 | const { VITE_APP_NAME: name, VITE_APP_TITLE: title, VITE_APP_DESC: desc } = import.meta.env; 17 | 18 | return { 19 | name, 20 | title, 21 | desc 22 | }; 23 | } 24 | 25 | /** 获取设备信息 */ 26 | export function useDeviceInfo() { 27 | const parser = new UAParser(); 28 | const result = parser.getResult(); 29 | return result; 30 | } 31 | 32 | /** 权限判断 */ 33 | export function usePermission() { 34 | const auth = useAuthStore(); 35 | 36 | function hasPermission(permission: Auth.RoleType | Auth.RoleType[]) { 37 | const { userRole } = auth.userInfo; 38 | 39 | let has = userRole === 'super'; 40 | if (!has) { 41 | if (isArray(permission)) { 42 | has = (permission as Auth.RoleType[]).includes(userRole); 43 | } 44 | if (isString(permission)) { 45 | has = (permission as Auth.RoleType) === userRole; 46 | } 47 | } 48 | return has; 49 | } 50 | 51 | return { 52 | hasPermission 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/router/modules/management.ts: -------------------------------------------------------------------------------- 1 | const management: AuthRoute.Route = { 2 | name: 'management', 3 | path: '/management', 4 | component: 'basic', 5 | children: [ 6 | { 7 | name: 'management_auth', 8 | path: '/management/auth', 9 | component: 'self', 10 | meta: { 11 | title: '权限管理', 12 | requiresAuth: true, 13 | icon: 'ic:baseline-security' 14 | } 15 | }, 16 | { 17 | name: 'management_role', 18 | path: '/management/role', 19 | component: 'self', 20 | meta: { 21 | title: '角色管理', 22 | requiresAuth: true, 23 | icon: 'carbon:user-role' 24 | } 25 | }, 26 | { 27 | name: 'management_user', 28 | path: '/management/user', 29 | component: 'self', 30 | meta: { 31 | title: '用户管理', 32 | requiresAuth: true, 33 | icon: 'ic:round-manage-accounts' 34 | } 35 | }, 36 | { 37 | name: 'management_route', 38 | path: '/management/route', 39 | component: 'self', 40 | meta: { 41 | title: '路由管理', 42 | requiresAuth: true, 43 | icon: 'material-symbols:route' 44 | } 45 | } 46 | ], 47 | meta: { 48 | title: '系统管理', 49 | icon: 'carbon:cloud-service-management', 50 | order: 9 51 | } 52 | }; 53 | 54 | export default management; 55 | -------------------------------------------------------------------------------- /src/views/_builtin/login/components/LoginBg/components/CornerTop.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/config/service.ts: -------------------------------------------------------------------------------- 1 | /** 请求超时时间 */ 2 | export const REQUEST_TIMEOUT = 60 * 1000; 3 | 4 | /** 错误信息的显示时间 */ 5 | export const ERROR_MSG_DURATION = 3 * 1000; 6 | 7 | /** 默认的请求错误code */ 8 | export const DEFAULT_REQUEST_ERROR_CODE = 'DEFAULT'; 9 | 10 | /** 默认的请求错误文本 */ 11 | export const DEFAULT_REQUEST_ERROR_MSG = '请求错误~'; 12 | 13 | /** 请求超时的错误code(为固定值:ECONNABORTED) */ 14 | export const REQUEST_TIMEOUT_CODE = 'ECONNABORTED'; 15 | 16 | /** 请求超时的错误文本 */ 17 | export const REQUEST_TIMEOUT_MSG = '请求超时~'; 18 | 19 | /** 网络不可用的code */ 20 | export const NETWORK_ERROR_CODE = 'NETWORK_ERROR'; 21 | 22 | /** 网络不可用的错误文本 */ 23 | export const NETWORK_ERROR_MSG = '网络不可用~'; 24 | 25 | /** 请求不成功各种状态的错误 */ 26 | export const ERROR_STATUS = { 27 | 400: '400: 请求出现语法错误~', 28 | 401: '401: 用户未授权~', 29 | 403: '403: 服务器拒绝访问~', 30 | 404: '404: 请求的资源不存在~', 31 | 405: '405: 请求方法未允许~', 32 | 408: '408: 网络请求超时~', 33 | 500: '500: 服务器内部错误~', 34 | 501: '501: 服务器未实现请求功能~', 35 | 502: '502: 错误网关~', 36 | 503: '503: 服务不可用~', 37 | 504: '504: 网关超时~', 38 | 505: '505: http版本不支持该请求~', 39 | [DEFAULT_REQUEST_ERROR_CODE]: DEFAULT_REQUEST_ERROR_MSG 40 | }; 41 | 42 | /** 不弹出错误信息的code */ 43 | export const NO_ERROR_MSG_CODE: (string | number)[] = []; 44 | 45 | /** token失效需要刷新token的code(这里的66666只是个例子,需要将后端表示token过期的code填进来) */ 46 | export const REFRESH_TOKEN_CODE: (string | number)[] = [66666]; 47 | -------------------------------------------------------------------------------- /src/components/common/AppLoading.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/views/dashboard/workbench/components/WorkbenchHeader/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/DarkMode/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | 40 | -------------------------------------------------------------------------------- /src/components/common/HoverContainer.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/views/_builtin/login/components/LoginBg/components/CornerBottom.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/custom/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from '@/service/api/electronAPI'; 2 | 3 | export type CommonLogLvl = 'Warn' | 'Info' | 'Error' | 'Success'; 4 | type tCommonLogMeta = Record; 5 | 6 | function logger(lvl: CommonLogLvl, msg: string, ...rest: any[]) { 7 | // if ((process.env.NODE_ENV !== 'production' || process.env.DEBUG_PROD === 'true') && fn[lvl]) { 8 | // fn[lvl](msg, ...rest); 9 | // } 10 | // if (fnAlways[lvl]) { 11 | // fnAlways[lvl](msg, ...rest); 12 | // } 13 | ipcRenderer.send('ipc-event-logger', { lvl, msg, rest }); 14 | } 15 | 16 | export const CommonLog = { 17 | Info(msg: string, meta?: tCommonLogMeta, isShow = false) { 18 | logger('Info', msg, meta); 19 | if (isShow) { 20 | window.$message?.info(msg, { duration: 5000 }); 21 | } 22 | }, 23 | 24 | Success(msg: string, meta?: tCommonLogMeta, isShow = false) { 25 | logger('Success', msg, meta); 26 | if (isShow) { 27 | window.$message?.success(msg, { duration: 5000 }); 28 | } 29 | }, 30 | 31 | Warn(msg: string, meta?: tCommonLogMeta, isShow = false) { 32 | logger('Warn', msg, meta); 33 | if (isShow) { 34 | window.$message?.warning(msg, { duration: 5000 }); 35 | } 36 | }, 37 | 38 | lError(msg: string, meta?: tCommonLogMeta, isShow = false) { 39 | logger('Error', msg, meta); 40 | if (isShow) { 41 | window.$message?.error(msg, { duration: 5000 }); 42 | } 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/ThemeColorSelect/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/custom/MonacoEditor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/composables/icon.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue'; 2 | import SvgIcon from '@/components/custom/SvgIcon.vue'; 3 | 4 | /** 5 | * 图标渲染 6 | * - 用于vue的render函数 7 | */ 8 | export const useIconRender = () => { 9 | interface IconConfig { 10 | /** 11 | * 图标名称(iconify图标的名称) 12 | * - 例如:mdi-account 或者 mdi:account 13 | */ 14 | icon?: string; 15 | /** 16 | * 本地svg图标文件名(assets/svg-icon文件夹下) 17 | */ 18 | localIcon?: string; 19 | /** 图标颜色 */ 20 | color?: string; 21 | /** 图标大小 */ 22 | fontSize?: number; 23 | } 24 | 25 | interface IconStyle { 26 | color?: string; 27 | fontSize?: string; 28 | } 29 | 30 | /** 31 | * 图标渲染 32 | * @param config 33 | * @property icon - 图标名称(iconify图标的名称), 例如:mdi-account 或者 mdi:account 34 | * @property localIcon - 本地svg图标文件名(assets/svg-icon文件夹下) 35 | * @property color - 图标颜色 36 | * @property fontSize - 图标大小 37 | */ 38 | const iconRender = (config: IconConfig) => { 39 | const { color, fontSize, icon, localIcon } = config; 40 | 41 | const style: IconStyle = {}; 42 | 43 | if (color) { 44 | style.color = color; 45 | } 46 | if (fontSize) { 47 | style.fontSize = `${fontSize}px`; 48 | } 49 | 50 | if (!icon && !localIcon) { 51 | window.console.warn('没有传递图标名称,请确保给icon或localIcon传递有效值!'); 52 | } 53 | 54 | return () => h(SvgIcon, { icon, localIcon, style }); 55 | }; 56 | 57 | return { 58 | iconRender 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/typings/page-route.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace PageRoute { 2 | /** 3 | * the root route key 4 | * @translate 根路由 5 | */ 6 | type RootRouteKey = 'root'; 7 | 8 | /** 9 | * the not found route, which catch the invalid route path 10 | * @translate 未找到路由(捕获无效路径的路由) 11 | */ 12 | type NotFoundRouteKey = 'not-found'; 13 | 14 | /** 15 | * the route key 16 | * @translate 页面路由 17 | */ 18 | type RouteKey = 19 | | '403' 20 | | '404' 21 | | '500' 22 | | 'constant-page' 23 | | 'login' 24 | | 'not-found' 25 | | 'dashboard' 26 | | 'dashboard_analysis' 27 | | 'dashboard_workbench' 28 | | 'management' 29 | | 'management_auth' 30 | | 'management_role' 31 | | 'management_route' 32 | | 'management_user' 33 | | 'mock' 34 | | 'mock_api-detail' 35 | | 'mock_api-list' 36 | | 'mock_project-detail' 37 | | 'mock_projects'; 38 | 39 | /** 40 | * last degree route key, which has the page file 41 | * @translate 最后一级路由(该级路有对应的页面文件) 42 | */ 43 | type LastDegreeRouteKey = Extract< 44 | RouteKey, 45 | | '403' 46 | | '404' 47 | | '500' 48 | | 'constant-page' 49 | | 'login' 50 | | 'not-found' 51 | | 'dashboard_analysis' 52 | | 'dashboard_workbench' 53 | | 'management_auth' 54 | | 'management_role' 55 | | 'management_route' 56 | | 'management_user' 57 | | 'mock_api-detail' 58 | | 'mock_api-list' 59 | | 'mock_project-detail' 60 | | 'mock_projects' 61 | >; 62 | } 63 | -------------------------------------------------------------------------------- /src/assets/svg-icon/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/logo-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/custom/BetterScroll.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalSider/components/VerticalMixSider/components/MixMenuDetail.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalHeader/components/HeaderMenu.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /src/plugins/userWorker.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | // eslint-disable-next-line import/no-unresolved 3 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; 4 | // eslint-disable-next-line import/no-unresolved 5 | import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; 6 | // eslint-disable-next-line import/no-unresolved 7 | import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; 8 | // eslint-disable-next-line import/no-unresolved 9 | import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; 10 | // eslint-disable-next-line import/no-unresolved 11 | import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; 12 | 13 | self.MonacoEnvironment = { 14 | getWorker(_: any, label: string) { 15 | if (label === 'json') { 16 | // eslint-disable-next-line new-cap 17 | return new jsonWorker(); 18 | } 19 | if (label === 'css' || label === 'scss' || label === 'less') { 20 | // eslint-disable-next-line new-cap 21 | return new cssWorker(); 22 | } 23 | if (label === 'html' || label === 'handlebars' || label === 'razor') { 24 | // eslint-disable-next-line new-cap 25 | return new htmlWorker(); 26 | } 27 | if (label === 'typescript' || label === 'javascript') { 28 | // eslint-disable-next-line new-cap 29 | return new tsWorker(); 30 | } 31 | // eslint-disable-next-line new-cap 32 | return new editorWorker(); 33 | } 34 | }; 35 | 36 | monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); 37 | -------------------------------------------------------------------------------- /src/utils/storage/session.ts: -------------------------------------------------------------------------------- 1 | import { decrypto, encrypto } from '../crypto'; 2 | 3 | export function setSession(key: string, value: unknown) { 4 | const json = encrypto(value); 5 | sessionStorage.setItem(key, json); 6 | } 7 | 8 | export function getSession(key: string) { 9 | const json = sessionStorage.getItem(key); 10 | let data: T | null = null; 11 | if (json) { 12 | try { 13 | data = decrypto(json); 14 | } catch { 15 | // 防止解析失败 16 | } 17 | } 18 | return data; 19 | } 20 | 21 | export function removeSession(key: string) { 22 | window.sessionStorage.removeItem(key); 23 | } 24 | 25 | export function clearSession() { 26 | window.sessionStorage.clear(); 27 | } 28 | 29 | function createSessionStorage() { 30 | function set(key: K, value: T[K]) { 31 | const json = encrypto(value); 32 | sessionStorage.setItem(key as string, json); 33 | } 34 | function get(key: K) { 35 | const json = sessionStorage.getItem(key as string); 36 | let data: T[K] | null = null; 37 | if (json) { 38 | try { 39 | data = decrypto(json); 40 | } catch { 41 | // 防止解析失败 42 | } 43 | } 44 | return data; 45 | } 46 | function remove(key: keyof T) { 47 | window.sessionStorage.removeItem(key as string); 48 | } 49 | function clear() { 50 | window.sessionStorage.clear(); 51 | } 52 | 53 | return { 54 | set, 55 | get, 56 | remove, 57 | clear 58 | }; 59 | } 60 | 61 | export const sessionStg = createSessionStorage(); 62 | -------------------------------------------------------------------------------- /src/layouts/common/GlobalHeader/components/UserAvatar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/ThemeColorSelect/components/ColorModal.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/router/guard/dynamic.ts: -------------------------------------------------------------------------------- 1 | import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'; 2 | import { routeName } from '@/router'; 3 | import { useRouteStore } from '@/store'; 4 | import { localStg } from '@/utils'; 5 | 6 | /** 7 | * 动态路由 8 | */ 9 | export async function createDynamicRouteGuard( 10 | to: RouteLocationNormalized, 11 | _from: RouteLocationNormalized, 12 | next: NavigationGuardNext 13 | ) { 14 | const route = useRouteStore(); 15 | const isLogin = Boolean(localStg.get('token')); 16 | 17 | // 初始化权限路由 18 | if (!route.isInitAuthRoute) { 19 | // 未登录情况下直接回到登录页,登录成功后再加载权限路由 20 | if (!isLogin) { 21 | const toName = to.name as AuthRoute.AllRouteKey; 22 | if (route.isValidConstantRoute(toName) && !to.meta.requiresAuth) { 23 | next(); 24 | } else { 25 | const redirect = to.fullPath; 26 | next({ name: routeName('login'), query: { redirect } }); 27 | } 28 | return false; 29 | } 30 | 31 | await route.initAuthRoute(); 32 | 33 | if (to.name === routeName('not-found')) { 34 | // 动态路由没有加载导致被not-found路由捕获,等待权限路由加载好了,回到之前的路由 35 | // 若路由是从根路由重定向过来的,重新回到根路由 36 | const ROOT_ROUTE_NAME: AuthRoute.AllRouteKey = 'root'; 37 | const path = to.redirectedFrom?.name === ROOT_ROUTE_NAME ? '/' : to.fullPath; 38 | next({ path, replace: true, query: to.query, hash: to.hash }); 39 | return false; 40 | } 41 | } 42 | 43 | // 权限路由已经加载,仍然未找到,重定向到404 44 | if (to.name === routeName('not-found')) { 45 | next({ name: routeName('404'), replace: true }); 46 | return false; 47 | } 48 | 49 | return true; 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/router/component.ts: -------------------------------------------------------------------------------- 1 | import type { RouteComponent } from 'vue-router'; 2 | import { BasicLayout, BlankLayout } from '@/layouts'; 3 | import { views } from '@/views'; 4 | import { isFunction } from '../common'; 5 | 6 | type Lazy = () => Promise; 7 | 8 | interface ModuleComponent { 9 | default: RouteComponent; 10 | } 11 | 12 | type LayoutComponent = Record>; 13 | 14 | /** 15 | * 获取布局的vue文件(懒加载的方式) 16 | * @param layoutType - 布局类型 17 | */ 18 | export function getLayoutComponent(layoutType: EnumType.LayoutComponentName) { 19 | const layoutComponent: LayoutComponent = { 20 | basic: BasicLayout, 21 | blank: BlankLayout 22 | }; 23 | return layoutComponent[layoutType]; 24 | } 25 | 26 | /** 27 | * 获取页面导入的vue文件 28 | * @param routeKey - 路由key 29 | */ 30 | export function getViewComponent(routeKey: AuthRoute.LastDegreeRouteKey) { 31 | if (!views[routeKey]) { 32 | throw new Error(`路由“${routeKey}”没有对应的组件文件!`); 33 | } 34 | return setViewComponentName(views[routeKey], routeKey); 35 | } 36 | 37 | /** 给页面组件设置名称 */ 38 | function setViewComponentName(component: RouteComponent | Lazy, name: string) { 39 | if (isAsyncComponent(component)) { 40 | return async () => { 41 | const result = await component(); 42 | Object.assign(result.default, { name }); 43 | return result; 44 | }; 45 | } 46 | 47 | Object.assign(component, { name }); 48 | 49 | return component; 50 | } 51 | 52 | function isAsyncComponent(component: RouteComponent | Lazy): component is Lazy { 53 | return isFunction(component); 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/storage/local.ts: -------------------------------------------------------------------------------- 1 | import { decrypto, encrypto } from '../crypto'; 2 | interface StorageData { 3 | value: T; 4 | expire: number | null; 5 | } 6 | 7 | function createLocalStorage() { 8 | /** 默认缓存期限为7天 */ 9 | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7; 10 | 11 | function set(key: K, value: T[K], expire: number | null = DEFAULT_CACHE_TIME) { 12 | const storageData: StorageData = { 13 | value, 14 | expire: expire !== null ? new Date().getTime() + expire * 1000 : null 15 | }; 16 | const json = encrypto(storageData); 17 | window.localStorage.setItem(key as string, json); 18 | } 19 | 20 | function get(key: K) { 21 | const json = window.localStorage.getItem(key as string); 22 | if (json) { 23 | let storageData: StorageData | null = null; 24 | try { 25 | storageData = decrypto(json); 26 | } catch { 27 | // 防止解析失败 28 | } 29 | if (storageData) { 30 | const { value, expire } = storageData; 31 | // 在有效期内直接返回 32 | if (expire === null || expire >= Date.now()) { 33 | return value as T[K]; 34 | } 35 | } 36 | remove(key); 37 | return null; 38 | } 39 | return null; 40 | } 41 | 42 | function remove(key: keyof T) { 43 | window.localStorage.removeItem(key as string); 44 | } 45 | function clear() { 46 | window.localStorage.clear(); 47 | } 48 | 49 | return { 50 | set, 51 | get, 52 | remove, 53 | clear 54 | }; 55 | } 56 | 57 | export const localStg = createLocalStorage(); 58 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/PageView/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/hooks/business/useSmsCode.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { REGEXP_PHONE } from '@/config'; 3 | import { fetchSmsCode } from '@/service'; 4 | import { useLoading } from '../common'; 5 | import useCountDown from './useCountDown'; 6 | 7 | export default function useSmsCode() { 8 | const { loading, startLoading, endLoading } = useLoading(); 9 | const { counts, start, isCounting } = useCountDown(60); 10 | 11 | const initLabel = '获取验证码'; 12 | const countingLabel = (second: number) => `${second}秒后重新获取`; 13 | const label = computed(() => { 14 | let text = initLabel; 15 | if (loading.value) { 16 | text = ''; 17 | } 18 | if (isCounting.value) { 19 | text = countingLabel(counts.value); 20 | } 21 | return text; 22 | }); 23 | 24 | /** 判断手机号码格式是否正确 */ 25 | function isPhoneValid(phone: string) { 26 | let valid = true; 27 | if (phone.trim() === '') { 28 | window.$message?.error('手机号码不能为空!'); 29 | valid = false; 30 | } else if (!REGEXP_PHONE.test(phone)) { 31 | window.$message?.error('手机号码格式错误!'); 32 | valid = false; 33 | } 34 | return valid; 35 | } 36 | 37 | /** 38 | * 获取短信验证码 39 | * @param phone - 手机号 40 | */ 41 | async function getSmsCode(phone: string) { 42 | const valid = isPhoneValid(phone); 43 | if (!valid || loading.value) return; 44 | 45 | startLoading(); 46 | const { data } = await fetchSmsCode(phone); 47 | if (data) { 48 | window.$message?.success('验证码发送成功!'); 49 | start(); 50 | } 51 | endLoading(); 52 | } 53 | 54 | return { 55 | label, 56 | start, 57 | isCounting, 58 | getSmsCode, 59 | loading 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/styles/css/transition.css: -------------------------------------------------------------------------------- 1 | /* fade */ 2 | .fade-enter-active, 3 | .fade-leave-active { 4 | transition: opacity 0.3s ease-in-out; 5 | } 6 | .fade-enter-from, 7 | .fade-leave-to { 8 | opacity: 0; 9 | } 10 | 11 | /* fade-slide */ 12 | .fade-slide-leave-active, 13 | .fade-slide-enter-active { 14 | transition: all 0.3s; 15 | } 16 | .fade-slide-enter-from { 17 | opacity: 0; 18 | transform: translateX(-30px); 19 | } 20 | .fade-slide-leave-to { 21 | opacity: 0; 22 | transform: translateX(30px); 23 | } 24 | 25 | /* fade-bottom */ 26 | .fade-bottom-enter-active, 27 | .fade-bottom-leave-active { 28 | transition: opacity 0.25s, transform 0.3s; 29 | } 30 | .fade-bottom-enter-from { 31 | opacity: 0; 32 | transform: translateY(-10%); 33 | } 34 | .fade-bottom-leave-to { 35 | opacity: 0; 36 | transform: translateY(10%); 37 | } 38 | 39 | /* fade-scale */ 40 | .fade-scale-leave-active, 41 | .fade-scale-enter-active { 42 | transition: all 0.28s; 43 | } 44 | .fade-scale-enter-from { 45 | opacity: 0; 46 | transform: scale(1.2); 47 | } 48 | .fade-scale-leave-to { 49 | opacity: 0; 50 | transform: scale(0.8); 51 | } 52 | 53 | /* zoom-fade */ 54 | .zoom-fade-enter-active, 55 | .zoom-fade-leave-active { 56 | transition: transform 0.2s, opacity 0.3s ease-out; 57 | } 58 | .zoom-fade-enter-from { 59 | opacity: 0; 60 | transform: scale(0.92); 61 | } 62 | .zoom-fade-leave-to { 63 | opacity: 0; 64 | transform: scale(1.06); 65 | } 66 | 67 | /* zoom-out */ 68 | .zoom-out-enter-active, 69 | .zoom-out-leave-active { 70 | transition: opacity 0.1s ease-in-out, transform 0.15s ease-out; 71 | } 72 | .zoom-out-enter-from, 73 | .zoom-out-leave-to { 74 | opacity: 0; 75 | transform: scale(0); 76 | } 77 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/utils/service/transform.ts: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import FormData from 'form-data'; 3 | import { EnumContentType } from '@/enum'; 4 | import { isArray, isFile } from '../common'; 5 | 6 | /** 7 | * 请求数据的转换 8 | * @param requestData - 请求数据 9 | * @param contentType - 请求头的Content-Type 10 | */ 11 | export async function transformRequestData(requestData: any, contentType?: string) { 12 | // application/json类型不处理 13 | let data = requestData; 14 | // form类型转换 15 | if (contentType === EnumContentType.formUrlencoded) { 16 | data = qs.stringify(requestData); 17 | } 18 | // form-data类型转换 19 | if (contentType === EnumContentType.formData) { 20 | data = await handleFormData(requestData); 21 | } 22 | 23 | return data; 24 | } 25 | 26 | async function handleFormData(data: Record) { 27 | const formData = new FormData(); 28 | const entries = Object.entries(data); 29 | 30 | entries.forEach(async ([key, value]) => { 31 | const isFileType = isFile(value) || (isArray(value) && value.length && isFile(value[0])); 32 | 33 | if (isFileType) { 34 | await transformFile(formData, key, value); 35 | } else { 36 | formData.append(key, value); 37 | } 38 | }); 39 | 40 | return formData; 41 | } 42 | 43 | /** 44 | * 接口为上传文件的类型时数据转换 45 | * @param key - 文件的属性名 46 | * @param file - 单文件或多文件 47 | */ 48 | async function transformFile(formData: FormData, key: string, file: File[] | File) { 49 | if (isArray(file)) { 50 | // 多文件 51 | await Promise.all( 52 | (file as File[]).map(item => { 53 | formData.append(key, item); 54 | return true; 55 | }) 56 | ); 57 | } else { 58 | // 单文件 59 | formData.append(key, file); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { getLoginModuleRegExp } from '@/utils'; 2 | 3 | /** 根路由: / */ 4 | export const ROOT_ROUTE: AuthRoute.Route = { 5 | name: 'root', 6 | path: '/', 7 | redirect: import.meta.env.VITE_ROUTE_HOME_PATH, 8 | meta: { 9 | title: 'Root' 10 | } 11 | }; 12 | 13 | /** 固定的路由 */ 14 | export const constantRoutes: AuthRoute.Route[] = [ 15 | ROOT_ROUTE, 16 | { 17 | name: 'login', 18 | path: '/login', 19 | component: 'self', 20 | props: route => { 21 | const moduleType = (route.params.module as EnumType.LoginModuleKey) || 'pwd-login'; 22 | return { 23 | module: moduleType 24 | }; 25 | }, 26 | meta: { 27 | title: '登录', 28 | dynamicPath: `/login/:module(${getLoginModuleRegExp()})?`, 29 | singleLayout: 'blank' 30 | } 31 | }, 32 | { 33 | name: 'constant-page', 34 | path: '/constant-page', 35 | component: 'self', 36 | meta: { 37 | title: '固定页面', 38 | singleLayout: 'blank' 39 | } 40 | }, 41 | { 42 | name: '403', 43 | path: '/403', 44 | component: 'self', 45 | meta: { 46 | title: '无权限', 47 | singleLayout: 'blank' 48 | } 49 | }, 50 | { 51 | name: '404', 52 | path: '/404', 53 | component: 'self', 54 | meta: { 55 | title: '未找到', 56 | singleLayout: 'blank' 57 | } 58 | }, 59 | { 60 | name: '500', 61 | path: '/500', 62 | component: 'self', 63 | meta: { 64 | title: '服务器错误', 65 | singleLayout: 'blank' 66 | } 67 | }, 68 | // 匹配无效路径的路由 69 | { 70 | name: 'not-found', 71 | path: '/:pathMatch(.*)*', 72 | component: 'blank', 73 | meta: { 74 | title: '未找到', 75 | singleLayout: 'blank' 76 | } 77 | } 78 | ]; 79 | -------------------------------------------------------------------------------- /src/layouts/common/SettingDrawer/components/ThemeConfig/index.vue: -------------------------------------------------------------------------------- 1 |