├── .browserslistrc ├── .editorconfig ├── .env.development ├── .env.production ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.en.md ├── README.md ├── babel.config.js ├── deploy └── index.js ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico └── index.html ├── src ├── @types │ ├── index.d.ts │ └── vue-proptery.d.ts ├── App.vue ├── api │ └── login.ts ├── assets │ ├── docs │ │ ├── qq.png │ │ └── wechat.png │ └── layout │ │ ├── discard1.png │ │ ├── discard2.png │ │ ├── discard3.png │ │ ├── logo-text-1.png │ │ ├── logo-text-2.png │ │ ├── logo-text-3.png │ │ ├── logo.png │ │ └── logo.svg ├── components │ ├── screenfull │ │ └── index.vue │ ├── svg-icon │ │ └── index.vue │ └── theme-switch │ │ └── index.vue ├── config │ ├── layout.ts │ ├── roles.ts │ ├── theme.ts │ └── white-list.ts ├── constant │ └── key.ts ├── directives │ ├── index.ts │ └── permission │ │ └── index.ts ├── icons │ ├── index.ts │ └── svg │ │ ├── 404.svg │ │ ├── bug.svg │ │ ├── dashboard.svg │ │ └── lock.svg ├── layout │ ├── components │ │ ├── app-main.vue │ │ ├── bread-crumb │ │ │ └── index.vue │ │ ├── hamburger │ │ │ └── index.vue │ │ ├── index.ts │ │ ├── navigation-bar │ │ │ └── index.vue │ │ ├── right-panel │ │ │ └── index.vue │ │ ├── settings │ │ │ └── index.vue │ │ ├── sidebar │ │ │ ├── index.vue │ │ │ ├── sidebar-item-link.vue │ │ │ ├── sidebar-item.vue │ │ │ └── sidebar-logo.vue │ │ └── tags-view │ │ │ ├── index.vue │ │ │ └── scroll-pane.vue │ ├── index.vue │ └── useResize.ts ├── main.ts ├── model │ └── demo.ts ├── plugins │ ├── element.ts │ ├── index.ts │ └── monitor.ts ├── router │ ├── index.ts │ └── permission.ts ├── shims-vue.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app.ts │ │ ├── permission.ts │ │ ├── settings.ts │ │ ├── tags-view.ts │ │ └── user.ts ├── styles │ ├── index.scss │ ├── mixins.scss │ ├── theme │ │ ├── dark │ │ │ ├── index.scss │ │ │ └── setting.scss │ │ ├── register.scss │ │ └── theme.scss │ └── transition.scss ├── utils │ ├── cookies.ts │ ├── index.ts │ ├── permission.ts │ ├── service.ts │ └── validate.ts └── views │ ├── dashboard │ ├── admin │ │ └── index.vue │ ├── editor │ │ └── index.vue │ └── index.vue │ ├── error-page │ ├── 401.vue │ └── 404.vue │ ├── login │ └── index.vue │ ├── monitor │ ├── components │ │ └── iframe-breadcurmb.vue │ └── index.vue │ ├── permission │ ├── components │ │ └── switch-roles.vue │ ├── directive.vue │ └── page.vue │ └── redirect │ └── index.vue ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | chrome 79 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 请勿改动这一项 2 | NODE_ENV = development 3 | 4 | # 自定义的环境变量可以修改 5 | VUE_APP_BASE_API = 'https://vue-typescript-admin-mock-server-armour.vercel.app/mock-api/v1' 6 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 请勿改动这一项 2 | NODE_ENV = production 3 | 4 | # 自定义的环境变量可以修改 5 | VUE_APP_BASE_API = 'https://vue-typescript-admin-mock-server-armour.vercel.app/mock-api/v1' 6 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 请勿改动这一项 2 | NODE_ENV = production 3 | 4 | # 自定义的环境变量可以修改 5 | VUE_APP_BASE_API = 'https://vue-typescript-admin-mock-server-armour.vercel.app/mock-api/v1' 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | vue.config.js 2 | mock/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true 7 | }, 8 | globals: { 9 | defineProps: 'readonly', 10 | defineEmits: 'readonly', 11 | defineExpose: 'readonly' 12 | }, 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | ecmaVersion: 2020 16 | }, 17 | extends: [ 18 | 'plugin:vue/vue3-recommended', 19 | 'plugin:vue/vue3-strongly-recommended', 20 | 'plugin:@typescript-eslint/recommended', 21 | '@vue/standard', 22 | '@vue/typescript/recommended' 23 | ], 24 | rules: { 25 | 'vue/multi-word-component-names': 'off', 26 | 'vue/comment-directive': 'off', 27 | 'no-console': 'off', 28 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 29 | '@typescript-eslint/ban-types': 'off', 30 | '@typescript-eslint/explicit-module-boundary-types': 'off', 31 | '@typescript-eslint/member-delimiter-style': ['error', 32 | { 33 | multiline: { 34 | delimiter: 'none' 35 | }, 36 | singleline: { 37 | delimiter: 'comma' 38 | } 39 | }], 40 | '@typescript-eslint/no-explicit-any': 'off', 41 | '@typescript-eslint/no-var-requires': 'off', 42 | 'prefer-regex-literals': 'off', 43 | 'space-before-function-paren': ['error', 'never'], 44 | 'vue/array-bracket-spacing': 'error', 45 | 'vue/arrow-spacing': 'error', 46 | 'vue/block-spacing': 'error', 47 | 'vue/brace-style': 'error', 48 | 'vue/camelcase': 'error', 49 | 'vue/comma-dangle': 'error', 50 | 'vue/component-name-in-template-casing': 'error', 51 | 'vue/eqeqeq': 'error', 52 | 'vue/key-spacing': 'error', 53 | 'vue/match-component-file-name': 'error', 54 | 'vue/object-curly-spacing': 'error', 55 | 'vue/max-attributes-per-line': 'off', 56 | 'vue/html-closing-bracket-newline': 'off', 57 | 'no-useless-escape': 'off', 58 | '@typescript-eslint/no-this-alias': [ 59 | 'error', 60 | { 61 | allowDestructuring: true, // Allow `const { props, state } = this`; false by default 62 | allowedNames: ['self'] // Allow `const self = this`; `[]` by default 63 | } 64 | ], 65 | 'vue/attribute-hyphenation': 'off', 66 | 'vue/custom-event-name-casing': 'off', 67 | 'dot-notation': 'off' 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build And Deploy v3-admin 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Setup Node.js 18.16.1 18 | uses: actions/setup-node@master 19 | with: 20 | node-version: "18.16.1" 21 | 22 | - name: Setup pnpm 23 | uses: pnpm/action-setup@v2 24 | with: 25 | version: "8.6.3" 26 | 27 | - name: Build 28 | run: pnpm install && pnpm build:prod 29 | 30 | - name: Deploy 31 | uses: JamesIves/github-pages-deploy-action@releases/v3 32 | with: 33 | ACCESS_TOKEN: ${{ secrets.V3 }} 34 | BRANCH: gh-pages 35 | FOLDER: dist 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | # Use the PNPM 25 | package-lock.json 26 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 UNPany 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 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | ## ❗ Note 2 | 3 | The project has been refactored with Vite since 3.1.3 and is open sourced here: [V3 Admin Vite](https://github.com/un-pany/v3-admin-vite), if Vue CLI is not required for you, then I recommend you use a new version based on Vite because it is faster, stronger, and more speciative! 4 | 5 | # ⚡️ Introduction 6 | 7 | A free and open source mid-back management system basic solution based on Vue3, TypeScript, Element Plus, Pinia and Vue CLI 5.x 8 | 9 | - Electron: [v3-electron](https://github.com/un-pany/v3-electron) 10 | 11 | ## Documentation 12 | 13 | [简体中文](https://juejin.cn/post/6963876125428678693) | English Docs 14 | 15 | ## preview 16 | 17 | | position | account | link | 18 | | ------------ | --------------- | ------------------------------------------ | 19 | | github-pages | admin or editor | [link](https://un-pany.github.io/v3-admin) | 20 | 21 | ## Features 22 | 23 | ```text 24 | - User management 25 | - login 26 | - logout 27 | 28 | - Permission Authentication 29 | - page permissions 30 | - directive permissions 31 | 32 | - Multi-environment build 33 | - development 34 | - test 35 | - production 36 | 37 | - Global Features 38 | - svg 39 | - Multiple themes switching(Contains dark themes) 40 | - Dynamic sidebar (supports multi-level routing) 41 | - Dynamic breadcrumb 42 | - Tags-view (Tab page Support right-click operation) 43 | - Screenfull 44 | - Responsive Sidebar 45 | - monitor(based on mitojs) 46 | 47 | - Error Page 48 | - 401 49 | - 404 50 | 51 | - Dashboard 52 | - admin 53 | - editor 54 | 55 | - Auto deployment 56 | ``` 57 | 58 | ## Directory 59 | 60 | ``` 61 | # v3-admin 62 | ├─ .env.development # development environment 63 | ├─ .env.production # production environment 64 | ├─ .env.test # test environment 65 | ├─ .eslintrc.js # eslint 66 | ├─ deploy # auto deployment 67 | ├─ public 68 | │ ├─ favicon.ico 69 | │ ├─ index.html 70 | ├─ src 71 | │ ├─ @types # ts declaration 72 | │ ├─ api # api interface 73 | │ ├─ assets # static resources 74 | │ ├─ components # global components 75 | │ ├─ config # global config 76 | │ ├─ constant # constant 77 | │ ├─ directives # global directives 78 | │ ├─ icons # svg icon 79 | │ ├─ layout # layout 80 | │ ├─ model # global model 81 | │ ├─ plugins # plugins 82 | │ ├─ router # router 83 | │ ├─ store # pinia store 84 | │ ├─ styles # global styles 85 | │ ├─ utils # utils 86 | │ └─ views # pages 87 | │ ├─ App.vue # entry page 88 | │ ├─ main.ts # entry file 89 | │ └─ shims.d.ts # module injection 90 | ├─ tsconfig.json # ts Compile config 91 | └─ vue.config.js # vue-cli config 92 | ``` 93 | 94 | ## Getting started 95 | 96 | ```bash 97 | # config 98 | 1. Install the 'eslint' plugin 99 | 2. Install the 'volar' plugin 100 | 3. node 16+ 101 | 4. pnpm 6+ 102 | 103 | # clone the project 104 | git clone https://github.com/un-pany/v3-admin.git 105 | 106 | # enter the project directory 107 | cd v3-admin 108 | 109 | # install dependency 110 | pnpm 111 | 112 | # develop 113 | pnpm dev 114 | ``` 115 | 116 | ## Multi-environment build 117 | 118 | ```bash 119 | # build test environment 120 | pnpm build:test 121 | 122 | # build production environment 123 | pnpm build:prod 124 | ``` 125 | 126 | ## Code format check 127 | 128 | ```bash 129 | pnpm lint 130 | ``` 131 | 132 | ## Auto deployment 133 | 134 | ```bash 135 | pnpm deploy 136 | ``` 137 | 138 | # 📚 Essentials 139 | 140 | ## Router 141 | 142 | ### Config items 143 | 144 | ``` 145 | // this route cannot be clicked in breadcrumb navigation when noRedirect is set 146 | redirect: 'noRedirect' 147 | 148 | // 'asyncRoutes': Be sure to fill in the name of the set route, otherwise there may be problems in resetting the route 149 | // If you want to show to 'tags-view', is required 150 | name: 'router-name' 151 | 152 | meta: { 153 | // Set the name of the route displayed in the sidebar and breadcrumbs 154 | title: 'title' 155 | // Icon to set this route, Remember to import svg into @/icons/svg 156 | icon: 'svg-name' 157 | // if set to true, lt will not appear in sidebar nav 158 | hidden: true 159 | // required roles to navigate to this route, Support multiple permissions stacking 160 | roles: ['admin', 'editor'] 161 | // The default is true. If it is set to false, it will not be displayed in breadcrumbs 162 | breadcrumb: false 163 | // The default is false. If set to true, it will be fixed in tags-view 164 | affix: true 165 | 166 | // When the children under a route declare more than one route, it will automatically become a nested mode 167 | // When there is only one, the sub route will be displayed in the sidebar as the root route 168 | // If you want to display your root route regardless of the number of children declarations below 169 | // You can set alwayShow: true, which will ignore the previously defined rules and always display the root route 170 | alwaysShow: true 171 | 172 | // When this property is set, the sidebar corresponding to the activemenu property will be highlighted when entering the route 173 | activeMenu: '/dashboard' 174 | } 175 | ``` 176 | 177 | ### Dynamic routes 178 | 179 | `constantRoutes` places routes that do not require judgment permission in the resident route 180 | 181 | `asyncRoutes` places routes that require dynamic permission judgment and are dynamically added through `addRoute` 182 | 183 | **Note: Dynamic routing must be configured with the name attribute, otherwise the routing will not be reset when the routing is reset, which may cause business bugs** 184 | 185 | ## Sidebar and breadcrumb 186 | 187 | ### Sidebar 188 | 189 | ![](https://ss.im5i.com/2021/10/20/yFV2O.png) 190 | 191 | Sidebar `@/layout/components/sidebar` is dynamically generated by reading routing and combining permission judgment (in other words, the constant route + has permission route) 192 | 193 | ### Sidebar external links 194 | 195 | You can set an outer chain in the sidebar, as long as you fill in the useful URL path in Path, you will help you open this page when you click on the sidebar. 196 | 197 | ```typescript 198 | { 199 | path: 'link', 200 | component: Layout, 201 | children: [ 202 | { 203 | path: 'https://github.com/un-pany/v3-admin', 204 | meta: { title: 'link', icon: 'link' }, 205 | name: 'Link' 206 | } 207 | ] 208 | } 209 | ``` 210 | 211 | ### Breadcrumb 212 | 213 | ![](https://ss.im5i.com/2021/10/20/yFdaR.png) 214 | 215 | Breadcrumb ` @/layout/components/bread-crumb` is also generated dynamically according to the route. The route setting `breadcrumb: false` will not appear in the breadcrumb. The route setting `redirect: 'noredirect'`cannot be clicked in the breadcrumb 216 | 217 | ## Permission 218 | 219 | When logging in, compare the routing table by obtaining the permissions (roles) of the current user, generate an accessible routing table according to the permissions of the current user, and then dynamically mount it to the router through `addRoute`. 220 | 221 | ### Role permission control 222 | 223 | The control codes are all in `@/router/permission.ts`, which can be modified according to specific business: 224 | 225 | ```typescript 226 | import NProgress from "nprogress" 227 | import "nprogress/nprogress.css" 228 | import router from "@/router" 229 | import { RouteLocationNormalized } from "vue-router" 230 | import { useUserStoreHook } from "@/store/modules/user" 231 | import { usePermissionStoreHook } from "@/store/modules/permission" 232 | import { ElMessage } from "element-plus" 233 | import { whiteList } from "@/config/white-list" 234 | import rolesSettings from "@/config/roles" 235 | import { getToken } from "@/utils/cookies" 236 | 237 | const userStore = useUserStoreHook() 238 | const permissionStore = usePermissionStoreHook() 239 | NProgress.configure({ showSpinner: false }) 240 | 241 | router.beforeEach(async (to: RouteLocationNormalized, _: RouteLocationNormalized, next: any) => { 242 | NProgress.start() 243 | // Determine if the user is logged in 244 | if (getToken()) { 245 | if (to.path === "/login") { 246 | // Redirect to the homepage if you log in and ready to enter the Login page. 247 | next({ path: "/" }) 248 | NProgress.done() 249 | } else { 250 | // Check if the user has obtained its permissions role 251 | if (userStore.roles.length === 0) { 252 | try { 253 | if (rolesSettings.openRoles) { 254 | // Note: The role must be an array! E.g: ['admin'] 或 ['developer', 'editor'] 255 | await userStore.getInfo() 256 | // Fetch the Roles returned by the interface 257 | const roles = userStore.roles 258 | // Generate accessible Routes based on roles 259 | permissionStore.setRoutes(roles) 260 | } else { 261 | // Enable the default role without turning on the role function 262 | userStore.setRoles(rolesSettings.defaultRoles) 263 | permissionStore.setRoutes(rolesSettings.defaultRoles) 264 | } 265 | // Dynamically add accessible Routes 266 | permissionStore.dynamicRoutes.forEach((route) => { 267 | router.addRoute(route) 268 | }) 269 | // Ensure that the added route has been completed 270 | // Set replace: true, so navigation will not leave a history 271 | next({ ...to, replace: true }) 272 | } catch (err: any) { 273 | // Delete token and redirect to the login page 274 | userStore.resetToken() 275 | ElMessage.error(err || "Has Error") 276 | next("/login") 277 | NProgress.done() 278 | } 279 | } else { 280 | next() 281 | } 282 | } 283 | } else { 284 | // If there is no TOKEN 285 | if (whiteList.indexOf(to.path) !== -1) { 286 | // If you are in a whitelist that you don't need to log in, you will enter directly. 287 | next() 288 | } else { 289 | // Other pages without access rights will be redirected to the login page 290 | next("/login") 291 | NProgress.done() 292 | } 293 | } 294 | }) 295 | 296 | router.afterEach(() => { 297 | NProgress.done() 298 | }) 299 | ``` 300 | 301 | ### Cancel the role feature 302 | 303 | If you don't need the function of role, you can turn it off in `@/config/roles`. After turning it off, the system will enable the default role (usually the admin role with the highest permission), that is, each logged in user can see all routes 304 | 305 | ```typescript 306 | interface RolesSettings { 307 | // Whether to enable the role function (After opening, the server needs to cooperate and return the role of the current user in the query user details interface) 308 | openRoles: boolean 309 | // After closing the role, the default role of the currently logged in user will take effect (admin by default, with all permissions) 310 | defaultRoles: Array 311 | } 312 | 313 | const rolesSettings: RolesSettings = { 314 | openRoles: true, 315 | defaultRoles: ["admin"], 316 | } 317 | 318 | export default rolesSettings 319 | ``` 320 | 321 | ### Directive permissions 322 | 323 | Concisely implement button level permission judgment (registered to the global and can be used directly): 324 | 325 | ```html 326 | admin is visible 327 | editor is visible 328 | admin and editor are visible 329 | ``` 330 | 331 | However, in some cases, `v-permission` is not suitable. For example: `el-tab` or `el-table-column` of ` element-plus` and other scenes that dynamically render `DOM`. You can only do this by manually setting `v-if`. 332 | 333 | At this time, you can use **permission judgment function**. 334 | 335 | ```typescript 336 | import { checkPermission } from "@/utils/permission" 337 | ``` 338 | 339 | ```html 340 | admin is visible 341 | editor id visible 342 | admin and editor are visible 343 | ``` 344 | 345 | ## Send HTTP request 346 | 347 | The general process is as follows: 348 | 349 | ![](https://ss.im5i.com/2021/10/20/yFlGd.png) 350 | 351 | ### Common management API 352 | 353 | `@/api/login.ts` 354 | 355 | ```typescript 356 | import { request } from "@/utils/service" 357 | 358 | interface UserRequestData { 359 | username: string 360 | password: string 361 | } 362 | 363 | export function accountLogin(data: UserRequestData) { 364 | return request({ 365 | url: "user/login", 366 | method: "post", 367 | data, 368 | }) 369 | } 370 | ``` 371 | 372 | ### Encapsulated service.ts 373 | 374 | `@/utils/service.ts ` is based on axios, which encapsulates request interceptor, response interceptor, unified error handling, unified timeout handling, baseURL setting, CancelToken, etc. 375 | 376 | ## Multi-environment 377 | 378 | ### Build 379 | 380 | When the project is developed and need build, there are two built-in environments: 381 | 382 | ```sh 383 | # build test environment 384 | pnpm build:test 385 | 386 | # build production environment 387 | pnpm build:prod 388 | ``` 389 | 390 | ### Variables 391 | 392 | In the `.env.xxx` and other files, the variables corresponding to the environment are configured: 393 | 394 | ```sh 395 | # Interface corresponding to current environment baseURL 396 | VUE_APP_BASE_API = 'https://www.xxx.com' 397 | ``` 398 | 399 | access: 400 | 401 | ```js 402 | console.log(process.env.VUE_APP_BASE_API) 403 | ``` 404 | 405 | # ✈️ Advanced 406 | 407 | ## ESLint 408 | 409 | Code specifications are important! 410 | 411 | - Config item:Set in the `.eslintrc.js` file 412 | - Cancel auto lint:Set `lintOnSave` to `false` in `vue.config.js` 413 | - The ESlint plug-in of VSCode is recommended here. When coding, it can mark the code that does not comply with the specification in red, and when you save the code, it will automatically help you repair some simple problematic code (VScode configuration ESlint tutorial can be found through Google) 414 | - Perform lint manually:`pnpm lint`(Execute this command before submitting the code, especially if your `lintOnSave` is `false`) 415 | 416 | ## Git Hooks 417 | 418 | gitHooks is configured in `package. json`, and the code will be detected every time you commit 419 | 420 | ```json 421 | "gitHooks": { 422 | "pre-commit": "lint-staged" 423 | }, 424 | "lint-staged": { 425 | "*.{js,jsx,vue,ts,tsx}": [ 426 | "vue-cli-service lint", 427 | "git add" 428 | ] 429 | } 430 | ``` 431 | 432 | ## Cross origin 433 | 434 | Use `proxy` in `vue.config` for reverse proxy. 435 | 436 | For the corresponding production environment, `nginx` can be used as the reverse proxy. 437 | 438 | ### Reverse proxy 439 | 440 | ```typescript 441 | proxy: { 442 | '/api/': { 443 | target: 'http://xxxxxx/api/', 444 | ws: true, 445 | pathRewrite: { 446 | '^/api/': '' 447 | }, 448 | changeOrigin: true, 449 | secure: false 450 | } 451 | } 452 | ``` 453 | 454 | ### CORS 455 | 456 | This scheme has nothing special to do for the front end. It is no different from ordinary sending requests in writing. Most of the workload is on the server. 457 | After completing [CORS](http://www.ruanyifeng.com/blog/2016/04/cors.html), you can easily call the interface in either the development environment or the production environment. 458 | 459 | ## SVG 460 | 461 | There are global `@/components/svg-icon` components, and the icons can be stored in `@/icons/svg`. 462 | 463 | ### Usage 464 | 465 | There is no need to import components into the page, which can be used directly. 466 | 467 | ```html 468 | 469 | 470 | 471 | ``` 472 | 473 | ### Get more icons 474 | 475 | Recommended use [iconfont](https://www.iconfont.cn/) 476 | 477 | ## Auto deployment 478 | 479 | Fill in the **server IP, port, username, password** and other information in the `deploy/index. JS` file, and then execute the `pnpm deploy` command to automatically publish dist file to the corresponding server. 480 | 481 | > Note: the username, password and other information in this file are sensitive information and shouldn't be uploaded to the remote repositories, which is very important! 482 | 483 | ## Add new theme(Take dark theme as an example) 484 | 485 | - New theme 486 | - `src/style/theme/dark/index.scss` 487 | - `src/style/theme/dark/setting.scss` 488 | - Register the new theme 489 | - `src/style/theme/register.scss` 490 | - `src/config/theme.ts` 491 | 492 | # ❓ Common problem 493 | 494 | ## All errors 495 | 496 | Google can solve 99% of error reports. 497 | 498 | ## Dependency error 499 | 500 | - Recommended use pnpm 501 | - Attempt to delete `node_modules` `.lock` and install again 502 | - Google search it 503 | 504 | ## When the routing mode is switched to browserhistory, a blank page appears after refreshing 505 | 506 | Change the value of `publicPath` in the ` @/config/vue.custom.config.ts` file from `./` to`/` 507 | 508 | # ☕ Other 509 | 510 | ## Standing on the shoulders of giants 511 | 512 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 513 | - [vue3-composition-admin](https://github.com/rcyj-FED/vue3-composition-admin) 514 | - [vue-vben-admin](https://github.com/anncwb/vue-vben-admin) 515 | - [d2-admin](https://github.com/d2-projects/d2-admin) 516 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) 517 | - [vue3-antd-admin](https://github.com/buqiyuan/vue3-antd-admin) 518 | 519 | # 📄 License 520 | 521 | [MIT](https://github.com/un-pany/v3-admin/blob/master/LICENSE) 522 | 523 | Copyright (c) 2021 UNPany 524 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ❗ 注意 2 | 3 | 该项目从 3.1.3 版本之后就用 Vite 进行了重构,并且开源在这里:[V3 Admin Vite](https://github.com/un-pany/v3-admin-vite),如果 Vue CLI 不是你的必选项,那么我建议你使用基于 Vite 的新版本,因为它更快、更强、更规范! 4 | 5 | ## ⚡️ 简介 6 | 7 | 一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、 Pinia 和 Vue CLI 5.x 8 | 9 | - Electron 桌面版: [v3-electron](https://github.com/un-pany/v3-electron) 10 | 11 | ## 📚 文档 12 | 13 | [简体中文](https://juejin.cn/post/6963876125428678693) | [English Docs](./README.en.md) 14 | 15 | ## 国内仓库 16 | 17 | [Gitee](https://gitee.com/un-pany/v3-admin) 18 | 19 | ## 预览 20 | 21 | | 位置 | 账号 | 链接 | 22 | | ------------ | --------------- | ------------------------------------------ | 23 | | github-pages | admin 或 editor | [链接](https://un-pany.github.io/v3-admin) | 24 | 25 | ## ⌛ 功能 26 | 27 | ```text 28 | - 用户管理 29 | - 登录 30 | - 注销 31 | 32 | - 权限验证 33 | - 页面权限 34 | - 指令权限 35 | 36 | - 多环境 37 | - development 38 | - test 39 | - production 40 | 41 | - 全局功能 42 | - svg 43 | - 多主题切换(内置黑暗主题) 44 | - 动态侧边栏 45 | - 动态面包屑 46 | - 标签页快捷导航 47 | - Screenfull 全屏 48 | - 自适应收缩侧边栏 49 | - 前端监控(基于 mitojs) 50 | 51 | - 错误页面 52 | - 401 53 | - 404 54 | 55 | - Dashboard 56 | - admin 57 | - editor 58 | 59 | - 自动部署 60 | ``` 61 | 62 | ## 目录结构 63 | 64 | ``` 65 | # v3-admin 66 | ├─ .env.development # 开发环境 67 | ├─ .env.production # 正式环境 68 | ├─ .env.test # 测试环境 69 | ├─ .eslintrc.js # eslint 70 | ├─ deploy # 自动部署 71 | ├─ public 72 | │ ├─ favicon.ico 73 | │ ├─ index.html 74 | ├─ src 75 | │ ├─ @types # ts 声明 76 | │ ├─ api # api 接口 77 | │ ├─ assets # 静态资源 78 | │ ├─ components # 全局组件 79 | │ ├─ config # 全局配置 80 | │ ├─ constant # 常量/枚举 81 | │ ├─ directives # 全局指令 82 | │ ├─ icons # svg icon 83 | │ ├─ layout # 布局 84 | │ ├─ model # 全局 model 85 | │ ├─ plugins # 插件 86 | │ ├─ router # 路由 87 | │ ├─ store # pinia store 88 | │ ├─ styles # 全局样式 89 | │ ├─ utils # 全局公共方法 90 | │ └─ views # 所有页面 91 | │ ├─ App.vue # 入口页面 92 | │ ├─ main.ts # 入口文件 93 | │ └─ shims.d.ts # 模块注入 94 | ├─ tsconfig.json # ts 编译配置 95 | └─ vue.config.js # vue-cli 配置 96 | ``` 97 | 98 | ## 🚀 开发 99 | 100 | ```bash 101 | # 配置 102 | 1. 安装 eslint 插件 103 | 2. 安装 volar 插件 104 | 3. node 16+ 105 | 4. pnpm 6+ 106 | 107 | # 克隆项目 108 | git clone https://github.com/un-pany/v3-admin.git 109 | 110 | # 进入项目目录 111 | cd v3-admin 112 | 113 | # 安装依赖 114 | pnpm i 115 | 116 | # 启动服务 117 | pnpm dev 118 | ``` 119 | 120 | ## 📦️ 多环境打包 121 | 122 | ```bash 123 | # 构建测试环境 124 | pnpm build:test 125 | 126 | # 构建生产环境 127 | pnpm build:prod 128 | ``` 129 | 130 | ## 🔧 代码格式检查 131 | 132 | ```bash 133 | pnpm lint 134 | ``` 135 | 136 | ## ✈️ 自动部署 137 | 138 | ```bash 139 | pnpm deploy 140 | ``` 141 | 142 | ## Git 提交规范 143 | 144 | - `feat` 增加新功能 145 | - `fix` 修复问题/BUG 146 | - `style` 代码风格相关无影响运行结果的 147 | - `perf` 优化/性能提升 148 | - `refactor` 重构 149 | - `revert` 撤销修改 150 | - `test` 测试相关 151 | - `docs` 文档/注释 152 | - `chore` 依赖更新/脚手架配置修改等 153 | - `workflow` 工作流改进 154 | - `ci` 持续集成 155 | - `types` 类型定义文件更改 156 | - `wip` 开发中 157 | - `mod` 不确定分类的修改 158 | 159 | ## 站在巨人的肩膀上 160 | 161 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 162 | - [vue3-composition-admin](https://github.com/rcyj-FED/vue3-composition-admin) 163 | - [vue-vben-admin](https://github.com/anncwb/vue-vben-admin) 164 | - [d2-admin](https://github.com/d2-projects/d2-admin) 165 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) 166 | - [vue3-antd-admin](https://github.com/buqiyuan/vue3-antd-admin) 167 | 168 | ## 交流群 169 | 170 | QQ 群:1014374415(左)&& 加我微信,拉你进微信群(右) 171 | 172 | ![qq.png](./src/assets/docs/qq.png) 173 | ![wechat.png](./src/assets/docs/wechat.png) 174 | 175 | ## 📄 License 176 | 177 | [MIT](https://github.com/un-pany/v3-admin/blob/master/LICENSE) 178 | 179 | Copyright (c) 2021 UNPany 180 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /deploy/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 第一步: pnpm run build 打包项目生成 dist 目录 3 | * 第二步: 准确填写下方账号密码 4 | * 第三步: pnpm deploy 触发该自动部署程序 5 | * 注意: 账号密码不能上传到 git 仓库,这涉及的账号安全问题,请在上传前删除账号密码,或者将该文件设置为忽略文件 6 | */ 7 | 8 | 'use strict' 9 | const scpClient = require('scp2') // 引入 scp2 10 | const ora = require('ora') 11 | const chalk = require('chalk') 12 | const spinner = ora('正在发布到服务器...') 13 | 14 | const Client = require('ssh2').Client 15 | const conn = new Client() 16 | 17 | const server = { 18 | host: '', // 服务器的 ip 地址 19 | port: '22', // 服务器端口, 一般为 22 20 | username: 'root', // 用户名 21 | password: '', // 密码 22 | path: '/var/www/html/', // 项目部署的服务器目标位置 23 | command: 'rm -rf /var/www/html/*' // 删除历史静态文件 24 | } 25 | 26 | conn.on('ready', () => { 27 | conn.exec(server.command, (err, stream) => { 28 | if (err) { throw err } 29 | stream.on('close', () => { 30 | spinner.start() 31 | scpClient.scp( 32 | 'dist/', // 本地打包文件的位置 33 | { 34 | host: server.host, 35 | port: server.port, 36 | username: server.username, 37 | password: server.password, 38 | path: server.path 39 | }, 40 | (err) => { 41 | spinner.stop() 42 | if (err) { 43 | console.log(chalk.red('发布失败!')) 44 | throw err 45 | } else { 46 | console.log(chalk.green('项目发布成功!')) 47 | } 48 | } 49 | ) 50 | conn.end() 51 | }).on('data', (data) => { 52 | console.log('STDOUT: ' + data) 53 | }).stderr.on('data', (data) => { 54 | console.log('STDERR: ' + data) 55 | }) 56 | }) 57 | }).connect({ 58 | host: server.host, 59 | port: server.port, 60 | username: server.username, 61 | password: server.password 62 | }) 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v3-admin", 3 | "version": "3.1.3", 4 | "homepage": "https://github.com/un-pany/v3-admin", 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "build:test": "vue-cli-service build --mode test", 8 | "build:prod": "vue-cli-service build", 9 | "lint": "vue-cli-service lint", 10 | "deploy": "node ./deploy" 11 | }, 12 | "dependencies": { 13 | "@element-plus/icons-vue": "1.1.4", 14 | "@mitojs/browser": "3.0.1", 15 | "@mitojs/vue": "3.0.1", 16 | "axios": "0.26.1", 17 | "dayjs": "1.11.1", 18 | "element-plus": "2.1.10", 19 | "js-cookie": "3.0.1", 20 | "lodash-es": "4.17.21", 21 | "normalize.css": "8.0.1", 22 | "nprogress": "0.2.0", 23 | "path-to-regexp": "6.2.0", 24 | "pinia": "2.0.13", 25 | "screenfull": "6.0.1", 26 | "vue": "3.2.33", 27 | "vue-router": "4.0.14" 28 | }, 29 | "devDependencies": { 30 | "@types/js-cookie": "3.0.1", 31 | "@types/lodash-es": "4.17.6", 32 | "@types/nprogress": "0.2.0", 33 | "@typescript-eslint/eslint-plugin": "5.20.0", 34 | "@typescript-eslint/parser": "5.20.0", 35 | "@vue/cli-plugin-babel": "5.0.4", 36 | "@vue/cli-plugin-eslint": "5.0.4", 37 | "@vue/cli-plugin-router": "5.0.4", 38 | "@vue/cli-plugin-typescript": "5.0.4", 39 | "@vue/cli-plugin-vuex": "5.0.4", 40 | "@vue/cli-service": "5.0.4", 41 | "@vue/eslint-config-standard": "6.1.0", 42 | "@vue/eslint-config-typescript": "10.0.0", 43 | "core-js": "3.22.0", 44 | "eslint": "8.13.0", 45 | "eslint-plugin-import": "2.26.0", 46 | "eslint-plugin-node": "11.1.0", 47 | "eslint-plugin-promise": "6.0.0", 48 | "eslint-plugin-vue": "8.6.0", 49 | "lint-staged": "12.3.8", 50 | "path-browserify": "1.0.1", 51 | "sass": "1.50.1", 52 | "sass-loader": "12.6.0", 53 | "scp2": "0.5.0", 54 | "svg-sprite-loader": "6.0.11", 55 | "terser-webpack-plugin": "5.3.1", 56 | "typescript": "4.6.3", 57 | "webpackbar": "5.0.2", 58 | "yorkie": "2.0.0" 59 | }, 60 | "gitHooks": { 61 | "pre-commit": "lint-staged" 62 | }, 63 | "lint-staged": { 64 | "*.{js,jsx,vue,ts,tsx}": [ 65 | "vue-cli-service lint", 66 | "git add" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= webpackConfig.name %> 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 项目类型声明 */ 2 | declare module '*.svg' 3 | declare module '*.png' 4 | declare module '*.jpg' 5 | declare module '*.jpeg' 6 | declare module '*.gif' 7 | declare module '*.bmp' 8 | declare module '*.tiff' 9 | declare module '*.yaml' 10 | declare module '*.json' 11 | -------------------------------------------------------------------------------- /src/@types/vue-proptery.d.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | 3 | declare module '@vue/runtime-core' { 4 | interface ComponentCustomProperties { 5 | $message: ElMessage 6 | } 7 | } 8 | 9 | declare module 'vue-router' { 10 | interface RouteMeta { 11 | roles?: string[] 12 | activeMenu?: string 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/api/login.ts: -------------------------------------------------------------------------------- 1 | import { request } from '@/utils/service' 2 | 3 | interface IUserRequestData { 4 | username: string 5 | password: string 6 | } 7 | 8 | /** 登录,返回 token */ 9 | export function accountLogin(data: IUserRequestData) { 10 | return request({ 11 | url: 'users/login', 12 | method: 'post', 13 | data 14 | }) 15 | } 16 | /** 获取用户详情 */ 17 | export function userInfoRequest() { 18 | return request({ 19 | url: 'users/info', 20 | method: 'post' 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/docs/qq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/docs/qq.png -------------------------------------------------------------------------------- /src/assets/docs/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/docs/wechat.png -------------------------------------------------------------------------------- /src/assets/layout/discard1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/layout/discard1.png -------------------------------------------------------------------------------- /src/assets/layout/discard2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/layout/discard2.png -------------------------------------------------------------------------------- /src/assets/layout/discard3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/layout/discard3.png -------------------------------------------------------------------------------- /src/assets/layout/logo-text-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/layout/logo-text-1.png -------------------------------------------------------------------------------- /src/assets/layout/logo-text-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/layout/logo-text-2.png -------------------------------------------------------------------------------- /src/assets/layout/logo-text-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/layout/logo-text-3.png -------------------------------------------------------------------------------- /src/assets/layout/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un-pany/v3-admin/4b378e24a87f71b9e06b2c30df5897a7aecfc235/src/assets/layout/logo.png -------------------------------------------------------------------------------- /src/assets/layout/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /src/components/svg-icon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /src/components/theme-switch/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 39 | -------------------------------------------------------------------------------- /src/config/layout.ts: -------------------------------------------------------------------------------- 1 | /** 布局配置 */ 2 | interface ILayoutSettings { 3 | /** 控制 settings panel 显示 */ 4 | showSettings: boolean 5 | /** 控制 tagsview 显示 */ 6 | showTagsView: boolean 7 | /** 控制 siderbar logo 显示 */ 8 | showSidebarLogo: boolean 9 | /** 如果为真,将固定 header */ 10 | fixedHeader: boolean 11 | /** 控制 换肤按钮 显示 */ 12 | showThemeSwitch: boolean 13 | /** 控制 全屏按钮 显示 */ 14 | showScreenfull: boolean 15 | } 16 | 17 | const layoutSettings: ILayoutSettings = { 18 | showSettings: true, 19 | showTagsView: true, 20 | fixedHeader: false, 21 | showSidebarLogo: true, 22 | showThemeSwitch: true, 23 | showScreenfull: true 24 | } 25 | 26 | export default layoutSettings 27 | -------------------------------------------------------------------------------- /src/config/roles.ts: -------------------------------------------------------------------------------- 1 | /** 角色配置 */ 2 | interface IRolesSettings { 3 | /** 是否开启角色功能(开启后需要后端配合,在查询用户详情接口返回当前用户的所属角色) */ 4 | openRoles: boolean 5 | /** 当角色功能关闭时,当前登录用户的默认角色将生效(默认为admin,拥有所有权限) */ 6 | defaultRoles: Array 7 | } 8 | 9 | const rolesSettings: IRolesSettings = { 10 | openRoles: true, 11 | defaultRoles: ['admin'] 12 | } 13 | 14 | export default rolesSettings 15 | -------------------------------------------------------------------------------- /src/config/theme.ts: -------------------------------------------------------------------------------- 1 | /** 注册的主题 */ 2 | const themeList = [ 3 | { 4 | title: '默认', 5 | name: 'normal' 6 | }, { 7 | title: '黑暗', 8 | name: 'dark' 9 | } 10 | ] 11 | 12 | export default themeList 13 | -------------------------------------------------------------------------------- /src/config/white-list.ts: -------------------------------------------------------------------------------- 1 | /** 免登录白名单 */ 2 | const whiteList = ['/login'] 3 | 4 | export { whiteList } 5 | -------------------------------------------------------------------------------- /src/constant/key.ts: -------------------------------------------------------------------------------- 1 | class Keys { 2 | static sidebarStatus = 'v3-admin-sidebar-status-key' 3 | static language = 'v3-admin-language-key' 4 | static token = 'v3-admin-token-key' 5 | static activeThemeName = 'v3-admin-active-theme-name' 6 | } 7 | 8 | export default Keys 9 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './permission' 2 | -------------------------------------------------------------------------------- /src/directives/permission/index.ts: -------------------------------------------------------------------------------- 1 | import { useUserStoreHook } from '@/store/modules/user' 2 | import { Directive } from 'vue' 3 | 4 | /** 权限指令 */ 5 | export const permission: Directive = { 6 | mounted(el, binding) { 7 | const { value } = binding 8 | const roles = useUserStoreHook().roles 9 | if (value && value instanceof Array && value.length > 0) { 10 | const permissionRoles = value 11 | const hasPermission = roles.some((role: any) => { 12 | return permissionRoles.includes(role) 13 | }) 14 | if (!hasPermission) { 15 | el.style.display = 'none' 16 | } 17 | } else { 18 | throw new Error("need roles! Like v-permission=\"['admin','editor']\"") 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/icons/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import SvgIcon from '@/components/svg-icon/index.vue' // svg component 3 | 4 | const requireAll = function(requireContext: any) { 5 | return requireContext.keys().map(requireContext) 6 | } 7 | const req = require['context']('./svg', false, /\.svg$/) 8 | 9 | requireAll(req) 10 | 11 | export default function(app: ReturnType) { 12 | app.component('SvgIcon', SvgIcon) 13 | } 14 | -------------------------------------------------------------------------------- /src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/app-main.vue: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 23 | 24 | 49 | -------------------------------------------------------------------------------- /src/layout/components/bread-crumb/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 71 | 72 | 89 | -------------------------------------------------------------------------------- /src/layout/components/hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /src/layout/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from './app-main.vue' 2 | export { default as NavigationBar } from './navigation-bar/index.vue' 3 | export { default as Settings } from './settings/index.vue' 4 | export { default as Sidebar } from './sidebar/index.vue' 5 | export { default as TagsView } from './tags-view/index.vue' 6 | export { default as RightPanel } from './right-panel/index.vue' 7 | -------------------------------------------------------------------------------- /src/layout/components/navigation-bar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | 72 | 73 | 103 | -------------------------------------------------------------------------------- /src/layout/components/right-panel/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 26 | 27 | 46 | -------------------------------------------------------------------------------- /src/layout/components/settings/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 96 | 97 | 119 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 58 | 59 | 82 | 83 | 144 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/sidebar-item-link.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/sidebar-item.vue: -------------------------------------------------------------------------------- 1 | 2 | 38 | 39 | 104 | 105 | 125 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/sidebar-logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 24 | 25 | 79 | -------------------------------------------------------------------------------- /src/layout/components/tags-view/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 235 | 236 | 313 | -------------------------------------------------------------------------------- /src/layout/components/tags-view/scroll-pane.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 69 | 70 | 164 | -------------------------------------------------------------------------------- /src/layout/useResize.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore, DeviceType } from '@/store/modules/app' 2 | import { computed, watch } from 'vue' 3 | import { useRoute } from 'vue-router' 4 | 5 | /** 参考 Bootstrap 的响应式设计 width = 992 */ 6 | const WIDTH = 992 7 | 8 | /** 根据大小变化重新布局 */ 9 | export default function() { 10 | const appStore = useAppStore() 11 | 12 | const device = computed(() => { 13 | return appStore.device 14 | }) 15 | 16 | const sidebar = computed(() => { 17 | return appStore.sidebar 18 | }) 19 | 20 | const currentRoute = useRoute() 21 | 22 | const watchRouter = watch( 23 | () => currentRoute.name, 24 | () => { 25 | if (appStore.device === DeviceType.Mobile && appStore.sidebar.opened) { 26 | appStore.closeSidebar(false) 27 | } 28 | } 29 | ) 30 | 31 | const isMobile = () => { 32 | const rect = document.body.getBoundingClientRect() 33 | return rect.width - 1 < WIDTH 34 | } 35 | 36 | const resizeMounted = () => { 37 | if (isMobile()) { 38 | appStore.toggleDevice(DeviceType.Mobile) 39 | appStore.closeSidebar(true) 40 | } 41 | } 42 | 43 | const resizeHandler = () => { 44 | if (!document.hidden) { 45 | appStore.toggleDevice( 46 | isMobile() ? DeviceType.Mobile : DeviceType.Desktop 47 | ) 48 | if (isMobile()) { 49 | appStore.closeSidebar(true) 50 | } 51 | } 52 | } 53 | 54 | const addEventListenerOnResize = () => { 55 | window.addEventListener('resize', resizeHandler) 56 | } 57 | 58 | const removeEventListenerResize = () => { 59 | window.removeEventListener('resize', resizeHandler) 60 | } 61 | 62 | return { 63 | device, 64 | sidebar, 65 | resizeMounted, 66 | addEventListenerOnResize, 67 | removeEventListenerResize, 68 | watchRouter 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, Directive } from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | import router from './router' 5 | import { loadAllPlugins } from './plugins' 6 | import '@/styles/index.scss' 7 | import 'normalize.css' 8 | import * as directives from '@/directives' 9 | import '@/router/permission' 10 | import loadSvg from '@/icons' 11 | 12 | const app = createApp(App) 13 | // 加载所有插件 14 | loadAllPlugins(app) 15 | // 加载全局 SVG 16 | loadSvg(app) 17 | // 自定义指令 18 | Object.keys(directives).forEach((key) => { 19 | app.directive(key, (directives as { [key: string]: Directive })[key]) 20 | }) 21 | 22 | app.use(store).use(router).mount('#app') 23 | -------------------------------------------------------------------------------- /src/model/demo.ts: -------------------------------------------------------------------------------- 1 | /** 存放 "后端接口返回的数据" 的类型、及其他的一些 model */ 2 | 3 | export interface IDemoModel { 4 | id: number 5 | title: string 6 | } 7 | -------------------------------------------------------------------------------- /src/plugins/element.ts: -------------------------------------------------------------------------------- 1 | import ElementPlus from 'element-plus' 2 | import 'element-plus/dist/index.css' 3 | 4 | /** element-plus 组件 */ 5 | export default function loadComponent(app: any) { 6 | app.use(ElementPlus) 7 | } 8 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | /** 加载插件文件 */ 4 | export function loadAllPlugins(app: ReturnType) { 5 | const files = require['context']('.', true, /\.ts$/) 6 | files.keys().forEach((key: any) => { 7 | if (typeof files(key).default === 'function') { 8 | if (key !== './index.ts') files(key).default(app) 9 | } 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/monitor.ts: -------------------------------------------------------------------------------- 1 | import { init } from '@mitojs/browser' 2 | import { vuePlugin } from '@mitojs/vue' 3 | 4 | /** 监控插件 */ 5 | export default function loadComponent(app: any) { 6 | const MitoInstance = init( 7 | { 8 | vue: app, // vue 的根实例 9 | apikey: 'v3-admin', // 每个项目都应该有一个唯一值 10 | dsn: '/api/v1/monitor/upload', // 上报到服务端的 url 11 | maxBreadcrumbs: 100, // 默认值是20,如果设置的值大于100,将用100取代。表示最大用户行为栈的长度 12 | debug: false, // 当为 true 时将会在控制台打印收集到的数据,建议只在开发环境开启 13 | silentConsole: true // 默认会监控 console,为 true 时,将不再监控 14 | }, 15 | [vuePlugin as any] 16 | ); 17 | (window as any).MitoInstance = MitoInstance 18 | } 19 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | const Layout = () => import('@/layout/index.vue') 3 | 4 | /** 常驻路由 */ 5 | export const constantRoutes: Array = [ 6 | { 7 | path: '/redirect', 8 | component: Layout, 9 | meta: { 10 | hidden: true 11 | }, 12 | children: [ 13 | { 14 | path: '/redirect/:path(.*)', 15 | component: () => import('@/views/redirect/index.vue') 16 | } 17 | ] 18 | }, 19 | { 20 | path: '/login', 21 | component: () => import('@/views/login/index.vue'), 22 | meta: { 23 | hidden: true 24 | } 25 | }, 26 | { 27 | path: '/', 28 | component: Layout, 29 | redirect: '/dashboard', 30 | children: [ 31 | { 32 | path: 'dashboard', 33 | component: () => import('@/views/dashboard/index.vue'), 34 | name: 'Dashboard', 35 | meta: { 36 | title: '首页', 37 | icon: 'dashboard', 38 | affix: true 39 | } 40 | } 41 | ] 42 | }, 43 | { 44 | path: '/monitor', 45 | component: Layout, 46 | redirect: '/monitor/index', 47 | children: [ 48 | { 49 | path: 'index', 50 | component: () => import('@/views/monitor/index.vue'), 51 | name: 'Monitor', 52 | meta: { 53 | title: '监控', 54 | icon: 'bug' 55 | } 56 | } 57 | ] 58 | } 59 | ] 60 | 61 | /** 62 | * 动态路由 63 | * 用来放置有权限的路由 64 | * 必须带有 name 属性 65 | */ 66 | export const asyncRoutes: Array = [ 67 | { 68 | path: '/permission', 69 | component: Layout, 70 | redirect: '/permission/page', 71 | name: 'Permission', 72 | meta: { 73 | title: '权限管理', 74 | icon: 'lock', 75 | roles: ['admin', 'editor'], // 可以在根路由中设置角色 76 | alwaysShow: true // 将始终显示根菜单 77 | }, 78 | children: [ 79 | { 80 | path: 'page', 81 | component: () => import('@/views/permission/page.vue'), 82 | name: 'PagePermission', 83 | meta: { 84 | title: '页面权限', 85 | roles: ['admin'] // 或者在子导航中设置角色 86 | } 87 | }, 88 | { 89 | path: 'directive', 90 | component: () => import('@/views/permission/directive.vue'), 91 | name: 'DirectivePermission', 92 | meta: { 93 | title: '指令权限' // 如果未设置角色,则表示:该页面不需要权限,但会继承根路由的角色 94 | } 95 | } 96 | ] 97 | }, 98 | { 99 | path: '/:pathMatch(.*)*', // 必须将 'ErrorPage' 路由放在最后, Must put the 'ErrorPage' route at the end 100 | component: Layout, 101 | redirect: '/404', 102 | name: 'ErrorPage', 103 | meta: { 104 | title: '错误页面', 105 | icon: '404', 106 | hidden: true 107 | }, 108 | children: [ 109 | { 110 | path: '401', 111 | component: () => import('@/views/error-page/401.vue'), 112 | name: '401', 113 | meta: { 114 | title: '401' 115 | } 116 | }, 117 | { 118 | path: '404', 119 | component: () => import('@/views/error-page/404.vue'), 120 | name: '404', 121 | meta: { 122 | title: '404' 123 | } 124 | } 125 | ] 126 | } 127 | ] 128 | 129 | const router = createRouter({ 130 | history: createWebHashHistory(), 131 | routes: constantRoutes 132 | }) 133 | 134 | /** 重置路由 */ 135 | export function resetRouter() { 136 | // 注意:所有动态路由路由必须带有 name 属性,否则可能会不能完全重置干净 137 | try { 138 | router.getRoutes().forEach((route) => { 139 | const { name, meta } = route 140 | if (name && meta.roles?.length) { 141 | router.hasRoute(name) && router.removeRoute(name) 142 | } 143 | }) 144 | } catch (error) { 145 | // 强制刷新浏览器,不过体验不是很好 146 | window.location.reload() 147 | } 148 | } 149 | 150 | export default router 151 | -------------------------------------------------------------------------------- /src/router/permission.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | import 'nprogress/nprogress.css' 3 | import router from '@/router' 4 | import { RouteLocationNormalized } from 'vue-router' 5 | import { useUserStoreHook } from '@/store/modules/user' 6 | import { usePermissionStoreHook } from '@/store/modules/permission' 7 | import { ElMessage } from 'element-plus' 8 | import { whiteList } from '@/config/white-list' 9 | import rolesSettings from '@/config/roles' 10 | import { getToken } from '@/utils/cookies' 11 | 12 | NProgress.configure({ showSpinner: false }) 13 | 14 | router.beforeEach(async(to: RouteLocationNormalized, _: RouteLocationNormalized, next: any) => { 15 | NProgress.start() 16 | const userStore = useUserStoreHook() 17 | const permissionStore = usePermissionStoreHook() 18 | // 判断该用户是否登录 19 | if (getToken()) { 20 | if (to.path === '/login') { 21 | // 如果登录,并准备进入 login 页面,则重定向到主页 22 | next({ path: '/' }) 23 | NProgress.done() 24 | } else { 25 | // 检查用户是否已获得其权限角色 26 | if (userStore.roles.length === 0) { 27 | try { 28 | if (rolesSettings.openRoles) { 29 | // 注意:角色必须是一个数组! 例如: ['admin'] 或 ['developer', 'editor'] 30 | await userStore.getInfo() 31 | // 获取接口返回的 roles 32 | const roles = userStore.roles 33 | // 根据角色生成可访问的 routes 34 | permissionStore.setRoutes(roles) 35 | } else { 36 | // 没有开启角色功能,则启用默认角色 37 | userStore.setRoles(rolesSettings.defaultRoles) 38 | permissionStore.setRoutes(rolesSettings.defaultRoles) 39 | } 40 | // 动态地添加可访问的 routes 41 | permissionStore.dynamicRoutes.forEach((route) => { 42 | router.addRoute(route) 43 | }) 44 | // 确保添加路由已完成 45 | // 设置 replace: true, 因此导航将不会留下历史记录 46 | next({ ...to, replace: true }) 47 | } catch (err: any) { 48 | // 删除 token,并重定向到登录页面 49 | userStore.resetToken() 50 | ElMessage.error(err.message || 'Has Error') 51 | next('/login') 52 | NProgress.done() 53 | } 54 | } else { 55 | next() 56 | } 57 | } 58 | } else { 59 | // 如果没有 token 60 | if (whiteList.indexOf(to.path) !== -1) { 61 | // 如果在免登录的白名单中,则直接进入 62 | next() 63 | } else { 64 | // 其他没有访问权限的页面将被重定向到登录页面 65 | next('/login') 66 | NProgress.done() 67 | } 68 | } 69 | }) 70 | 71 | router.afterEach(() => { 72 | NProgress.done() 73 | }) 74 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | 7 | declare module '*.gif' { 8 | export const gif: any 9 | } 10 | 11 | declare module '*.svg' { 12 | const content: any 13 | export default content 14 | } 15 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const store = createPinia() 4 | 5 | export default store 6 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { 3 | getSidebarStatus, 4 | getActiveThemeName, 5 | setSidebarStatus, 6 | setActiveThemeName 7 | } from '@/utils/cookies' 8 | import themeList from '@/config/theme' 9 | 10 | export enum DeviceType { 11 | Mobile, 12 | Desktop, 13 | } 14 | 15 | interface IAppState { 16 | device: DeviceType 17 | sidebar: { 18 | opened: boolean 19 | withoutAnimation: boolean 20 | } 21 | /** 主题列表 */ 22 | themeList: { title: string, name: string }[] 23 | /** 正在应用的主题的名字 */ 24 | activeThemeName: string 25 | } 26 | 27 | export const useAppStore = defineStore({ 28 | id: 'app', 29 | state: (): IAppState => { 30 | return { 31 | device: DeviceType.Desktop, 32 | sidebar: { 33 | opened: getSidebarStatus() !== 'closed', 34 | withoutAnimation: false 35 | }, 36 | themeList: themeList, 37 | activeThemeName: getActiveThemeName() || 'normal' 38 | } 39 | }, 40 | actions: { 41 | toggleSidebar(withoutAnimation: boolean) { 42 | this.sidebar.opened = !this.sidebar.opened 43 | this.sidebar.withoutAnimation = withoutAnimation 44 | if (this.sidebar.opened) { 45 | setSidebarStatus('opened') 46 | } else { 47 | setSidebarStatus('closed') 48 | } 49 | }, 50 | closeSidebar(withoutAnimation: boolean) { 51 | this.sidebar.opened = false 52 | this.sidebar.withoutAnimation = withoutAnimation 53 | setSidebarStatus('closed') 54 | }, 55 | toggleDevice(device: DeviceType) { 56 | this.device = device 57 | }, 58 | setTheme(activeThemeName: string) { 59 | // 检查这个主题在主题列表里是否存在 60 | this.activeThemeName = this.themeList.find( 61 | (theme) => theme.name === activeThemeName 62 | ) 63 | ? activeThemeName 64 | : this.themeList[0].name 65 | // 应用到 dom 66 | document.body.className = `theme-${this.activeThemeName}` 67 | // 持久化 68 | setActiveThemeName(this.activeThemeName) 69 | }, 70 | initTheme() { 71 | // 初始化 72 | document.body.className = `theme-${this.activeThemeName}` 73 | } 74 | } 75 | }) 76 | -------------------------------------------------------------------------------- /src/store/modules/permission.ts: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import { defineStore } from 'pinia' 3 | import { RouteRecordRaw } from 'vue-router' 4 | import { constantRoutes, asyncRoutes } from '@/router' 5 | 6 | interface IPermissionState { 7 | routes: RouteRecordRaw[] 8 | dynamicRoutes: RouteRecordRaw[] 9 | } 10 | 11 | const hasPermission = (roles: string[], route: RouteRecordRaw) => { 12 | if (route.meta && route.meta.roles) { 13 | return roles.some((role) => { 14 | if (route.meta?.roles !== undefined) { 15 | return route.meta.roles.includes(role) 16 | } else { 17 | return false 18 | } 19 | }) 20 | } else { 21 | return true 22 | } 23 | } 24 | 25 | const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => { 26 | const res: RouteRecordRaw[] = [] 27 | routes.forEach((route) => { 28 | const r = { ...route } 29 | if (hasPermission(roles, r)) { 30 | if (r.children) { 31 | r.children = filterAsyncRoutes(r.children, roles) 32 | } 33 | res.push(r) 34 | } 35 | }) 36 | return res 37 | } 38 | 39 | export const usePermissionStore = defineStore({ 40 | id: 'permission', 41 | state: (): IPermissionState => { 42 | return { 43 | routes: [], 44 | dynamicRoutes: [] 45 | } 46 | }, 47 | actions: { 48 | setRoutes(roles: string[]) { 49 | let accessedRoutes 50 | if (roles.includes('admin')) { 51 | accessedRoutes = asyncRoutes 52 | } else { 53 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) 54 | } 55 | this.routes = constantRoutes.concat(accessedRoutes) 56 | this.dynamicRoutes = accessedRoutes 57 | } 58 | } 59 | }) 60 | 61 | /** 在 setup 外使用 */ 62 | export function usePermissionStoreHook() { 63 | return usePermissionStore(store) 64 | } 65 | -------------------------------------------------------------------------------- /src/store/modules/settings.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import layoutSettings from '@/config/layout' 3 | 4 | interface ISettingsState { 5 | fixedHeader: boolean 6 | showSettings: boolean 7 | showTagsView: boolean 8 | showSidebarLogo: boolean 9 | showThemeSwitch: boolean 10 | showScreenfull: boolean 11 | } 12 | 13 | export const useSettingsStore = defineStore({ 14 | id: 'settings', 15 | state: (): ISettingsState => { 16 | return { 17 | fixedHeader: layoutSettings.fixedHeader, 18 | showSettings: layoutSettings.showSettings, 19 | showTagsView: layoutSettings.showTagsView, 20 | showSidebarLogo: layoutSettings.showSidebarLogo, 21 | showThemeSwitch: layoutSettings.showThemeSwitch, 22 | showScreenfull: layoutSettings.showScreenfull 23 | } 24 | }, 25 | actions: { 26 | changeSetting(payload: { key: string, value: any }) { 27 | const { key, value } = payload 28 | switch (key) { 29 | case 'fixedHeader': 30 | this.fixedHeader = value 31 | break 32 | case 'showSettings': 33 | this.showSettings = value 34 | break 35 | case 'showSidebarLogo': 36 | this.showSidebarLogo = value 37 | break 38 | case 'showTagsView': 39 | this.showTagsView = value 40 | break 41 | case 'showThemeSwitch': 42 | this.showThemeSwitch = value 43 | break 44 | case 'showScreenfull': 45 | this.showScreenfull = value 46 | break 47 | default: 48 | break 49 | } 50 | } 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /src/store/modules/tags-view.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { _RouteLocationBase, RouteLocationNormalized } from 'vue-router' 3 | 4 | export interface ITagView extends Partial { 5 | title?: string 6 | to?: _RouteLocationBase 7 | } 8 | 9 | interface ITagsViewState { 10 | visitedViews: ITagView[] 11 | } 12 | 13 | export const useTagsViewStore = defineStore({ 14 | id: 'tags-view', 15 | state: (): ITagsViewState => { 16 | return { 17 | visitedViews: [] 18 | } 19 | }, 20 | actions: { 21 | addVisitedView(view: ITagView) { 22 | if (this.visitedViews.some((v) => v.path === view.path)) return 23 | this.visitedViews.push( 24 | Object.assign({}, view, { 25 | title: view.meta?.title || 'no-name' 26 | }) 27 | ) 28 | }, 29 | delVisitedView(view: ITagView) { 30 | for (const [i, v] of this.visitedViews.entries()) { 31 | if (v.path === view.path) { 32 | this.visitedViews.splice(i, 1) 33 | break 34 | } 35 | } 36 | }, 37 | delOthersVisitedViews(view: ITagView) { 38 | this.visitedViews = this.visitedViews.filter((v) => { 39 | return v.meta?.affix || v.path === view.path 40 | }) 41 | }, 42 | delAllVisitedViews() { 43 | // keep affix tags 44 | const affixTags = this.visitedViews.filter((tag) => tag.meta?.affix) 45 | this.visitedViews = affixTags 46 | }, 47 | updateVisitedView(view: ITagView) { 48 | for (let v of this.visitedViews) { 49 | if (v.path === view.path) { 50 | v = Object.assign(v, view) 51 | break 52 | } 53 | } 54 | } 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import { defineStore } from 'pinia' 3 | import { usePermissionStore } from './permission' 4 | import { getToken, removeToken, setToken } from '@/utils/cookies' 5 | import router, { resetRouter } from '@/router' 6 | import { accountLogin, userInfoRequest } from '@/api/login' 7 | import { RouteRecordRaw } from 'vue-router' 8 | 9 | interface IUserState { 10 | token: string 11 | roles: string[] 12 | } 13 | 14 | export const useUserStore = defineStore({ 15 | id: 'user', 16 | state: (): IUserState => { 17 | return { 18 | token: getToken() || '', 19 | roles: [] 20 | } 21 | }, 22 | actions: { 23 | /** 设置角色数组 */ 24 | setRoles(roles: string[]) { 25 | this.roles = roles 26 | }, 27 | /** 登录 */ 28 | login(userInfo: { username: string, password: string }) { 29 | return new Promise((resolve, reject) => { 30 | accountLogin({ 31 | username: userInfo.username.trim(), 32 | password: userInfo.password 33 | }) 34 | .then((res: any) => { 35 | setToken(res.data.accessToken) 36 | this.token = res.data.accessToken 37 | resolve(true) 38 | }) 39 | .catch((error) => { 40 | reject(error) 41 | }) 42 | }) 43 | }, 44 | /** 获取用户详情 */ 45 | getInfo() { 46 | return new Promise((resolve, reject) => { 47 | userInfoRequest() 48 | .then((res: any) => { 49 | this.roles = res.data.user.roles 50 | resolve(res) 51 | }) 52 | .catch((error) => { 53 | reject(error) 54 | }) 55 | }) 56 | }, 57 | /** 切换角色 */ 58 | async changeRoles(role: string) { 59 | const token = role + '-token' 60 | this.token = token 61 | setToken(token) 62 | await this.getInfo() 63 | const permissionStore = usePermissionStore() 64 | permissionStore.setRoutes(this.roles) 65 | resetRouter() 66 | permissionStore.dynamicRoutes.forEach((item: RouteRecordRaw) => { 67 | router.addRoute(item) 68 | }) 69 | }, 70 | /** 登出 */ 71 | logout() { 72 | removeToken() 73 | this.token = '' 74 | this.roles = [] 75 | resetRouter() 76 | }, 77 | /** 重置 token */ 78 | resetToken() { 79 | removeToken() 80 | this.token = '' 81 | this.roles = [] 82 | } 83 | } 84 | }) 85 | 86 | /** 在 setup 外使用 */ 87 | export function useUserStoreHook() { 88 | return useUserStore(store) 89 | } 90 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "./mixins.scss"; // mixins 2 | @import "./transition.scss"; // transition 3 | @import "./theme/register.scss"; // 注册主题 4 | 5 | .app-container { 6 | padding: 20px; 7 | } 8 | 9 | html { 10 | height: 100%; 11 | } 12 | 13 | body { 14 | height: 100%; 15 | background-color: #f0f2f5; // 全局背景色 16 | -moz-osx-font-smoothing: grayscale; 17 | -webkit-font-smoothing: antialiased; 18 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", 19 | "Microsoft YaHei", "微软雅黑", Arial, sans-serif; 20 | } 21 | 22 | #app { 23 | height: 100%; 24 | } 25 | 26 | *, 27 | *:before, 28 | *:after { 29 | box-sizing: border-box; 30 | } 31 | 32 | a, 33 | a:focus, 34 | a:hover { 35 | color: inherit; 36 | outline: none; 37 | text-decoration: none; 38 | } 39 | 40 | div:focus { 41 | outline: none; 42 | } 43 | -------------------------------------------------------------------------------- /src/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/theme/dark/index.scss: -------------------------------------------------------------------------------- 1 | @import './setting.scss'; 2 | @import '../theme.scss'; 3 | -------------------------------------------------------------------------------- /src/styles/theme/dark/setting.scss: -------------------------------------------------------------------------------- 1 | // 主题名称 2 | $theme-name: "dark"; 3 | // 主题背景颜色 4 | $theme-bg-color: #151515; 5 | // active 状态下主题背景颜色 6 | $active-theme-bg-color: #409eff; 7 | // 默认文字颜色 8 | $font-color: #c0c4cc; 9 | // active 状态下文字颜色 10 | $active-font-color: #fff; 11 | // hover 状态下文字颜色 12 | $hover-color: #fff; 13 | // 边框颜色 14 | $border-color: #303133; 15 | -------------------------------------------------------------------------------- /src/styles/theme/register.scss: -------------------------------------------------------------------------------- 1 | // 注册的主题 2 | @import '@/styles/theme/dark/index.scss'; 3 | -------------------------------------------------------------------------------- /src/styles/theme/theme.scss: -------------------------------------------------------------------------------- 1 | .theme-#{$theme-name} { 2 | /** Layout */ 3 | 4 | .app-wrapper { 5 | background-color: $theme-bg-color; 6 | color: $font-color; 7 | // 侧边栏 8 | .sidebar-container { 9 | .sidebar-logo-container { 10 | background-color: lighten($theme-bg-color, 2%) !important; 11 | } 12 | .el-menu { 13 | background-color: lighten($theme-bg-color, 4%) !important; 14 | .el-menu-item { 15 | background-color: lighten($theme-bg-color, 4%) !important; 16 | &.is-active, 17 | &:hover { 18 | background-color: lighten($theme-bg-color, 8%) !important; 19 | color: $active-font-color !important; 20 | } 21 | } 22 | } 23 | .el-sub-menu__title { 24 | background-color: lighten($theme-bg-color, 4%) !important; 25 | } 26 | } 27 | 28 | // 顶部导航栏 29 | .navbar { 30 | background-color: $theme-bg-color; 31 | .el-breadcrumb__inner { 32 | a { 33 | color: $font-color; 34 | &:hover { 35 | color: $hover-color; 36 | } 37 | } 38 | .no-redirect { 39 | color: $font-color; 40 | } 41 | } 42 | .right-menu { 43 | .el-icon { 44 | color: $font-color; 45 | } 46 | .el-avatar { 47 | background: lighten($theme-bg-color, 20%); 48 | .el-icon { 49 | color: #fff; 50 | } 51 | } 52 | } 53 | } 54 | 55 | // tags-view 56 | .tags-view-container { 57 | background-color: $theme-bg-color !important; 58 | border-bottom: 1px solid lighten($theme-bg-color, 10%) !important; 59 | .tags-view-item { 60 | background-color: $theme-bg-color !important; 61 | color: $font-color !important; 62 | border: 1px solid $border-color !important; 63 | &.active { 64 | background-color: $active-theme-bg-color !important; 65 | color: $active-font-color !important; 66 | border-color: $border-color !important; 67 | } 68 | } 69 | .contextmenu { 70 | // 右键菜单 71 | background-color: lighten($theme-bg-color, 8%); 72 | color: $font-color; 73 | li:hover { 74 | background-color: lighten($theme-bg-color, 16%); 75 | color: $active-font-color; 76 | } 77 | } 78 | } 79 | 80 | // 右侧设置面板 81 | .handle-button { 82 | background-color: lighten($theme-bg-color, 20%) !important; 83 | } 84 | .el-drawer.rtl { 85 | background-color: $theme-bg-color; 86 | .drawer-title, 87 | .drawer-item { 88 | color: $font-color; 89 | } 90 | } 91 | } 92 | 93 | /** app-main 主要写 view 页面的黑暗样式 */ 94 | 95 | .app-main { 96 | // 指令权限页面 /permission/directive 97 | .permission-alert { 98 | background-color: lighten($theme-bg-color, 8%); 99 | } 100 | // 监控页面 /monitor 101 | .monitor { 102 | background-color: $theme-bg-color; 103 | } 104 | } 105 | 106 | /** login 页面 */ 107 | 108 | .login-container { 109 | background-color: $theme-bg-color; 110 | color: $font-color; 111 | .login-card { 112 | background-color: lighten($theme-bg-color, 4%) !important; 113 | } 114 | .el-icon { 115 | color: $font-color; 116 | } 117 | } 118 | 119 | /** element-plus */ 120 | 121 | // 侧边栏的 item 的 popper 122 | .el-popper { 123 | border: none !important; 124 | .el-menu { 125 | background-color: lighten($theme-bg-color, 4%) !important; 126 | .el-menu-item { 127 | background-color: lighten($theme-bg-color, 4%) !important; 128 | &.is-active, 129 | &:hover { 130 | background-color: lighten($theme-bg-color, 8%) !important; 131 | color: $active-font-color !important; 132 | } 133 | } 134 | .el-sub-menu__title { 135 | background-color: lighten($theme-bg-color, 4%) !important; 136 | } 137 | } 138 | } 139 | 140 | // 下拉菜单 141 | .el-dropdown__popper .el-dropdown__list { 142 | background-color: lighten($theme-bg-color, 8%); 143 | .el-dropdown-menu { 144 | background-color: lighten($theme-bg-color, 8%); 145 | .el-dropdown-menu__item { 146 | color: $font-color; 147 | &.is-disabled { 148 | color: #606266; 149 | } 150 | &:not(.is-disabled):hover { 151 | background-color: lighten($theme-bg-color, 16%); 152 | color: $active-font-color; 153 | } 154 | } 155 | .el-dropdown-menu__item--divided:before { 156 | background-color: lighten($theme-bg-color, 8%); 157 | } 158 | } 159 | } 160 | .el-popper__arrow::before { 161 | // 下拉菜单顶部三角区域 162 | background-color: lighten($theme-bg-color, 8%) !important; 163 | border: lighten($theme-bg-color, 8%) !important; 164 | } 165 | 166 | // 单选框按钮样式 167 | .el-radio-button__inner { 168 | background-color: lighten($theme-bg-color, 8%); 169 | color: $active-font-color; 170 | border: 1px solid $border-color; 171 | } 172 | .el-radio-button:first-child .el-radio-button__inner { 173 | border-left: none; 174 | } 175 | 176 | // el-tag 177 | .el-tag { 178 | background-color: lighten($theme-bg-color, 8%); 179 | border-color: $border-color; 180 | color: $active-font-color; 181 | &.el-tag--info { 182 | background-color: lighten($theme-bg-color, 8%); 183 | border-color: $border-color; 184 | color: $active-font-color; 185 | } 186 | } 187 | 188 | // tabs 标签页 189 | .el-tabs--border-card { 190 | background: lighten($theme-bg-color, 8%); 191 | border: 1px solid $border-color; 192 | .el-tabs__header { 193 | background-color: lighten($theme-bg-color, 8%); 194 | border-bottom: 1px solid $border-color; 195 | .el-tabs__item.is-active { 196 | background-color: lighten($theme-bg-color, 8%); 197 | border-right-color: $border-color; 198 | border-left-color: $border-color; 199 | } 200 | } 201 | } 202 | 203 | // 卡片 card 204 | .el-card { 205 | background: lighten($theme-bg-color, 8%); 206 | border: 1px solid $border-color; 207 | color: $font-color; 208 | } 209 | 210 | // 输入框 input 211 | .el-input__wrapper { 212 | background: lighten($theme-bg-color, 8%) !important; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // See https://vuejs.org/v2/guide/transitions.html for detail 2 | 3 | // fade 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | // fade-transform 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all 0.5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | // breadcrumb 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all 0.5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all 0.5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | /** cookies 封装 */ 2 | 3 | import Keys from '@/constant/key' 4 | import Cookies from 'js-cookie' 5 | 6 | export const getSidebarStatus = () => Cookies.get(Keys.sidebarStatus) 7 | export const setSidebarStatus = (sidebarStatus: string) => Cookies.set(Keys.sidebarStatus, sidebarStatus) 8 | 9 | export const getToken = () => Cookies.get(Keys.token) 10 | export const setToken = (token: string) => Cookies.set(Keys.token, token) 11 | export const removeToken = () => Cookies.remove(Keys.token) 12 | 13 | export const getActiveThemeName = () => Cookies.get(Keys.activeThemeName) 14 | export const setActiveThemeName = (themeName: string) => { Cookies.set(Keys.activeThemeName, themeName) } 15 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | /** 格式化时间 */ 4 | export const formatDateTime = (time: any) => { 5 | if (time == null || time === '') { 6 | return 'N/A' 7 | } 8 | const date = new Date(time) 9 | return dayjs(date).format('YYYY-MM-DD HH:mm:ss') 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/permission.ts: -------------------------------------------------------------------------------- 1 | import { useUserStoreHook } from '@/store/modules/user' 2 | 3 | /** 全局权限判断函数,和指令 v-permission 功能类似 */ 4 | export const checkPermission = (value: string[]): boolean => { 5 | if (value && value instanceof Array && value.length > 0) { 6 | const roles = useUserStoreHook().roles 7 | const permissionRoles = value 8 | return roles.some(role => { 9 | return permissionRoles.includes(role) 10 | }) 11 | } else { 12 | console.error('need roles! Like v-permission="[\'admin\',\'editor\']"') 13 | return false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/service.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' 2 | import { get } from 'lodash-es' 3 | import { ElMessage } from 'element-plus' 4 | import { getToken } from '@/utils/cookies' 5 | import { useUserStoreHook } from '@/store/modules/user' 6 | 7 | /** 创建请求实例 */ 8 | function createService() { 9 | // 创建一个 axios 实例 10 | const service = axios.create() 11 | // 请求拦截 12 | service.interceptors.request.use( 13 | (config) => config, 14 | // 发送失败 15 | (error) => Promise.reject(error) 16 | ) 17 | // 响应拦截(可根据具体业务作出相应的调整) 18 | service.interceptors.response.use( 19 | (response) => { 20 | // apiData 是 api 返回的数据 21 | const apiData = response.data as any 22 | // 这个 code 是和后端约定的业务 code 23 | const code = apiData.code 24 | // 如果没有 code, 代表这不是项目后端开发的 api 25 | if (code === undefined) { 26 | ElMessage.error('非本系统的接口') 27 | return Promise.reject(new Error('非本系统的接口')) 28 | } else { 29 | switch (code) { 30 | case 0: 31 | // code === 0 代表没有错误 32 | return apiData 33 | case 20000: 34 | // code === 20000 代表没有错误 35 | return apiData 36 | default: 37 | // 不是正确的 code 38 | ElMessage.error(apiData.msg || 'Error') 39 | return Promise.reject(new Error('Error')) 40 | } 41 | } 42 | }, 43 | (error) => { 44 | // status 是 HTTP 状态码 45 | const status = get(error, 'response.status') 46 | switch (status) { 47 | case 400: 48 | error.message = '请求错误' 49 | break 50 | case 401: 51 | error.message = '未授权,请登录' 52 | break 53 | case 403: 54 | // token 过期时,直接退出登录并强制刷新页面(会重定向到登录页) 55 | useUserStoreHook().logout() 56 | location.reload() 57 | break 58 | case 404: 59 | error.message = '请求地址出错' 60 | break 61 | case 408: 62 | error.message = '请求超时' 63 | break 64 | case 500: 65 | error.message = '服务器内部错误' 66 | break 67 | case 501: 68 | error.message = '服务未实现' 69 | break 70 | case 502: 71 | error.message = '网关错误' 72 | break 73 | case 503: 74 | error.message = '服务不可用' 75 | break 76 | case 504: 77 | error.message = '网关超时' 78 | break 79 | case 505: 80 | error.message = 'HTTP版本不受支持' 81 | break 82 | default: 83 | break 84 | } 85 | ElMessage.error(error.message) 86 | return Promise.reject(error) 87 | } 88 | ) 89 | return service 90 | } 91 | 92 | /** 创建请求方法 */ 93 | function createRequestFunction(service: AxiosInstance) { 94 | return function(config: AxiosRequestConfig) { 95 | const configDefault = { 96 | headers: { 97 | // 携带 token 98 | 'X-Access-Token': getToken(), 99 | 'Content-Type': get(config, 'headers.Content-Type', 'application/json') 100 | }, 101 | timeout: 5000, 102 | baseURL: process.env.VUE_APP_BASE_API, 103 | data: {} 104 | } 105 | return service(Object.assign(configDefault, config)) 106 | } 107 | } 108 | 109 | /** 用于网络请求的实例 */ 110 | export const service = createService() 111 | /** 用于网络请求的方法 */ 112 | export const request = createRequestFunction(service) 113 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | export const isExternal = (path: string) => /^(https?:|mailto:|tel:)/.test(path) 2 | 3 | export const isArray = (arg: any) => { 4 | if (typeof Array.isArray === 'undefined') { 5 | return Object.prototype.toString.call(arg) === '[object Array]' 6 | } 7 | return Array.isArray(arg) 8 | } 9 | 10 | export const isValidURL = (url: string) => { 11 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 12 | return reg.test(url) 13 | } 14 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /src/views/dashboard/editor/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/views/error-page/401.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/views/error-page/404.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 139 | 140 | 190 | -------------------------------------------------------------------------------- /src/views/monitor/components/iframe-breadcurmb.vue: -------------------------------------------------------------------------------- 1 |