├── .dockerignore ├── .editorconfig ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-rc.yml │ └── build-stable.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── jsconfig.json ├── nginx.conf ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── account.js │ ├── login.js │ ├── netdisk │ │ ├── manage.class.js │ │ └── overview.js │ └── sys │ │ ├── dept.class.js │ │ ├── log.class.js │ │ ├── menu.class.js │ │ ├── online.class.js │ │ ├── param-config.class.js │ │ ├── role.class.js │ │ ├── serve.js │ │ ├── task.class.js │ │ └── user.class.js ├── assets │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── background.svg │ └── no-preview.png ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── ContextMenu │ │ ├── ContextMenu.vue │ │ └── index.js │ ├── Echarts │ │ └── index.js │ ├── FormDialog │ │ ├── FormDialog.js │ │ ├── README.md │ │ ├── formdialog.module.scss │ │ ├── index.js │ │ └── vnode.js │ ├── Hamburger │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── SocketStatus │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── Table │ │ ├── README.md │ │ ├── column-setting-popover.vue │ │ ├── index.js │ │ └── index.module.scss │ └── WarningConfirmButton │ │ └── index.vue ├── config │ ├── router.config.js │ └── settings.js ├── core │ ├── bootstrap.js │ ├── directives │ │ ├── el-drag-dialog │ │ │ ├── drag.js │ │ │ └── index.js │ │ ├── permission │ │ │ ├── index.js │ │ │ └── permission.js │ │ └── table-infinite-scroll │ │ │ ├── index.js │ │ │ └── table-infinite-scroll.js │ ├── mixins │ │ ├── message-box.js │ │ ├── permission.js │ │ └── socket-hook.js │ ├── permission │ │ ├── check-permission.js │ │ ├── decorator.js │ │ └── index.js │ ├── socket │ │ ├── event-type.js │ │ └── socket-io.js │ └── use.js ├── icons │ ├── index.js │ ├── svg │ │ ├── captcha.svg │ │ ├── dashboard.svg │ │ ├── disk-overview.svg │ │ ├── documentation.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── file-type-code.svg │ │ ├── file-type-dir.svg │ │ ├── file-type-docx.svg │ │ ├── file-type-excel.svg │ │ ├── file-type-img.svg │ │ ├── file-type-music.svg │ │ ├── file-type-office.svg │ │ ├── file-type-pdf.svg │ │ ├── file-type-ppt.svg │ │ ├── file-type-psd.svg │ │ ├── file-type-txt.svg │ │ ├── file-type-unknown.svg │ │ ├── file-type-video.svg │ │ ├── file-type-zip.svg │ │ ├── fullscreen.svg │ │ ├── guide.svg │ │ ├── international.svg │ │ ├── log.svg │ │ ├── menu.svg │ │ ├── monitor.svg │ │ ├── netdisk-manage.svg │ │ ├── netdisk.svg │ │ ├── param-config-list.svg │ │ ├── param-config.svg │ │ ├── password.svg │ │ ├── people.svg │ │ ├── peoples.svg │ │ ├── permission.svg │ │ ├── reload.svg │ │ ├── role.svg │ │ ├── schedule-log.svg │ │ ├── schedule.svg │ │ ├── serve.svg │ │ ├── socket-status-close.svg │ │ ├── socket-status-connected.svg │ │ ├── socket-status-connecting1.svg │ │ ├── socket-status-connecting2.svg │ │ ├── socket-status-connecting3.svg │ │ ├── system.svg │ │ ├── task.svg │ │ └── user.svg │ └── svgo.yml ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── BlankLayout.vue │ │ ├── Navbar.vue │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── Logo.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ ├── TableLayout.vue │ │ ├── TagsView │ │ │ ├── ScrollPane.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── router │ ├── generator-routers.js │ ├── index.js │ ├── modules │ │ ├── netdisk.js │ │ └── system.js │ └── router-guard.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── permission.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ ├── user.js │ │ └── ws.js ├── styles │ ├── element-ui.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── event-bus.js │ ├── get-page-title.js │ ├── index.js │ ├── request.js │ └── validate.js ├── vendor │ ├── Export2Excel.js │ └── Export2Zip.js └── views │ ├── 404.vue │ ├── account │ ├── about.vue │ ├── components │ │ ├── basic.vue │ │ └── safe.vue │ └── settings.vue │ ├── dashboard │ └── index.vue │ ├── login │ └── index.vue │ ├── netdisk │ ├── components │ │ ├── file-operate-button-list.vue │ │ ├── file-preview-drawer.vue │ │ ├── file-upload-drawer.vue │ │ └── overview-header-item.vue │ ├── manage.vue │ └── overview.vue │ └── system │ ├── monitor │ ├── login-log.vue │ ├── online.vue │ └── serve.vue │ ├── param-config │ └── config-list.vue │ ├── permission │ ├── components │ │ ├── dept-tree-pane.vue │ │ ├── menu-form-dialog.vue │ │ ├── menu-icon-selector.vue │ │ ├── permission-cascader.vue │ │ ├── role-form-dialog.vue │ │ └── user-form-dialog.vue │ ├── menu.vue │ ├── role.vue │ └── user.vue │ └── schedule │ ├── components │ └── task-form-dialog.vue │ ├── log.vue │ └── task.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Breadcrumb.spec.js │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js │ └── utils │ ├── formatTime.spec.js │ ├── param2Obj.spec.js │ ├── parseTime.spec.js │ └── validate.spec.js └── vue.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api/admin/' 6 | VUE_APP_BASE_SOCKET_PATH = '/ws' 7 | VUE_APP_BASE_SOCKET_NSP = '/admin' 8 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api/admin/' 6 | VUE_APP_BASE_SOCKET_PATH = '/ws' 7 | VUE_APP_BASE_SOCKET_NSP = '/admin' 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report(报告问题) 3 | about: Create a report to help us improve 4 | --- 5 | 10 | 11 | 12 | ## Bug report(问题描述) 13 | 14 | #### Steps to reproduce(问题复现步骤) 15 | 20 | 21 | #### Screenshot or Gif(截图或动态图) 22 | 23 | 24 | #### Link to minimal reproduction(最小可在线还原demo) 25 | 26 | 29 | 30 | #### Other relevant information(格外信息) 31 | - Your OS: 32 | - Node.js version: 33 | - vue-element-admin version: 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request(新功能建议) 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ## Feature request(新功能建议) 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/build-rc.yml: -------------------------------------------------------------------------------- 1 | name: Build RC Image 2 | 3 | on: 4 | create: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repo 13 | uses: actions/checkout@v2 14 | - name: Set output 15 | id: vars 16 | run: echo ::set-output name=short_ref::${GITHUB_REF#refs/*/} 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v1 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v1 21 | - name: Login to DockerHub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKERHUB_USERNAME }} 25 | password: ${{ secrets.DOCKERHUB_TOKEN }} 26 | - name: Build and push 27 | id: docker_build 28 | uses: docker/build-push-action@v2 29 | with: 30 | push: true 31 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/sfvueadmin:rc 32 | - name: Image digest 33 | run: echo ${{ steps.docker_build.outputs.digest }} 34 | -------------------------------------------------------------------------------- /.github/workflows/build-stable.yml: -------------------------------------------------------------------------------- 1 | name: Build Stable Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'nest' 7 | - 'midway' 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v2 15 | - name: Set output 16 | id: vars 17 | run: echo ::set-output name=short_ref::${GITHUB_REF#refs/*/} 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v1 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v1 22 | - name: Login to DockerHub 23 | uses: docker/login-action@v1 24 | with: 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | - name: Build and push 28 | id: docker_build 29 | uses: docker/build-push-action@v2 30 | with: 31 | push: true 32 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/sfvueadmin:${{ steps.vars.outputs.short_ref }} 33 | - name: Image digest 34 | run: echo ${{ steps.docker_build.outputs.digest }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | yarn.lock 9 | tests/**/coverage/ 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "semi": false, 5 | "trailingComma": "none", 6 | "javascript.format.insertSpaceBeforeFunctionParenthesis": false 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as builder 2 | WORKDIR /sf-vue-admin 3 | # RUN npm set registry https://registry.npm.taobao.org 4 | # cache step 5 | COPY package.json /sf-vue-admin/package.json 6 | RUN npm install 7 | # build 8 | COPY ./ /sf-vue-admin 9 | RUN npm run build:prod 10 | 11 | FROM nginx as production 12 | RUN mkdir /web 13 | COPY --from=builder /sf-vue-admin/dist/ /web 14 | COPY --from=builder /sf-vue-admin/nginx.conf /etc/nginx/nginx.conf 15 | EXPOSE 80 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present Changyuan Yang 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.md: -------------------------------------------------------------------------------- 1 | # sf-vue-admin 2 | 3 | ![](https://img.shields.io/github/commit-activity/m/hackycy/sf-vue-admin) ![](https://img.shields.io/github/license/hackycy/sf-vue-admin) ![](https://img.shields.io/github/repo-size/hackycy/sf-vue-admin) ![](https://img.shields.io/github/languages/top/hackycy/sf-vue-admin) 4 | 5 | **基于NestJs + TypeScript + TypeORM + Redis + MySql + Vue + Element-UI编写的一款简单高效的前后端分离的权限管理系统。希望这个项目在全栈的路上能够帮助到你。** 6 | 7 | - 使用文档:[https://blog.si-yee.com/sf-admin-cli/](https://blog.si-yee.com/sf-admin-cli/) 8 | - 演示站点:[http://opensource.admin.si-yee.com](http://opensource.admin.si-yee.com/) 9 | - **Vue3版请移步:**[https://github.com/arklnk/ark-admin-nest](https://github.com/arklnk/ark-admin-nest) 10 | - Swagger Api文档:[http://opensource.admin.si-yee.com/api/doc/admin/swagger-api/static/index.html](http://opensource.admin.si-yee.com/api/doc/admin/swagger-api/static/index.html) 11 | 12 | 演示环境账号密码: 13 | 14 | | 账号 | 密码 | 权限 | 15 | | :----------: | :----: | :----------------------: | 16 | | openadmin | 123456 | 仅只有各个功能的查询权限 | 17 | | monitoradmin | 123456 | 系统监控页面及按钮权限 | 18 | 19 | > 所有新建的用户初始密码都为123456 20 | 21 | # 欢迎Star && PR 22 | 23 | **如果项目有帮助到你可以点个Star支持下。有更好的实现欢迎PR。** 24 | 25 | # LICENSE 26 | 27 | [MIT](LICENSE) 28 | 29 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app 4 | '@vue/cli-plugin-babel/preset' 5 | ], 6 | plugins: ['lodash'], 7 | 'env': { 8 | 'development': { 9 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). 10 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. 11 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html 12 | 'plugins': ['dynamic-import-node'] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "baseUrl": "./", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "dist"] 10 | } 11 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | worker_rlimit_nofile 51200; 6 | 7 | events { 8 | use epoll; 9 | worker_connections 51200; 10 | multi_accept on; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | access_log /var/log/nginx/access_log.log; 17 | error_log /var/log/error_log.log; 18 | 19 | # server_names_hash_bucket_size 512; 20 | # client_header_buffer_size 32k; 21 | # large_client_header_buffers 4 32k; 22 | # client_max_body_size 50m; 23 | 24 | keepalive_timeout 60; 25 | tcp_nodelay on; 26 | 27 | gzip on; 28 | gzip_min_length 1k; 29 | gzip_buffers 4 16k; 30 | gzip_http_version 1.1; 31 | gzip_comp_level 2; 32 | gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml; 33 | gzip_vary on; 34 | gzip_proxied expired no-cache no-store private auth; 35 | gzip_disable "MSIE [1-6]\."; 36 | 37 | server { 38 | listen 80; 39 | server_name sfvueadmin; 40 | 41 | location / { 42 | # same docker config 43 | root /web; 44 | index index.html; 45 | # support history mode 46 | try_files $uri $uri/ /index.html; 47 | } 48 | 49 | #禁止访问的文件或目录 50 | location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md) 51 | { 52 | return 404; 53 | } 54 | 55 | # api proxy 56 | location /api/ { 57 | proxy_pass http://sfserver:7001/; 58 | 59 | proxy_set_header Host $host; 60 | proxy_set_header X-Real-IP $remote_addr; 61 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 62 | proxy_set_header REMOTE-HOST $remote_addr; 63 | 64 | #缓存相关配置 65 | # proxy_cache cache_one; 66 | # proxy_cache_key $host$request_uri$is_args$args; 67 | # proxy_cache_valid 200 304 301 302 1h; 68 | 69 | # 持久化连接相关配置 70 | proxy_connect_timeout 3000s; 71 | proxy_read_timeout 86400s; 72 | proxy_send_timeout 3000s; 73 | # proxy_http_version 1.1; 74 | # proxy_set_header Upgrade $http_upgrade; 75 | # proxy_set_header Connection "upgrade"; 76 | # expires 12h; 77 | } 78 | 79 | # ws proxy 80 | location /ws/ { 81 | # 非绝对路径,需要带上/ws socket port 7002 82 | proxy_pass http://sfserver:7002; 83 | 84 | proxy_set_header Host $host; 85 | proxy_set_header X-Real-IP $remote_addr; 86 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 87 | proxy_set_header REMOTE-HOST $remote_addr; 88 | 89 | proxy_connect_timeout 3600s; 90 | proxy_read_timeout 3600s; 91 | proxy_send_timeout 3600s; 92 | 93 | proxy_http_version 1.1; 94 | proxy_set_header Upgrade $http_upgrade; 95 | proxy_set_header Connection "upgrade"; 96 | 97 | # rewrite /ws/(.*) /$1 break; 98 | 99 | proxy_redirect off; 100 | } 101 | 102 | error_page 500 502 503 504 /50x.html; 103 | location = /50x.html { 104 | root /usr/share/nginx/html; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sf-vue-admin", 3 | "version": "2.4.0", 4 | "description": "simple and efficient authority management system with separation of front and backends", 5 | "author": "hackycy", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "vue-cli-service serve", 10 | "build:report": "vue-cli-service build --report", 11 | "build:prod": "vue-cli-service build", 12 | "build:stage": "vue-cli-service build --mode staging", 13 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", 14 | "lint": "eslint --ext .js,.vue src", 15 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 16 | "test:ci": "npm run lint && npm run test:unit" 17 | }, 18 | "dependencies": { 19 | "axios": "^0.21.1", 20 | "core-js": "^3.12.1", 21 | "echarts": "^4.9.0", 22 | "element-ui": "^2.15.6", 23 | "file-saver": "^2.0.5", 24 | "jszip": "^3.6.0", 25 | "lodash": "^4.17.21", 26 | "mitt": "^1.1.2", 27 | "normalize.css": "^8.0.1", 28 | "nprogress": "^0.2.0", 29 | "path-to-regexp": "^2.4.0", 30 | "qiniu-js": "^3.1.4", 31 | "screenfull": "^4.2.0", 32 | "socket.io-client": "4.1.3", 33 | "store": "^2.0.12", 34 | "vue": "^2.6.10", 35 | "vue-count-to": "^1.0.13", 36 | "vue-echarts": "^4.1.0", 37 | "vue-router": "^3.5.1", 38 | "vuedraggable": "^2.24.3", 39 | "vuex": "^3.6.2", 40 | "xlsx": "^0.17.0" 41 | }, 42 | "devDependencies": { 43 | "@vue/cli-plugin-babel": "^4.4.4", 44 | "@vue/cli-plugin-eslint": "^4.4.4", 45 | "@vue/cli-plugin-unit-jest": "^4.4.4", 46 | "@vue/cli-service": "^4.4.4", 47 | "@vue/test-utils": "^1.0.0-beta.29", 48 | "autoprefixer": "^9.5.1", 49 | "babel-eslint": "^10.1.0", 50 | "babel-jest": "^23.6.0", 51 | "babel-plugin-dynamic-import-node": "^2.3.3", 52 | "babel-plugin-lodash": "^3.3.4", 53 | "compression-webpack-plugin": "^6.1.1", 54 | "date-fns": "^2.23.0", 55 | "eslint": "^6.7.2", 56 | "eslint-plugin-vue": "^6.2.2", 57 | "html-webpack-plugin": "^3.2.0", 58 | "lodash-webpack-plugin": "^0.11.6", 59 | "sass": "^1.26.8", 60 | "sass-loader": "^8.0.2", 61 | "script-ext-html-webpack-plugin": "^2.1.3", 62 | "svg-sprite-loader": "^4.1.3", 63 | "svgo": "^1.2.2", 64 | "vue-template-compiler": "^2.6.10" 65 | }, 66 | "browserslist": [ 67 | "> 1%", 68 | "last 2 versions" 69 | ], 70 | "engines": { 71 | "node": ">=8.9", 72 | "npm": ">= 3.0.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackycy/sf-vue-admin/a56e217b45d3bfa5593c34249aebe742bfdb1524/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 12 | 15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/api/account.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function updateAccountInfo(data) { 4 | return request({ 5 | url: 'account/update', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function updatePassword(data) { 12 | return request({ 13 | url: 'account/password', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | 19 | export function getInfo() { 20 | return request({ 21 | url: 'account/info', 22 | method: 'get' 23 | }) 24 | } 25 | 26 | export function permmenu() { 27 | return request({ 28 | url: 'account/permmenu', 29 | method: 'get' 30 | }) 31 | } 32 | 33 | export function logout() { 34 | return request({ 35 | url: 'account/logout', 36 | method: 'post' 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: 'login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getImageCaptcha(query) { 12 | return request({ 13 | url: 'captcha/img', 14 | method: 'get', 15 | query 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/api/netdisk/manage.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | const NETDISK_TIMEOUT_INTERVAL = 60000 5 | 6 | @PermissionPrefix('netdisk/manage') 7 | class NetdiskManage { 8 | @PermissionAction() 9 | list(query) { 10 | return request({ 11 | url: 'netdisk/manage/list', 12 | method: 'get', 13 | params: query 14 | }) 15 | } 16 | 17 | @PermissionAction() 18 | mkdir(data) { 19 | return request({ 20 | url: 'netdisk/manage/mkdir', 21 | method: 'post', 22 | data 23 | }) 24 | } 25 | 26 | @PermissionAction() 27 | rename(data) { 28 | return request({ 29 | url: 'netdisk/manage/rename', 30 | method: 'post', 31 | data, 32 | timeout: NETDISK_TIMEOUT_INTERVAL 33 | }) 34 | } 35 | 36 | @PermissionAction() 37 | download(data) { 38 | return request({ 39 | url: 'netdisk/manage/download', 40 | method: 'post', 41 | data 42 | }) 43 | } 44 | 45 | @PermissionAction() 46 | delete(data) { 47 | return request({ 48 | url: 'netdisk/manage/delete', 49 | method: 'post', 50 | data, 51 | timeout: NETDISK_TIMEOUT_INTERVAL 52 | }) 53 | } 54 | 55 | @PermissionAction() 56 | token() { 57 | return request({ 58 | url: 'netdisk/manage/token', 59 | method: 'get' 60 | }) 61 | } 62 | 63 | @PermissionAction() 64 | info(data) { 65 | return request({ 66 | url: 'netdisk/manage/info', 67 | method: 'post', 68 | data 69 | }) 70 | } 71 | 72 | @PermissionAction() 73 | mark(data) { 74 | return request({ 75 | url: 'netdisk/manage/mark', 76 | method: 'post', 77 | data 78 | }) 79 | } 80 | 81 | @PermissionAction() 82 | cut(data) { 83 | return request({ 84 | url: 'netdisk/manage/cut', 85 | method: 'post', 86 | data, 87 | timeout: NETDISK_TIMEOUT_INTERVAL 88 | }) 89 | } 90 | 91 | @PermissionAction() 92 | copy(data) { 93 | return request({ 94 | url: 'netdisk/manage/copy', 95 | method: 'post', 96 | data, 97 | timeout: NETDISK_TIMEOUT_INTERVAL 98 | }) 99 | } 100 | } 101 | 102 | export default NetdiskManage 103 | -------------------------------------------------------------------------------- /src/api/netdisk/overview.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getDesc() { 4 | return request({ 5 | url: 'netdisk/overview/desc', 6 | method: 'get' 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/api/sys/dept.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | @PermissionPrefix('sys/dept') 5 | class SysDept { 6 | @PermissionAction() 7 | list() { 8 | return request({ 9 | url: 'sys/dept/list', 10 | method: 'get' 11 | }) 12 | } 13 | 14 | @PermissionAction() 15 | move(data) { 16 | return request({ 17 | url: 'sys/dept/move', 18 | method: 'post', 19 | data 20 | }) 21 | } 22 | 23 | @PermissionAction() 24 | update(data) { 25 | return request({ 26 | url: 'sys/dept/update', 27 | method: 'post', 28 | data 29 | }) 30 | } 31 | 32 | @PermissionAction() 33 | delete(data) { 34 | return request({ 35 | url: 'sys/dept/delete', 36 | method: 'post', 37 | data 38 | }) 39 | } 40 | 41 | @PermissionAction() 42 | add(data) { 43 | return request({ 44 | url: 'sys/dept/add', 45 | method: 'post', 46 | data 47 | }) 48 | } 49 | 50 | @PermissionAction() 51 | info(query) { 52 | return request({ 53 | url: 'sys/dept/info', 54 | method: 'get', 55 | params: query 56 | }) 57 | } 58 | 59 | @PermissionAction() 60 | transfer(data) { 61 | return request({ 62 | url: 'sys/dept/transfer', 63 | method: 'post', 64 | data 65 | }) 66 | } 67 | } 68 | 69 | export default SysDept 70 | -------------------------------------------------------------------------------- /src/api/sys/log.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | @PermissionPrefix('sys/log') 5 | class SysLog { 6 | @PermissionAction('login/page') 7 | loginPage(query) { 8 | return request({ 9 | url: 'sys/log/login/page', 10 | method: 'get', 11 | params: query 12 | }) 13 | } 14 | 15 | @PermissionAction('task/page') 16 | taskPage(query) { 17 | return request({ 18 | url: 'sys/log/task/page', 19 | method: 'get', 20 | params: query 21 | }) 22 | } 23 | } 24 | 25 | export default SysLog 26 | -------------------------------------------------------------------------------- /src/api/sys/menu.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | @PermissionPrefix('sys/menu') 5 | class SysMenu { 6 | @PermissionAction() 7 | list() { 8 | return request({ 9 | url: 'sys/menu/list', 10 | method: 'get' 11 | }) 12 | } 13 | 14 | @PermissionAction() 15 | info(query) { 16 | return request({ 17 | url: 'sys/menu/info', 18 | method: 'get', 19 | params: query 20 | }) 21 | } 22 | 23 | @PermissionAction() 24 | add(data) { 25 | return request({ 26 | url: 'sys/menu/add', 27 | method: 'post', 28 | data 29 | }) 30 | } 31 | 32 | @PermissionAction() 33 | update(data) { 34 | return request({ 35 | url: 'sys/menu/update', 36 | method: 'post', 37 | data 38 | }) 39 | } 40 | 41 | @PermissionAction() 42 | delete(data) { 43 | return request({ 44 | url: 'sys/menu/delete', 45 | method: 'post', 46 | data 47 | }) 48 | } 49 | } 50 | 51 | export default SysMenu 52 | -------------------------------------------------------------------------------- /src/api/sys/online.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | @PermissionPrefix('sys/online') 5 | class SysOnline { 6 | @PermissionAction() 7 | list() { 8 | return request({ 9 | url: 'sys/online/list', 10 | method: 'get' 11 | }) 12 | } 13 | 14 | @PermissionAction() 15 | kick(data) { 16 | return request({ 17 | url: 'sys/online/kick', 18 | method: 'post', 19 | data 20 | }) 21 | } 22 | } 23 | 24 | export default SysOnline 25 | -------------------------------------------------------------------------------- /src/api/sys/param-config.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | @PermissionPrefix('sys/param-config') 5 | class SysParamConfig { 6 | @PermissionAction() 7 | page(query) { 8 | return request({ 9 | url: 'sys/param-config/page', 10 | method: 'get', 11 | params: query 12 | }) 13 | } 14 | 15 | @PermissionAction() 16 | info(query) { 17 | return request({ 18 | url: 'sys/param-config/info', 19 | method: 'get', 20 | params: query 21 | }) 22 | } 23 | 24 | @PermissionAction() 25 | add(data) { 26 | return request({ 27 | url: 'sys/param-config/add', 28 | method: 'post', 29 | data 30 | }) 31 | } 32 | 33 | @PermissionAction() 34 | update(data) { 35 | return request({ 36 | url: 'sys/param-config/update', 37 | method: 'post', 38 | data 39 | }) 40 | } 41 | 42 | @PermissionAction() 43 | delete(data) { 44 | return request({ 45 | url: 'sys/param-config/delete', 46 | method: 'post', 47 | data 48 | }) 49 | } 50 | } 51 | 52 | export default SysParamConfig 53 | -------------------------------------------------------------------------------- /src/api/sys/role.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | @PermissionPrefix('sys/role') 5 | class SysRole { 6 | @PermissionAction() 7 | info(query) { 8 | return request({ 9 | url: 'sys/role/info', 10 | method: 'get', 11 | params: query 12 | }) 13 | } 14 | 15 | @PermissionAction() 16 | list() { 17 | return request({ 18 | url: 'sys/role/list', 19 | method: 'get' 20 | }) 21 | } 22 | 23 | @PermissionAction() 24 | page(query) { 25 | return request({ 26 | url: 'sys/role/page', 27 | method: 'get', 28 | params: query 29 | }) 30 | } 31 | 32 | @PermissionAction() 33 | add(data) { 34 | return request({ 35 | url: 'sys/role/add', 36 | method: 'post', 37 | data 38 | }) 39 | } 40 | 41 | @PermissionAction() 42 | update(data) { 43 | return request({ 44 | url: 'sys/role/update', 45 | method: 'post', 46 | data 47 | }) 48 | } 49 | 50 | @PermissionAction() 51 | delete(data) { 52 | return request({ 53 | url: 'sys/role/delete', 54 | method: 'post', 55 | data 56 | }) 57 | } 58 | } 59 | 60 | export default SysRole 61 | -------------------------------------------------------------------------------- /src/api/sys/serve.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getServeStat() { 4 | return request({ 5 | url: 'sys/serve/stat', 6 | method: 'get' 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/api/sys/task.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | @PermissionPrefix('sys/task') 5 | class SysTask { 6 | @PermissionAction() 7 | page(query) { 8 | return request({ 9 | url: 'sys/task/page', 10 | params: query, 11 | method: 'get' 12 | }) 13 | } 14 | 15 | @PermissionAction() 16 | info(query) { 17 | return request({ 18 | url: 'sys/task/info', 19 | params: query, 20 | method: 'get' 21 | }) 22 | } 23 | 24 | @PermissionAction() 25 | add(data) { 26 | return request({ 27 | url: 'sys/task/add', 28 | method: 'post', 29 | data 30 | }) 31 | } 32 | 33 | @PermissionAction() 34 | delete(data) { 35 | return request({ 36 | url: 'sys/task/delete', 37 | method: 'post', 38 | data 39 | }) 40 | } 41 | 42 | @PermissionAction() 43 | update(data) { 44 | return request({ 45 | url: 'sys/task/update', 46 | method: 'post', 47 | data 48 | }) 49 | } 50 | 51 | @PermissionAction() 52 | once(data) { 53 | return request({ 54 | url: 'sys/task/once', 55 | method: 'post', 56 | data 57 | }) 58 | } 59 | 60 | @PermissionAction() 61 | start(data) { 62 | return request({ 63 | url: 'sys/task/start', 64 | method: 'post', 65 | data 66 | }) 67 | } 68 | 69 | @PermissionAction() 70 | stop(data) { 71 | return request({ 72 | url: 'sys/task/stop', 73 | method: 'post', 74 | data 75 | }) 76 | } 77 | } 78 | 79 | export default SysTask 80 | -------------------------------------------------------------------------------- /src/api/sys/user.class.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import { PermissionAction, PermissionPrefix } from '@/core/permission/decorator' 3 | 4 | @PermissionPrefix('sys/user') 5 | class SysUser { 6 | @PermissionAction() 7 | page(data) { 8 | return request({ 9 | url: 'sys/user/page', 10 | method: 'post', 11 | data 12 | }) 13 | } 14 | 15 | @PermissionAction() 16 | add(data) { 17 | return request({ 18 | url: 'sys/user/add', 19 | method: 'post', 20 | data 21 | }) 22 | } 23 | 24 | @PermissionAction() 25 | info(query) { 26 | return request({ 27 | url: 'sys/user/info', 28 | method: 'get', 29 | params: query 30 | }) 31 | } 32 | 33 | @PermissionAction() 34 | update(data) { 35 | return request({ 36 | url: 'sys/user/update', 37 | method: 'post', 38 | data 39 | }) 40 | } 41 | 42 | @PermissionAction() 43 | password(data) { 44 | return request({ 45 | url: 'sys/user/password', 46 | method: 'post', 47 | data 48 | }) 49 | } 50 | 51 | @PermissionAction() 52 | delete(data) { 53 | return request({ 54 | url: 'sys/user/delete', 55 | method: 'post', 56 | data 57 | }) 58 | } 59 | } 60 | 61 | export default SysUser 62 | -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackycy/sf-vue-admin/a56e217b45d3bfa5593c34249aebe742bfdb1524/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackycy/sf-vue-admin/a56e217b45d3bfa5593c34249aebe742bfdb1524/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/no-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackycy/sf-vue-admin/a56e217b45d3bfa5593c34249aebe742bfdb1524/src/assets/no-preview.png -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 75 | 76 | 89 | -------------------------------------------------------------------------------- /src/components/ContextMenu/index.js: -------------------------------------------------------------------------------- 1 | import ContextMenu from './ContextMenu' 2 | 3 | function install(Vue) { 4 | if (Vue.prototype.$openContextMenu) { 5 | return 6 | } 7 | 8 | const component = Vue.component('s-context-menu', ContextMenu) 9 | 10 | const ContextMenuConstructor = Vue.extend(component) 11 | 12 | const instance = new ContextMenuConstructor({ 13 | el: document.createElement('div') 14 | }) 15 | 16 | Vue.prototype.$openContextMenu = instance.show 17 | } 18 | 19 | ContextMenu.install = install 20 | 21 | export default ContextMenu 22 | -------------------------------------------------------------------------------- /src/components/Echarts/index.js: -------------------------------------------------------------------------------- 1 | import VueECharts from 'vue-echarts' // 在 webpack 环境下指向 components/ECharts.vue 2 | 3 | // https://github.com/ecomfe/vue-echarts/blob/5.x/README.zh_CN.md 4 | // 手动引入 ECharts 各模块来减小打包体积 5 | import 'echarts/lib/component/tooltip' 6 | import 'echarts/lib/component/toolbox' 7 | import 'echarts/lib/component/legend' 8 | import 'echarts/lib/component/dataset' 9 | import 'echarts/lib/component/grid' 10 | 11 | import 'echarts/lib/chart/line' 12 | import 'echarts/lib/chart/bar' 13 | 14 | // 如果需要配合 ECharts 扩展使用,只需要直接引入扩展包即可 15 | // 以 ECharts-GL 为例: 16 | // 需要安装依赖:npm install --save echarts-gl,并添加如下引用 17 | // import 'echarts-gl' 18 | 19 | export default { 20 | install(Vue) { 21 | // 注册组件后即可使用 22 | Vue.component('v-chart', VueECharts) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/FormDialog/formdialog.module.scss: -------------------------------------------------------------------------------- 1 | .s-formdialog { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | &__header { 6 | position: relative; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | &-icon { 12 | display: inline-block; 13 | cursor: pointer; 14 | position: absolute; 15 | top: 0; 16 | right: 0; 17 | color: #909399; 18 | 19 | &--fullscreen { 20 | right: 16px; 21 | margin-right: 10px; 22 | } 23 | } 24 | } 25 | 26 | & :global(.el-dialog__body) { 27 | flex: 1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/FormDialog/index.js: -------------------------------------------------------------------------------- 1 | import FormDialog from './FormDialog' 2 | 3 | function install(Vue) { 4 | if (Vue.prototype.$openFormDialog) { 5 | return 6 | } 7 | 8 | const component = Vue.component('form-dialog', FormDialog) 9 | 10 | const FormDialogConstructor = Vue.extend(component) 11 | 12 | const instance = new FormDialogConstructor({ 13 | el: document.createElement('div') 14 | }) 15 | 16 | Vue.prototype.$openFormDialog = instance.open 17 | } 18 | 19 | FormDialog.install = install 20 | 21 | export default FormDialog 22 | -------------------------------------------------------------------------------- /src/components/FormDialog/vnode.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isObject, cloneDeep, isString } from 'lodash' 2 | 3 | export function renderVNode(vnode, { scope, $scopedSlots, prop }) { 4 | // get h 5 | const h = this.$createElement 6 | 7 | if (!vnode) { 8 | return null 9 | } 10 | // 插槽 11 | if (isString(vnode) && vnode.startsWith('slot-')) { 12 | // template 13 | const s = $scopedSlots[vnode] 14 | if (s) { 15 | return s({ scope }) 16 | } else { 17 | throw new Error(`can not find this slot:${vnode}`) 18 | } 19 | } 20 | 21 | // render函数 22 | if (isFunction(vnode)) { 23 | return vnode(h, { scope }) 24 | } 25 | 26 | // createElement 参数对象 27 | if (isObject(vnode)) { 28 | if (vnode.context) { 29 | return vnode 30 | } 31 | 32 | if (vnode.name) { 33 | const data = cloneDeep(vnode) 34 | 35 | // https://cn.vuejs.org/v2/guide/render-function.html#深入数据对象 36 | const keys = [ 37 | 'class', 38 | 'style', 39 | 'props', 40 | 'attrs', 41 | 'domProps', 42 | 'on', 43 | 'nativeOn', 44 | 'directives', 45 | 'scopedSlots', 46 | 'slot', 47 | 'key', 48 | 'ref', 49 | 'refInFor' 50 | ] 51 | 52 | for (const key in data) { 53 | if (!keys.includes(key)) { 54 | delete data[key] 55 | } 56 | } 57 | 58 | if (scope) { 59 | if (!data.props) { 60 | data.props = {} 61 | } 62 | 63 | if (!data.on) { 64 | data.on = {} 65 | } 66 | 67 | // bind domProps value 68 | data.props.value = scope[prop] 69 | 70 | const onInput = data.on.input 71 | 72 | data.on.input = (val) => { 73 | // watch 74 | this.$set(scope, prop, val) 75 | 76 | if (onInput) { 77 | onInput(val) 78 | } 79 | } 80 | } 81 | 82 | return h(vnode.name, data) 83 | } else { 84 | throw new Error('component name can not be empty') 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 49 | 50 | 60 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /src/components/Table/column-setting-popover.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 78 | 79 | 118 | 119 | 126 | -------------------------------------------------------------------------------- /src/components/Table/index.module.scss: -------------------------------------------------------------------------------- 1 | .sf-table-wrapper { 2 | position: relative; 3 | display: flex; 4 | width: 100%; 5 | display: -webkit-flex; 6 | flex-direction: column; 7 | 8 | .sf-table-header { 9 | padding-bottom: 12px; 10 | display: flex; 11 | display: -webkit-flex; 12 | flex-direction: row; 13 | 14 | .prepend-box { 15 | flex: 1; 16 | } 17 | 18 | .tb-option-box { 19 | display: flex; 20 | display: -webkit-flex; 21 | align-items: center; 22 | justify-content: flex-end; 23 | font-size: 18px; 24 | i { 25 | cursor: pointer; 26 | margin-left: 15px; 27 | } 28 | } 29 | } 30 | 31 | .sf-table-content { 32 | width: 100%; 33 | } 34 | 35 | .sf-table-pagination { 36 | display: flex; 37 | width: 100%; 38 | display: -webkit-flex; 39 | justify-content: flex-end; 40 | width: 100%; 41 | margin-top: 20px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/WarningConfirmButton/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 104 | -------------------------------------------------------------------------------- /src/config/router.config.js: -------------------------------------------------------------------------------- 1 | import { dashboardName } from '@/config/settings' 2 | 3 | // 前端未找到页面路由(固定不用改) 4 | export const NotFoundRouter = { 5 | path: '*', 6 | redirect: '/404', 7 | hidden: true 8 | } 9 | 10 | export const RouteView = { 11 | name: 'RouteView', 12 | render: (h) => h('router-view') 13 | } 14 | 15 | /* Layout */ 16 | import Layout from '@/layout' 17 | 18 | /** 19 | * Note: sub-menu only appear when route children.length >= 1 20 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html 21 | * 22 | * hidden: true if set true, item will not show in the sidebar(default is false) 23 | * alwaysShow: true if set true, will always show the root menu 24 | * if not set alwaysShow, when item has more than one children route, 25 | * it will becomes nested mode, otherwise not show the root menu 26 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb 27 | * name:'router-name' the name is used by (must set!!!) 28 | * meta : { 29 | title: 'title' the name show in sidebar and breadcrumb (recommend set) 30 | icon: 'svg-name'/'el-icon-x' the icon show in the sidebar 31 | noCache: true if set true, the page will no be cached(default is false) 32 | affix: true if set true, the tag will affix in the tags-view 33 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) 34 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set 35 | } 36 | */ 37 | 38 | /** 39 | * constantRoutes 40 | */ 41 | export const constantRoutes = [ 42 | { 43 | path: '/login', 44 | component: () => import('@/views/login/index'), 45 | hidden: true, 46 | meta: { title: '登录' } 47 | }, 48 | 49 | { 50 | path: '/404', 51 | component: () => import('@/views/404'), 52 | hidden: true 53 | }, 54 | 55 | { 56 | path: '/', 57 | component: Layout, 58 | redirect: '/dashboard', 59 | children: [{ 60 | path: 'dashboard', 61 | name: 'Dashboard', 62 | component: () => import('@/views/dashboard/index'), 63 | meta: { title: dashboardName, icon: 'dashboard', affix: true } 64 | }] 65 | }, 66 | 67 | { 68 | path: '/account', 69 | component: Layout, 70 | redirect: '/account/settings', 71 | hidden: true, 72 | children: [ 73 | { 74 | path: 'settings', 75 | name: 'Settings', 76 | component: () => import('@/views/account/settings'), 77 | meta: { title: '个人设置' } 78 | }, 79 | { 80 | path: 'about', 81 | name: 'about', 82 | component: () => import('@/views/account/about'), 83 | meta: { title: '关于' } 84 | } 85 | ] 86 | } 87 | 88 | // { 89 | // path: 'external-link', 90 | // component: Layout, 91 | // children: [ 92 | // { 93 | // path: 'https://panjiachen.github.io/vue-element-admin-site/#/', 94 | // meta: { title: 'External Link', icon: 'link' } 95 | // } 96 | // ] 97 | // }, 98 | 99 | // 404 page must be placed at the end !!! 100 | // { path: '*', redirect: '/404', hidden: true } 101 | ] 102 | -------------------------------------------------------------------------------- /src/config/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'SF管理后台', 4 | 5 | dashboardName: '工作台', 6 | 7 | /** 8 | * @type {boolean} true | false 9 | * @description Whether fix the header 10 | */ 11 | fixedHeader: false, 12 | 13 | /** 14 | * @type {boolean} true | false 15 | * @description Whether show the logo in sidebar 16 | */ 17 | sidebarLogo: false 18 | } 19 | -------------------------------------------------------------------------------- /src/core/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { printANSI } from '@/utils' 2 | import store from '@/store' 3 | import { getToken } from '@/utils/auth' 4 | 5 | export default function Initializer() { 6 | printANSI() 7 | 8 | store.commit('user/SET_TOKEN', getToken()) 9 | } 10 | -------------------------------------------------------------------------------- /src/core/directives/el-drag-dialog/drag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置拖拽 3 | */ 4 | function setupDrag(el) { 5 | const dialogHeaderEl = el.querySelector('.el-dialog__header') 6 | const dragDom = el.querySelector('.el-dialog') 7 | dialogHeaderEl.style.cssText += ';cursor:move;' 8 | dragDom.style.cssText += ';top:0px;' 9 | 10 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); 11 | const getStyle = (function() { 12 | if (window.document.currentStyle) { 13 | return (dom, attr) => dom.currentStyle[attr] 14 | } else { 15 | return (dom, attr) => getComputedStyle(dom, false)[attr] 16 | } 17 | })() 18 | 19 | dialogHeaderEl.onmousedown = e => { 20 | // 鼠标按下,计算当前元素距离可视区的距离 21 | const disX = e.clientX - dialogHeaderEl.offsetLeft 22 | const disY = e.clientY - dialogHeaderEl.offsetTop 23 | 24 | const dragDomWidth = dragDom.offsetWidth 25 | const dragDomHeight = dragDom.offsetHeight 26 | 27 | const screenWidth = document.body.clientWidth 28 | const screenHeight = document.body.clientHeight 29 | 30 | const minDragDomLeft = dragDom.offsetLeft 31 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth 32 | 33 | const minDragDomTop = dragDom.offsetTop 34 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight 35 | 36 | // 获取到的值带px 正则匹配替换 37 | let styL = getStyle(dragDom, 'left') 38 | let styT = getStyle(dragDom, 'top') 39 | 40 | if (styL.includes('%')) { 41 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100) 42 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100) 43 | } else { 44 | styL = +styL.replace(/\px/g, '') 45 | styT = +styT.replace(/\px/g, '') 46 | } 47 | 48 | document.onmousemove = function(e) { 49 | // 通过事件委托,计算移动的距离 50 | let left = e.clientX - disX 51 | let top = e.clientY - disY 52 | 53 | // 边界处理 54 | if (-left > minDragDomLeft) { 55 | left = -minDragDomLeft 56 | } else if (left > maxDragDomLeft) { 57 | left = maxDragDomLeft 58 | } 59 | 60 | if (-top > minDragDomTop) { 61 | top = -minDragDomTop 62 | } else if (top > maxDragDomTop) { 63 | top = maxDragDomTop 64 | } 65 | 66 | // 移动当前元素 67 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` 68 | 69 | // emit onDrag event 70 | // vnode.child.$emit('dragDialog') 71 | } 72 | 73 | document.onmouseup = function(e) { 74 | document.onmousemove = null 75 | document.onmouseup = null 76 | } 77 | } 78 | } 79 | 80 | export default { 81 | bind(el, binding, vnode) { 82 | setupDrag(el) 83 | }, 84 | update(el, binding, vnode) { 85 | if (binding.oldArg === binding.arg) { 86 | return 87 | } 88 | // 全屏下取消拖拽 89 | if (binding.arg === 'fullscreen') { 90 | const dialogHeaderEl = el.querySelector('.el-dialog__header') 91 | const dragDom = el.querySelector('.el-dialog') 92 | // 全屏模式下恢复 93 | dialogHeaderEl.style.cssText += ';cursor:auto;' 94 | dragDom.style.cssText += ';top:0px;left:0px;' 95 | dialogHeaderEl.onmousedown = null 96 | } else { 97 | // 恢复拖拽 98 | setupDrag(el) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/core/directives/el-drag-dialog/index.js: -------------------------------------------------------------------------------- 1 | import drag from './drag' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-drag-dialog', drag) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-drag-dialog'] = drag 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | drag.install = install 13 | export default drag 14 | -------------------------------------------------------------------------------- /src/core/directives/permission/index.js: -------------------------------------------------------------------------------- 1 | import permission from './permission' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('permission', permission) 5 | } 6 | 7 | if (window.Vue) { 8 | window['permission'] = permission 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | permission.install = install 13 | export default permission 14 | -------------------------------------------------------------------------------- /src/core/directives/permission/permission.js: -------------------------------------------------------------------------------- 1 | import { isString, isArray, isBoolean } from 'lodash' 2 | import { checkPermission } from '@/core/permission/check-permission' 3 | 4 | function checkPermissionEl(el, binding) { 5 | const { value } = binding 6 | let hasPermission = false 7 | if (isString(value)) { 8 | hasPermission = checkPermission(value) 9 | } else if (isArray(value)) { 10 | hasPermission = value.some(checkPermission) 11 | } else if (isBoolean(value)) { 12 | hasPermission = value 13 | } 14 | 15 | // hasn't will remove el 16 | if (!hasPermission) { 17 | el.parentNode && el.parentNode.removeChild(el) 18 | } 19 | } 20 | 21 | export default { 22 | // 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 23 | inserted(el, binding) { 24 | checkPermissionEl(el, binding) 25 | }, 26 | // 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变, 27 | // 也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。 28 | update(el, binding) { 29 | checkPermissionEl(el, binding) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/core/directives/table-infinite-scroll/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 来源:https://github.com/yujinpan/el-table-infinite-scroll 3 | */ 4 | 5 | // directive 6 | import elTableInfiniteScroll from './table-infinite-scroll' 7 | 8 | // Vue.use() 9 | elTableInfiniteScroll.install = Vue => { 10 | Vue.directive('el-table-infinite-scroll', elTableInfiniteScroll) 11 | } 12 | 13 | // Vue.component() 14 | export default elTableInfiniteScroll 15 | -------------------------------------------------------------------------------- /src/core/directives/table-infinite-scroll/table-infinite-scroll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 对 element-ui 的无限滚动在 el-table 上使用的封装 3 | */ 4 | import elInfiniteScroll from 'element-ui/lib/infinite-scroll' 5 | 6 | const elScope = 'ElInfiniteScroll' // scope name 7 | const msgTitle = `[el-table-infinite-scroll]: ` // message title 8 | const elTableScrollWrapperClass = '.el-table__body-wrapper' 9 | 10 | export default { 11 | inserted(el, binding, vnode, oldVnode) { 12 | // 获取 table 中的滚动层 13 | const scrollElem = el.querySelector(elTableScrollWrapperClass) 14 | 15 | // 如果没找到元素,返回错误 16 | if (!scrollElem) { 17 | throw new Error(`${msgTitle}找不到 ${elTableScrollWrapperClass} 容器`) 18 | } 19 | 20 | // 设置自动滚动 21 | scrollElem.style.overflowY = 'auto' 22 | 23 | // dom 渲染后 24 | setTimeout(() => { 25 | if (!el.style.height) { 26 | scrollElem.style.height = '400px' 27 | console.warn( 28 | `${msgTitle}请尽量设置 el-table 的高度,可以设置为 auto/100%(自适应高度),未设置会取 400px 的默认值(不然会导致一直加载)` 29 | ) 30 | } 31 | 32 | asyncElOptions(vnode, el, scrollElem) 33 | 34 | // 绑定 infinite-scroll 35 | elInfiniteScroll.inserted(scrollElem, binding, vnode, oldVnode) 36 | 37 | // 将子集的引用放入 el 上,用于 unbind 中销毁事件 38 | el[elScope] = scrollElem[elScope] 39 | }, 0) 40 | }, 41 | componentUpdated(el, binding, vnode) { 42 | asyncElOptions(vnode, el, el.querySelector(elTableScrollWrapperClass)) 43 | }, 44 | unbind: elInfiniteScroll.unbind 45 | } 46 | 47 | /** 48 | * 同步 el-infinite-scroll 的配置项 49 | * @param sourceVNode 50 | * @param sourceElem 51 | * @param targetElem 52 | */ 53 | function asyncElOptions(sourceVNode, sourceElem, targetElem) { 54 | let value 55 | ;['disabled', 'delay', 'immediate'].forEach(name => { 56 | name = 'infinite-scroll-' + name 57 | value = sourceElem.getAttribute(name) 58 | if (value !== null) { 59 | targetElem.setAttribute(name, value) 60 | } 61 | }) 62 | 63 | // fix: windows/chrome 的 scrollTop + clientHeight 与 scrollHeight 不一致的 BUG 64 | const name = 'infinite-scroll-distance' 65 | value = sourceElem.getAttribute(name) 66 | targetElem.setAttribute(name, value < 1 ? 1 : value) 67 | } 68 | -------------------------------------------------------------------------------- /src/core/mixins/message-box.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | openLoadingConfirm({ on, content = '此操作无法恢复,是否继续?', title = '警告', 4 | confirmButtonText = '确定', cancelButtonText = '取消', type = 'warning' }) { 5 | const { confirm, cancel, closed } = on 6 | this.$confirm(content, title, { 7 | confirmButtonText, 8 | cancelButtonText, 9 | type, 10 | beforeClose: (action, instance, done) => { 11 | if (action === 'confirm') { 12 | instance.confirmButtonLoading = true 13 | if (confirm) { 14 | confirm({ 15 | close: done, 16 | done: () => { 17 | instance.confirmButtonLoading = false 18 | } 19 | }) 20 | } 21 | } else { 22 | // 取消时手动自动关闭加载 23 | instance.confirmButtonLoading = false 24 | done() 25 | } 26 | } 27 | }) 28 | .then(act => { 29 | if (closed) { 30 | closed(act) 31 | } 32 | }) 33 | .catch(e => { 34 | if (cancel) { 35 | cancel() 36 | } 37 | }) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/core/mixins/socket-hook.js: -------------------------------------------------------------------------------- 1 | import { mapGetters } from 'vuex' 2 | 3 | export const SOCKET_HOOK_KEY = 'socketHook' 4 | 5 | /** 6 | * 当包含自定义 socketHook 选项时,会进行自动注册socket事件,自动销毁 7 | */ 8 | export default { 9 | computed: { 10 | ...mapGetters(['socketClient']) 11 | }, 12 | watch: { 13 | socketClient() { 14 | // client instance实例发生变化时重新注册 15 | this.$registerSocketEvent() 16 | } 17 | }, 18 | mounted() { 19 | this.$registerSocketEvent() 20 | }, 21 | beforeDestroy() { 22 | this.$unregisterSocketEvent() 23 | }, 24 | methods: { 25 | '$registerSocketEvent'() { 26 | if (this.$options[SOCKET_HOOK_KEY]) { 27 | // mounted map, cache wrapper func 28 | if (!this.$socket) { 29 | this.$socket = new Map() 30 | } 31 | Object.keys(this.$options[SOCKET_HOOK_KEY]).forEach(e => { 32 | if (this.socketClient) { 33 | // bind this 34 | const wrapFunc = this.$options[SOCKET_HOOK_KEY][e].bind(this) 35 | this.$socket.set(e, wrapFunc) 36 | this.socketClient.subscribe(e, wrapFunc) 37 | } 38 | }) 39 | } 40 | }, 41 | '$unregisterSocketEvent'() { 42 | if (this.$options[SOCKET_HOOK_KEY]) { 43 | Object.keys(this.$options[SOCKET_HOOK_KEY]).forEach(e => { 44 | // 增加判断避免被移除掉所有事件 45 | if (this.socketClient && this.$socket && this.$socket.has(e)) { 46 | this.socketClient.unsubscribe(e, this.$socket.get(e)) 47 | } 48 | }) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/core/permission/check-permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * 根据给定值判断是否具有该权限 例如给定 sys.dept.add 5 | */ 6 | export function checkPermission(perm) { 7 | try { 8 | const permissionList = store.getters.perms 9 | 10 | // 转换获取真实的权限名称 11 | const pms = perm.split('.') 12 | let permissionName = null 13 | let cur = store.$api 14 | 15 | // 遍历查找最后所属的权限名称 16 | for (let i = 0; i < pms.length; i++) { 17 | if (i < pms.length - 1) { 18 | cur = cur[pms[i]] 19 | } else { 20 | permissionName = cur._permission[pms[i]] 21 | } 22 | } 23 | 24 | if (typeof permissionName !== 'string') { 25 | return false 26 | } 27 | 28 | // 判断是否存在 29 | return permissionList.indexOf(permissionName) > -1 30 | } catch (e) { 31 | return false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/core/permission/decorator.js: -------------------------------------------------------------------------------- 1 | import { isString } from 'lodash' 2 | 3 | /** 4 | * 权限点,例如 add 5 | * 会与prefix 拼接组成 例如:sys/user/add 6 | */ 7 | export function PermissionAction(action) { 8 | return function(target, propertyKey, _) { 9 | const realAction = action || propertyKey 10 | 11 | if (!target._permission) { 12 | target._permission = {} 13 | } 14 | 15 | // 延迟执行 16 | setTimeout(() => { 17 | // 挂载权限点 18 | target._permission[propertyKey] = 19 | (`${target.$permissonPrefix ? target.$permissonPrefix + '/' : ''}${realAction}`).replace(/\//g, ':') 20 | }, 0) 21 | } 22 | } 23 | 24 | /** 25 | * 权限前缀,例如 sys/user 26 | */ 27 | export function PermissionPrefix(prefix = '') { 28 | return function(constructor) { 29 | if (!isString(prefix)) { 30 | throw new Error('unsupport permission prefix type') 31 | } 32 | 33 | constructor.prototype.$permissonPrefix = prefix 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/permission/index.js: -------------------------------------------------------------------------------- 1 | import { toHump } from '@/utils' 2 | import store from '@/store' 3 | import { checkPermission } from './check-permission' 4 | 5 | /** 6 | * example 7 | * path -> ./modules/user 8 | * Button 9 | * path -> ./modules/sys/user 10 | * Button 11 | * @param Vue 12 | */ 13 | function plugin(Vue) { 14 | if (plugin.installed) { 15 | return 16 | } 17 | 18 | const apiModules = {} 19 | 20 | // 查找所有api列表下以 .class.js 后缀的文件定义 21 | const modulesPermissionFiles = require.context('@/api/', true, /\.class\.js$/) 22 | 23 | // result like this ['./netdisk/test.class.js'] 24 | modulesPermissionFiles.keys().forEach(path => { 25 | // 格式转换 26 | const pathList = path 27 | .substr(2) // 去除 ./ 前缀 28 | .replace('.class.js', '') // 移除文件后缀名 29 | .split('/') // 切割 30 | .map(e => { 31 | // 转换驼峰 32 | return toHump(e) 33 | }) 34 | 35 | // 获取父级文件夹名称 36 | const parents = pathList.slice(0, pathList.length - 1) 37 | const name = pathList[pathList.length - 1] 38 | 39 | const n = path.replace('./', '') 40 | const Rqd = require(`@/api/${n}`).default 41 | 42 | if (!Rqd) { 43 | throw new Error('use .class.js must export default class') 44 | } 45 | 46 | // 实例化 47 | const instance = new Rqd() 48 | 49 | if (parents.length <= 0) { 50 | // 无父级路径时 51 | apiModules[name] = instance 52 | } else { 53 | // 将路径转化为对象 54 | let cur = apiModules 55 | 56 | parents.forEach(k => { 57 | if (!cur[k]) { 58 | cur[k] = {} 59 | } 60 | 61 | cur = cur[k] 62 | }) 63 | 64 | cur[name] = instance 65 | } 66 | }) 67 | 68 | // 挂载所有权限列表到实例上 69 | !Vue.prototype.$api && (Vue.prototype.$api = apiModules) 70 | !store.$api && (store.$api = apiModules) 71 | 72 | // 返回一个函数进行判断是否有该权限 例如 $auth('sys.user.add') 73 | !Vue.prototype.$auth && 74 | Object.defineProperties(Vue.prototype, { 75 | $auth: { 76 | get() { 77 | return checkPermission 78 | } 79 | } 80 | }) 81 | } 82 | 83 | export default plugin 84 | -------------------------------------------------------------------------------- /src/core/socket/event-type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Socket事件名定义 3 | */ 4 | 5 | // 强制踢下线 6 | export const EVENT_KICK = 'kick' 7 | -------------------------------------------------------------------------------- /src/core/use.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ElementUI from 'element-ui' 3 | 4 | import PermissionHelper from '@/core/permission' 5 | import PermissionDirective from '@/core/directives/permission' 6 | 7 | import ELDragDialog from '@/core/directives/el-drag-dialog' 8 | import ELTableInfiniteScroll from '@/core/directives/table-infinite-scroll' 9 | import FormDialog from '@/components/FormDialog' 10 | import ContextMenu from '@/components/ContextMenu' 11 | import EventBus from '@/utils/event-bus' 12 | import VEcharts from '@/components/Echarts' 13 | 14 | Vue.use(EventBus) 15 | // directives 16 | Vue.use(PermissionHelper) 17 | Vue.use(PermissionDirective) 18 | Vue.use(ELDragDialog) 19 | Vue.use(ELTableInfiniteScroll) 20 | 21 | // ui 22 | Vue.use(ElementUI) 23 | Vue.use(FormDialog) 24 | Vue.use(ContextMenu) 25 | Vue.use(VEcharts) 26 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/icons/svg/captcha.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/disk-overview.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-code.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-dir.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-docx.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-excel.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-img.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-music.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-office.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-ppt.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-txt.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-video.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/file-type-zip.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/log.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/monitor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/netdisk-manage.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/netdisk.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/param-config-list.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/param-config.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/permission.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/reload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/role.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/schedule-log.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/schedule.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/serve.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/socket-status-close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/socket-status-connected.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/socket-status-connecting1.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/socket-status-connecting2.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/socket-status-connecting3.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/system.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/task.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 40 | 41 | 57 | 58 | 66 | -------------------------------------------------------------------------------- /src/layout/components/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | -------------------------------------------------------------------------------- /src/layout/components/TableLayout.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 41 | 42 | 85 | -------------------------------------------------------------------------------- /src/layout/components/TagsView/ScrollPane.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 78 | 79 | 95 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | export { default as TagsView } from './TagsView' 5 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 54 | 55 | 96 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import 'element-ui/lib/theme-chalk/index.css'// lang i18n 6 | 7 | import '@/styles/index.scss' // global css 8 | 9 | import App from './App' 10 | import store from './store' 11 | import router from './router' 12 | import bootstrap from '@/core/bootstrap' 13 | 14 | import '@/core/use' // vue use 15 | import '@/icons' // icon 16 | import '@/router/router-guard' // permission control 17 | 18 | Vue.config.productionTip = false 19 | 20 | new Vue({ 21 | el: '#app', 22 | router, 23 | store, 24 | // init localstorage, vuex 25 | created: bootstrap, 26 | render: h => h(App) 27 | }) 28 | -------------------------------------------------------------------------------- /src/router/generator-routers.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout' 2 | import BlankLayout from '@/layout/components/BlankLayout' 3 | import { isExternal } from '@/utils/validate' 4 | import { toHump } from '@/utils' 5 | import { constantRouterComponents } from '@/router' 6 | 7 | /** 8 | * 创建Route对象,需过滤权限节点 9 | * @param {Object} menu 菜单 10 | * @param {Boolean} isRoot 是否为根节点 11 | */ 12 | function createRoute(menu, isRoot) { 13 | // 目录 14 | if (menu.type === 0) { 15 | return { 16 | path: menu.router, 17 | component: isRoot ? Layout : BlankLayout, 18 | alwaysShow: true, 19 | meta: { title: menu.name, icon: menu.icon } 20 | } 21 | } 22 | // 外链菜单 23 | if (isExternal(menu.router)) { 24 | return { 25 | path: `external-link${menu.id}`, 26 | component: Layout, 27 | children: [ 28 | { 29 | path: menu.router, 30 | meta: { title: menu.name, icon: menu.icon } 31 | } 32 | ] 33 | } 34 | } 35 | const component = constantRouterComponents[menu.viewPath] 36 | if (!component) { 37 | return undefined 38 | } 39 | // 根菜单 40 | return isRoot ? { 41 | path: menu.router, 42 | redirect: `${menu.router}/index`, 43 | component: Layout, 44 | children: [ 45 | { 46 | path: 'index', 47 | name: toHump(menu.viewPath), 48 | component, 49 | meta: { 50 | title: menu.name, 51 | icon: menu.icon, 52 | noCache: !menu.keepalive 53 | } 54 | } 55 | ] 56 | } 57 | : { 58 | path: menu.router, 59 | name: toHump(menu.viewPath), 60 | component, 61 | meta: { 62 | title: menu.name, 63 | icon: menu.icon, 64 | noCache: !menu.keepalive 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Filter asynchronous routing tables by recursion 71 | * @param routes asyncRoutes 72 | * @param roles 73 | */ 74 | export function filterAsyncRoutes(routes, parentRoute) { 75 | const res = [] 76 | 77 | routes.forEach(route => { 78 | if (route.type === 2 || !route.isShow) { 79 | // 如果是权限或隐藏直接跳过 80 | return 81 | } 82 | // 根级别菜单渲染 83 | let realRoute 84 | if (!parentRoute && !route.parentId && route.type === 1) { 85 | // 根菜单 86 | realRoute = createRoute(route, true) 87 | } else if (!parentRoute && !route.parentId && route.type === 0) { 88 | // 目录 89 | const childRoutes = filterAsyncRoutes(routes, route) 90 | realRoute = createRoute(route, true) 91 | if (childRoutes && childRoutes.length > 0) { 92 | realRoute.redirect = childRoutes[0].path 93 | realRoute.children = childRoutes 94 | } 95 | } else if (parentRoute && parentRoute.id === route.parentId && route.type === 1) { 96 | // 子菜单 97 | realRoute = createRoute(route, false) 98 | } else if (parentRoute && parentRoute.id === route.parentId && route.type === 0) { 99 | // 如果还是目录,继续递归 100 | const childRoute = filterAsyncRoutes(routes, route) 101 | realRoute = createRoute(route, false) 102 | if (childRoute && childRoute.length > 0) { 103 | realRoute.redirect = childRoute[0].path 104 | realRoute.children = childRoute 105 | } 106 | } 107 | // add curent route 108 | if (realRoute) { 109 | res.push(realRoute) 110 | } 111 | }) 112 | return res 113 | } 114 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import { constantRoutes } from '@/config/router.config' 4 | 5 | Vue.use(Router) 6 | 7 | // generate components map 8 | export const constantRouterComponents = {} 9 | 10 | // auto load 11 | const modulesFiles = require.context('./modules', true, /\.js$/) 12 | 13 | modulesFiles.keys().forEach(path => { 14 | const value = modulesFiles(path).default 15 | 16 | // mouted 17 | Object.keys(value).forEach(ele => { 18 | constantRouterComponents[ele] = value[ele] 19 | }) 20 | }) 21 | 22 | // create router 23 | const createRouter = () => new Router({ 24 | mode: 'history', // require service support 25 | scrollBehavior: () => ({ y: 0 }), 26 | routes: constantRoutes 27 | }) 28 | 29 | const router = createRouter() 30 | 31 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 32 | export function resetRouter() { 33 | const newRouter = createRouter() 34 | router.matcher = newRouter.matcher // reset router 35 | } 36 | 37 | export default router 38 | -------------------------------------------------------------------------------- /src/router/modules/netdisk.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'views/netdisk/manage': () => import('@/views/netdisk/manage'), 3 | 'views/netdisk/overview': () => import('@/views/netdisk/overview') 4 | } 5 | -------------------------------------------------------------------------------- /src/router/modules/system.js: -------------------------------------------------------------------------------- 1 | /** 2 | * system module 3 | */ 4 | export default { 5 | 'views/system/permission/menu': () => import('@/views/system/permission/menu'), 6 | 'views/system/permission/user': () => import('@/views/system/permission/user'), 7 | 'views/system/permission/role': () => import('@/views/system/permission/role'), 8 | 'views/system/monitor/online': () => import('@/views/system/monitor/online'), 9 | 'views/system/monitor/login-log': () => import('@/views/system/monitor/login-log'), 10 | 'views/system/monitor/serve': () => import('@/views/system/monitor/serve'), 11 | 'views/system/schedule/task': () => import('@/views/system/schedule/task'), 12 | 'views/system/schedule/log': () => import('@/views/system/schedule/log'), 13 | 'views/system/param-config/config-list': () => import('@/views/system/param-config/config-list') 14 | } 15 | -------------------------------------------------------------------------------- /src/router/router-guard.js: -------------------------------------------------------------------------------- 1 | import router from '.' 2 | import store from '../store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | import { constantRoutes } from '@/config/router.config' 9 | 10 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 11 | 12 | const whiteList = ['/login'] // no redirect whitelist 13 | 14 | router.beforeEach(async(to, from, next) => { 15 | // start progress bar 16 | NProgress.start() 17 | 18 | // set page title 19 | document.title = getPageTitle(to.meta.title) 20 | 21 | // determine whether the user has logged in 22 | const hasToken = getToken() 23 | 24 | if (hasToken) { 25 | if (to.path === '/login') { 26 | // if is logged in, redirect to the home page 27 | next({ path: '/' }) 28 | NProgress.done() 29 | } else { 30 | // 判断是否有权限路由 31 | const hasRoutes = store.getters.permissionRoutes && 32 | (store.getters.permissionRoutes.length > constantRoutes.length) 33 | 34 | if (hasRoutes) { 35 | // pass 36 | next() 37 | } else { 38 | try { 39 | // get user info 40 | const { menus } = await store.dispatch('user/getInfo') 41 | 42 | // generate accessible routes map based on roles 43 | const accessRoutes = await store.dispatch('permission/generateRoutes', menus) 44 | 45 | // dynamically add accessible routes 46 | router.addRoutes(accessRoutes) 47 | 48 | // hack method to ensure that addRoutes is complete 49 | // set the replace: true, so the navigation will not leave a history record 50 | next({ ...to, replace: true }) 51 | } catch (error) { 52 | // remove token 53 | await store.dispatch('user/resetToken') 54 | Message.error(`${error}` || '发生了一些未知的错误,请重试!') 55 | 56 | // go to login page to re-login 57 | next(`/login?redirect=${to.path}`) 58 | NProgress.done() 59 | } 60 | } 61 | } 62 | } else { 63 | /* has no token*/ 64 | 65 | if (whiteList.indexOf(to.path) !== -1) { 66 | // in the free login whitelist, go directly 67 | next() 68 | } else { 69 | // other pages that do not have permission to access are redirected to the login page. 70 | next(`/login?redirect=${to.path}`) 71 | NProgress.done() 72 | } 73 | } 74 | }) 75 | 76 | router.afterEach(() => { 77 | // finish progress bar 78 | NProgress.done() 79 | }) 80 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name, 7 | perms: state => state.user.perms, 8 | permissionRoutes: state => state.permission.routes, 9 | socketClient: state => state.ws.client, 10 | socketStatus: state => state.ws.status 11 | } 12 | export default getters 13 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | 5 | Vue.use(Vuex) 6 | 7 | // https://webpack.js.org/guides/dependency-management/#requirecontext 8 | const modulesFiles = require.context('./modules', true, /\.js$/) 9 | 10 | // you do not need `import app from './modules/app'` 11 | // it will auto require all vuex module from modules file 12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 13 | // set './app.js' => 'app' 14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 15 | const value = modulesFiles(modulePath) 16 | modules[moduleName] = value.default 17 | return modules 18 | }, {}) 19 | 20 | const store = new Vuex.Store({ 21 | modules, 22 | getters 23 | }) 24 | 25 | export default store 26 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import storage from 'store' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: storage.get('sidebarStatus') ? !!+storage.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | storage.set('sidebarStatus', 1) 17 | } else { 18 | storage.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | storage.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { constantRoutes } from '@/config/router.config' 2 | import { filterAsyncRoutes } from '@/router/generator-routers' 3 | import { NotFoundRouter } from '@/config/router.config' 4 | 5 | const state = { 6 | routes: [], 7 | addRoutes: [] 8 | } 9 | 10 | const mutations = { 11 | SET_ROUTES: (state, routes) => { 12 | state.addRoutes = routes 13 | state.routes = constantRoutes.concat(routes) 14 | } 15 | } 16 | 17 | const actions = { 18 | generateRoutes({ commit }, menus) { 19 | return new Promise(resolve => { 20 | // 后端路由json进行转换成真正的router map 21 | const accessRoutes = filterAsyncRoutes(menus, null) 22 | // 404 route must be end 23 | accessRoutes.push(NotFoundRouter) 24 | commit('SET_ROUTES', accessRoutes) 25 | resolve(accessRoutes) 26 | }) 27 | }, 28 | resetRoutes({ commit }) { 29 | return new Promise(resolve => { 30 | commit('SET_ROUTES', []) 31 | resolve() 32 | }) 33 | } 34 | } 35 | 36 | export default { 37 | namespaced: true, 38 | state, 39 | mutations, 40 | actions 41 | } 42 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/config/settings' 2 | 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | sidebarLogo: sidebarLogo 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | // eslint-disable-next-line no-prototype-builtins 14 | if (state.hasOwnProperty(key)) { 15 | state[key] = value 16 | } 17 | } 18 | } 19 | 20 | const actions = { 21 | changeSetting({ commit }, data) { 22 | commit('CHANGE_SETTING', data) 23 | } 24 | } 25 | 26 | export default { 27 | namespaced: true, 28 | state, 29 | mutations, 30 | actions 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login } from '@/api/login' 2 | import { logout, getInfo, permmenu } from '@/api/account' 3 | import { setToken, removeToken } from '@/utils/auth' 4 | import { resetRouter } from '@/router' 5 | 6 | const state = { 7 | token: '', 8 | name: '', 9 | avatar: '', 10 | // like [ 'sys:user:add', 'sys:user:update' ] 11 | perms: [] 12 | } 13 | 14 | const mutations = { 15 | RESET_STATE: state => { 16 | state.token = '' 17 | state.name = '' 18 | state.avatar = '' 19 | state.perms = [] 20 | }, 21 | SET_TOKEN: (state, token) => { 22 | state.token = token 23 | }, 24 | SET_NAME: (state, name) => { 25 | state.name = name 26 | }, 27 | SET_AVATAR: (state, avatar) => { 28 | state.avatar = avatar 29 | }, 30 | SET_PERMS: (state, perms) => { 31 | state.perms = perms 32 | } 33 | } 34 | 35 | const actions = { 36 | // 管理员登录 37 | login({ commit }, loginInfo) { 38 | const { username, password, captchaId, verifyCode } = loginInfo 39 | return new Promise((resolve, reject) => { 40 | login({ 41 | username: username.trim(), 42 | password: password.trim(), 43 | captchaId: captchaId.trim(), 44 | verifyCode: verifyCode.trim() 45 | }) 46 | .then(response => { 47 | const { data } = response 48 | commit('SET_TOKEN', data.token) 49 | setToken(data.token) 50 | resolve() 51 | }) 52 | .catch(error => { 53 | reject(error) 54 | }) 55 | }) 56 | }, 57 | 58 | // 初始化用户及权限信息 59 | getInfo({ commit, dispatch }) { 60 | return new Promise((resolve, reject) => { 61 | Promise.all([permmenu(), getInfo()]).then((results) => { 62 | const pm = results[0].data 63 | const info = results[1].data 64 | const { perms, menus } = pm 65 | 66 | // set store 67 | commit('SET_PERMS', perms) 68 | commit('SET_NAME', info.name) 69 | commit('SET_AVATAR', info.headImg) 70 | 71 | // init socket 72 | dispatch('ws/initSocket', null, { root: true }) 73 | 74 | resolve({ menus, perms, user: info }) 75 | }).catch(error => { 76 | reject(error) 77 | }) 78 | }) 79 | }, 80 | 81 | // 管理员退出 82 | logout({ commit, dispatch }) { 83 | return new Promise((resolve, reject) => { 84 | logout() 85 | .then(() => { 86 | // 清除localstorage存储的token 87 | removeToken() 88 | 89 | // 清除store存储的routes 90 | dispatch('permission/resetRoutes', null, { root: true }) 91 | // disconnect socket 92 | dispatch('ws/closeSocket', null, { root: true }) 93 | 94 | // clean vue-router 95 | resetRouter() 96 | commit('RESET_STATE') 97 | resolve() 98 | }) 99 | .catch(error => { 100 | reject(error) 101 | }) 102 | }) 103 | }, 104 | 105 | // 清除token 106 | resetToken({ commit }) { 107 | return new Promise(resolve => { 108 | // 清除localstorage存储的token 109 | removeToken() 110 | 111 | // reset state 112 | commit('RESET_STATE') 113 | resolve() 114 | }) 115 | } 116 | } 117 | 118 | export default { 119 | namespaced: true, 120 | state, 121 | mutations, 122 | actions 123 | } 124 | -------------------------------------------------------------------------------- /src/store/modules/ws.js: -------------------------------------------------------------------------------- 1 | import { EVENT_KICK } from '@/core/socket/event-type' 2 | import { SocketIOWrapper, SocketStatus } from '@/core/socket/socket-io' 3 | import { MessageBox } from 'element-ui' 4 | 5 | const state = { 6 | // socket wrapper 实例 7 | client: null, 8 | // socket 连接状态 9 | status: SocketStatus.CLOSE 10 | } 11 | 12 | const mutations = { 13 | SET_CLIENT(state, client) { 14 | state.client = client 15 | }, 16 | SET_STATUS(state, status) { 17 | if (state.status === status) { 18 | return 19 | } 20 | state.status = status 21 | } 22 | } 23 | 24 | const actions = { 25 | // 初始化Socket 26 | initSocket({ commit, state, dispatch }) { 27 | // check is init 28 | if (state.client && state.client.isConnected()) { 29 | return 30 | } 31 | const ws = new SocketIOWrapper() 32 | ws.subscribe(EVENT_KICK, async(data) => { 33 | // reset token 34 | await dispatch('user/resetToken', null, { root: true }) 35 | MessageBox.confirm(`您已被管理员${data.operater}踢下线!`, '警告', { 36 | confirmButtonText: '重新登录', 37 | cancelButtonText: '取消', 38 | type: 'warning' 39 | }).finally(() => { 40 | // 刷新页面 41 | window.location.reload() 42 | }) 43 | }) 44 | commit('SET_CLIENT', ws) 45 | }, 46 | 47 | // 关闭Socket连接 48 | closeSocket({ commit, state }) { 49 | state.client && state.client.close() 50 | commit('SET_CLIENT', null) 51 | } 52 | } 53 | 54 | export default { 55 | namespaced: true, 56 | state, 57 | mutations, 58 | actions 59 | } 60 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 500 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | 46 | // to fix el-date-picker css style 47 | .el-range-separator { 48 | box-sizing: content-box; 49 | } 50 | 51 | .el-table { 52 | .el-table__cell { 53 | font-weight: 500 !important; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 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 .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 transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .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 .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#ffffff; 3 | $menuActiveText:#ffffff; 4 | $menuActiveBg: #1890ff; 5 | $subMenuActiveText:#ffffff; // https://github.com/ElemeFE/element/issues/12951 6 | 7 | $menuBg:#2f3447; 8 | $menuHover:#1890ff; 9 | 10 | $subMenuBg:#2b3043; 11 | $subMenuHover:#1890ff; 12 | 13 | $sideBarWidth: 256px; 14 | 15 | // the :export directive is the magic sauce for webpack 16 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 17 | :export { 18 | menuText: $menuText; 19 | menuActiveText: $menuActiveText; 20 | subMenuActiveText: $subMenuActiveText; 21 | menuBg: $menuBg; 22 | menuHover: $menuHover; 23 | subMenuBg: $subMenuBg; 24 | subMenuHover: $subMenuHover; 25 | sideBarWidth: $sideBarWidth; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import storage from 'store' 2 | 3 | const TokenKey = 'admin_auth_token' 4 | 5 | export function getToken() { 6 | return storage.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return storage.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return storage.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/event-bus.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | const install = (Vue) => { 4 | const emitter = mitt() 5 | Vue.prototype.$eventBus = emitter 6 | } 7 | 8 | export default install 9 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/config/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Admin Template' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | const UNKNOWN_ERROR = '未知错误,请重试' 7 | 8 | // create an axios instance 9 | const service = axios.create({ 10 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 11 | // withCredentials: true, // send cookies when cross-domain requests 12 | timeout: 10000 // request timeout 13 | }) 14 | 15 | // request interceptor 16 | service.interceptors.request.use( 17 | config => { 18 | // do something before request is sent 19 | 20 | if (store.getters.token) { 21 | // let each request carry token 22 | // ['X-Token'] is a custom headers key 23 | // please modify it according to the actual situation 24 | config.headers['Authorization'] = getToken() 25 | } 26 | return config 27 | }, 28 | error => { 29 | // do something with request error 30 | 31 | return Promise.reject(error) 32 | } 33 | ) 34 | 35 | // response interceptor 36 | service.interceptors.response.use( 37 | /** 38 | * If you want to get http information such as headers or status 39 | * Please return response => response 40 | */ 41 | 42 | /** 43 | * Determine the request status by custom code 44 | * Here is just an example 45 | * You can also judge the status by HTTP Status Code 46 | */ 47 | response => { 48 | const res = response.data 49 | 50 | // if the custom code is not 200, it is judged as an error. 51 | if (res.code !== 200) { 52 | Message({ 53 | message: res.message || UNKNOWN_ERROR, 54 | type: 'error', 55 | duration: 5 * 1000 56 | }) 57 | 58 | // Illegal token 59 | if (res.code === 11001 || res.code === 11002) { 60 | // to re-login 61 | MessageBox.confirm(res.message || '账号异常,您可以取消停留在该页上,或重新登录', '警告', { 62 | confirmButtonText: '重新登录', 63 | cancelButtonText: '取消', 64 | type: 'warning' 65 | }).then(() => { 66 | store.dispatch('user/resetToken').then(() => { 67 | location.reload() 68 | }) 69 | }).catch(() => {}) 70 | } 71 | 72 | // throw other 73 | const error = new Error(res.message || UNKNOWN_ERROR) 74 | error.code = res.code 75 | return Promise.reject(error) 76 | } else { 77 | return res 78 | } 79 | }, 80 | error => { 81 | // 处理 422 或者 500 的错误异常提示 82 | const errMsg = (error && error.response && error.response.data && error.response.data.message) ? error.response.data.message : UNKNOWN_ERROR 83 | Message({ 84 | message: errMsg, 85 | type: 'error', 86 | duration: 5 * 1000 87 | }) 88 | return Promise.reject(error) 89 | } 90 | ) 91 | 92 | export default service 93 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | -------------------------------------------------------------------------------- /src/vendor/Export2Zip.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { saveAs } from 'file-saver' 3 | import JSZip from 'jszip' 4 | 5 | export function export_txt_to_zip(th, jsonData, txtName, zipName) { 6 | const zip = new JSZip() 7 | const txt_name = txtName || 'file' 8 | const zip_name = zipName || 'file' 9 | const data = jsonData 10 | let txtData = `${th}\r\n` 11 | data.forEach((row) => { 12 | let tempStr = '' 13 | tempStr = row.toString() 14 | txtData += `${tempStr}\r\n` 15 | }) 16 | zip.file(`${txt_name}.txt`, txtData) 17 | zip.generateAsync({ 18 | type: "blob" 19 | }).then((blob) => { 20 | saveAs(blob, `${zip_name}.zip`) 21 | }, (err) => { 22 | alert('导出失败') 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/views/account/about.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 88 | 89 | 111 | -------------------------------------------------------------------------------- /src/views/account/components/basic.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 71 | 72 | 82 | -------------------------------------------------------------------------------- /src/views/account/components/safe.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 90 | 91 | 101 | -------------------------------------------------------------------------------- /src/views/account/settings.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 45 | 46 | 58 | -------------------------------------------------------------------------------- /src/views/netdisk/components/overview-header-item.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 62 | -------------------------------------------------------------------------------- /src/views/system/monitor/login-log.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 36 | -------------------------------------------------------------------------------- /src/views/system/monitor/online.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 81 | 82 | -------------------------------------------------------------------------------- /src/views/system/permission/components/menu-icon-selector.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/views/system/permission/components/permission-cascader.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/views/system/permission/role.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 69 | 70 | 73 | -------------------------------------------------------------------------------- /src/views/system/schedule/log.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 64 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('timestamp string', () => { 9 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('ten digits timestamp', () => { 12 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('new Date', () => { 15 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 16 | }) 17 | it('format', () => { 18 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 19 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 20 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 24 | }) 25 | it('get the day of the week', () => { 26 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 27 | }) 28 | it('empty argument', () => { 29 | expect(parseTime()).toBeNull() 30 | }) 31 | 32 | it('null', () => { 33 | expect(parseTime(null)).toBeNull() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | --------------------------------------------------------------------------------