├── .env ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── index.html ├── markimg ├── 0.png ├── 1.png ├── 2.png └── 3.png ├── package-lock.json ├── package.json ├── public ├── api.js ├── epgms │ ├── account │ │ ├── getUserInfo.json │ │ ├── login.json │ │ └── logout.json │ └── template │ │ └── list.json ├── favicon.ico └── other │ └── manage │ └── add.json ├── src ├── App.vue ├── api │ ├── epgms │ │ ├── account │ │ │ ├── getUserInfo.js │ │ │ ├── login.js │ │ │ └── logout.js │ │ ├── index.js │ │ └── template │ │ │ └── list.js │ └── other │ │ ├── index.js │ │ └── manage │ │ └── add.js ├── assets │ ├── 404.png │ ├── 404_cloud.png │ ├── logo.png │ ├── user.png │ └── welcome.png ├── components │ ├── ColorPicker │ │ └── index.vue │ └── Layout │ │ ├── components │ │ ├── appmain │ │ │ └── index.vue │ │ ├── navbar │ │ │ ├── components │ │ │ │ ├── breadcrumb.vue │ │ │ │ ├── hamburger.vue │ │ │ │ └── userInfo.vue │ │ │ └── index.vue │ │ └── sidebar │ │ │ ├── components │ │ │ ├── filterRouter.js │ │ │ ├── item.vue │ │ │ └── logo.vue │ │ │ ├── index.vue │ │ │ └── sidebarItem.vue │ │ └── index.vue ├── core │ ├── addRoutePermission.js │ ├── auth.js │ ├── get-page-title.js │ ├── permission.js │ └── request.js ├── directive │ └── index.js ├── elementui.js ├── main.js ├── mixin │ └── global.js ├── plugin │ └── icons │ │ ├── svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── link.svg │ │ ├── logo.svg │ │ ├── logo1.svg │ │ ├── nest.svg │ │ ├── portal-create.svg │ │ ├── portal.svg │ │ ├── template-audit.svg │ │ ├── template-create.svg │ │ ├── template.svg │ │ ├── user-paint.svg │ │ └── user.svg │ │ ├── svgBuilder.js │ │ └── svgIcon.vue ├── router │ └── index.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── skin.js │ │ └── user.js ├── stylus │ ├── chrome.styl │ ├── elementUI.styl │ ├── index.styl │ ├── mixin.styl │ ├── sidebar.styl │ ├── transition.styl │ └── variables.styl └── views │ ├── 404 │ └── index.vue │ ├── dashboard │ └── index.vue │ ├── login │ └── index.vue │ ├── portal │ └── create │ │ └── index.vue │ └── template │ ├── audit │ └── index.vue │ └── create │ └── index.vue └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | # 是否显示系统标题 2 | VITE_SYSTEM_SWITCH=true 3 | 4 | # 系统标题名称 5 | VITE_SYSTEM_NAME="vue3-admin-template-vite" 6 | 7 | # 系统标题svg 8 | VITE_SYSTEM_LOGO="logo1" 9 | 10 | # 权限类型 1: 静态权限 2: 动态权限 11 | VITE_POWER_TYPE=2 12 | 13 | # 版本号 14 | VITE_VERSION="Beta 0.0.1" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "printWidth": 70 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Maxfengyan 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 | ## vue3-element-vite-template 2 | 3 | > 一款 vue3 开箱即用的纯净的后台管理系统,只包含 ui/权限/axios 封装/svg 等 4 | 5 | [github在线预览地址](https://maxfengyan.github.io/vue3-admin-template-vite/#/)
6 | 7 | [gitee在线预览地址](https://maxfengyan.gitee.io/vue3-admin-template-vite/#/) 8 | 9 | > 此项目是在 pan 大佬的vue-admin-template项目基础上进行功能微改的 vue3 版本,基本沿用 vue-admin-template 项目构造,添加了一些更贴合实际项目的改动和功能。如果有使用过 vue-admin-template 或者 vue-element-admin 的同学,轻松上手无缝切换。没使用过的话可以当作 vue3 的入门练习。 10 | 11 | #### 技术栈:vue3.x + vite2.x + element-plus + vue-router4.x + vuex4.x 12 | 13 | #### Build Setup 14 | 15 | ```bash 16 | git clone https://gitee.com/Maxfengyan/vue3-admin-template-vite.git 17 | cd vue3-admin-template-vite 18 | npm install 19 | npm run dev 20 | ``` 21 | #### 项目截图 22 | 23 | ![登录](./markimg/0.png) 24 | 25 | ![登录](./markimg/1.png) 26 | 27 | ![登录](./markimg/2.png) 28 | 29 | ![登录](./markimg/3.png) 30 | 31 | #### 相对于 vue-admin-template 改动 32 | 33 | 1. **Vite**:使用 vite 构建工具,放弃 webpack。(vite 真香) 34 | 2. **Axios 封装请求**:
35 | 36 | > (1) 根据实际项目开发需求,系统中可能会请求多个不同的 **baseurl**,为方便处理封装 axios 类(自定义修改响应拦截在*src/core/request.js*)。
37 | 38 | > (2) 为了项目打包后也可以灵活调整后端接口地址,在打包后自动切换为闭包形式读取 baseurl(_public/api.js_),方便项目打包后部署时随便修改接口地址。 39 | 40 | 3. **权限过滤拦截**:权限处理更加颗粒化/灵活化。
41 | 42 | > (1) 在.env 配置文件中设置 VITE_POWER_TYPE 是否启用**动态权限**路由功能
43 | 44 | > (2) 改变动态路由逻辑,原本通过角色来确定路由权限,现改为每个路由中添加各自的唯一的路由标识,通过接口获取用户的权限标识,进行颗粒化的比对分配权限,更加灵活一些。
45 | 46 | > (3) 权限数据并非统一结构,可以结合项目进行调整(修改逻辑在*src/core/addRoutePermission*) 47 | 48 | 4. **Sidebar**:剔除了侧边栏适配 mobile 的响应式(懒...),调整代码逻辑,在.env 文件中设置。 49 | ``` 50 | VITE_SYSTEM_SWITCH: 是否展示系统标题和logo 51 | VITE_SYSTEM_NAME: 系统标题 52 | VITE_SYSTEM_LOGO: 系统 logo 53 | ``` 54 | 5. **svg**:下载 svg 文件存放于*src/plugin/icons/svg/*下,使用方式如下 55 | ```html 56 | 57 | 58 | ``` 59 | 6. **mockJs**:因为本地开发时使用 **mockJs** 无法在浏览器捕捉到响应信息,故放弃,可采用以下两种方法:
60 | 61 | > (1) easy-mock(推荐):服务器搭建 easy-mock 服务进行接口管理,前后端开发方便协作规范。
62 | 63 | > (2) public:vite 项目 public 文件夹下文件会自动变为静态资源服务,可以在 public 文件夹下根据接口规范创建相应路径的 json 文件达到 ajax 请求目的。 64 | 65 | 66 | tips:**如果需要前后端联调接口需要在 vite.config.js 配置 proxy** 67 | 68 | #### 目录结构 69 | 70 | ``` 71 | |- public // 公共静态资源以及模拟接口json文件 72 | |- src // 开发文件夹 73 | | |- api // 项目请求接口 74 | | |- assets // 静态图片 75 | | |- components // 公共组件 76 | | | |- ColorPicker // 颜色组件(修改全局皮肤,未完成) 77 | | | |- Layout // 项目布局核心组件 78 | | |- core // 封装核心功能 79 | | | |- addRoutePermission // 权限校验 80 | | | |- auth // 存放缓存 81 | | | |- get-page-title // 网页标题 82 | | | |- request // Axios封装 83 | | |- directive // 自定义指令 84 | | |- mixin // vue mixin 85 | | | |- global // 读取全局参数(颜色组件相关,未用) 86 | | |- plugin // 全局插件 87 | | | |- icons // 封装svg plugin 88 | | |- router // vue路由 89 | | |- store // vuex 90 | | | |- modules // vuex modules 91 | | | |- getters // vuex getters 92 | | | |- index // vuex入口 93 | | |- stylus // css-stylus 94 | | | |- chrome // chrome 覆盖原生css 95 | | | |- elementUI // element 覆盖原生css 96 | | | |- index // 入口styl 97 | | | |- sidebar // 侧边栏 98 | | | |- transition // 过渡动画 99 | | | |- variables // stylus变量 100 | | |- views // 页面 101 | | | |- 404 // 404 102 | | | |- dashboard // 根页面 103 | | | |- login // 登录 104 | | |- App // vue入口组件 105 | | |- elementui // 按需加载elementui组件 106 | | |- main // vue入口文件 107 | |- .env // 全局配置参数 108 | |- vite.config.js // vite配置文件 109 | ``` 110 | 111 | #### 未完待续 112 | 113 | - 实现 vue-element-admin中的组件功能 114 | - typescript 版本 115 | - 采用setup方式,进行精简代码 116 | 117 | #### 路过的同学不要走 😀,如果本项目对你有帮助,请给我~~一键三连~~ 一个 star⭐~ 118 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Vite App 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /markimg/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/markimg/0.png -------------------------------------------------------------------------------- /markimg/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/markimg/1.png -------------------------------------------------------------------------------- /markimg/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/markimg/2.png -------------------------------------------------------------------------------- /markimg/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/markimg/3.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build", 6 | "serve": "vite preview" 7 | }, 8 | "dependencies": { 9 | "@element-plus/icons-vue": "^2.0.10", 10 | "axios": "^0.21.1", 11 | "element-plus": "^2.2.32", 12 | "js-cookie": "^2.2.1", 13 | "normalize.css": "^8.0.1", 14 | "nprogress": "^0.2.0", 15 | "path-to-regexp": "^6.2.0", 16 | "vue": "^3.2.6", 17 | "vue-router": "^4.0.11", 18 | "vuex": "^4.0.2" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-vue": "^1.6.0", 22 | "@vue/compiler-sfc": "^3.2.6", 23 | "eslint-config-prettier": "^8.3.0", 24 | "eslint-plugin-prettier": "^3.4.0", 25 | "ip": "^1.1.5", 26 | "stylus": "^0.54.8", 27 | "svg-sprite-loader": "^6.0.7", 28 | "unplugin-auto-import": "^0.15.0", 29 | "unplugin-element-plus": "^0.7.0", 30 | "unplugin-vue-components": "^0.24.0", 31 | "vite": "^2.5.1", 32 | "vite-plugin-style-import": "^1.2.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/api.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.api = { 3 | epgms: 4 | "http://10.10.8.14:9999/mock/5f23d33facd7311a719430ed/epgms", 5 | others: 6 | "http://10.10.8.14:9999/mock/5f23d3f0acd7311a719430f2/boss", 7 | }; 8 | })(); 9 | -------------------------------------------------------------------------------- /public/epgms/account/getUserInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "resultCode": "0", 3 | "resultMessage": "获取成功", 4 | "data": { 5 | "roles": [ 6 | "template", 7 | "template-create", 8 | "template-audit", 9 | "portal", 10 | "portal-create" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/epgms/account/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "resultCode": "0", 3 | "resultMessage": "登录成功", 4 | "data": { 5 | "userName": "admin", 6 | "avator": "", 7 | "token": "xxxxxxxxxxxxxxxxxxxxxxxx", 8 | "role": "admin", 9 | "userAccount": "mafengyan" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/epgms/account/logout.json: -------------------------------------------------------------------------------- 1 | { 2 | "resultCode": "0", 3 | "resultMessage": "退出成功", 4 | "data": {} 5 | } 6 | -------------------------------------------------------------------------------- /public/epgms/template/list.json: -------------------------------------------------------------------------------- 1 | { 2 | "resultCode": "0", 3 | "resultMessage": "请求", 4 | "data": [1, 2, 3, 4, 5] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/public/favicon.ico -------------------------------------------------------------------------------- /public/other/manage/add.json: -------------------------------------------------------------------------------- 1 | { 2 | "resultCode": "0", 3 | "resultMessage": "新增成功", 4 | "data": {} 5 | } 6 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | -------------------------------------------------------------------------------- /src/api/epgms/account/getUserInfo.js: -------------------------------------------------------------------------------- 1 | import epgmsRequest from "../index"; 2 | 3 | export const getUserInfo = (data) => { 4 | return epgmsRequest({ 5 | url: "/account/getUserInfo.json", 6 | method: "post", 7 | data, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/epgms/account/login.js: -------------------------------------------------------------------------------- 1 | import epgmsRequest from "../index"; 2 | 3 | export const login = (data) => { 4 | return epgmsRequest({ 5 | url: "/account/login.json", 6 | method: "post", 7 | data, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/epgms/account/logout.js: -------------------------------------------------------------------------------- 1 | import epgmsRequest from "../index"; 2 | 3 | export const logout = () => { 4 | return epgmsRequest({ 5 | url: "/account/logout.json", 6 | method: "post", 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /src/api/epgms/index.js: -------------------------------------------------------------------------------- 1 | import Axios from "@/core/request"; 2 | 3 | const url = import.meta.env.DEV ? "/epgms" : window.api.epgms; 4 | 5 | const epgmsRequest = new Axios(url).getInstance(); 6 | 7 | export default epgmsRequest; 8 | -------------------------------------------------------------------------------- /src/api/epgms/template/list.js: -------------------------------------------------------------------------------- 1 | import epgmsRequest from "../index"; 2 | 3 | export const getList = (data) => { 4 | return epgmsRequest({ 5 | url: "/template/list.json", 6 | method: "post", 7 | data, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/other/index.js: -------------------------------------------------------------------------------- 1 | import Axios from "@/core/request"; 2 | 3 | const url = import.meta.env.DEV ? "/other" : window.api.others; 4 | 5 | const otherRequest = new Axios(url).getInstance(); 6 | 7 | export default otherRequest; 8 | -------------------------------------------------------------------------------- /src/api/other/manage/add.js: -------------------------------------------------------------------------------- 1 | import otherRequest from "../index"; 2 | 3 | export const manageAdd = (data) => { 4 | return otherRequest({ 5 | url: "/manage/add.json", 6 | method: "post", 7 | data, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/assets/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/src/assets/404.png -------------------------------------------------------------------------------- /src/assets/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/src/assets/404_cloud.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/src/assets/user.png -------------------------------------------------------------------------------- /src/assets/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxfengyan/vue3-admin-template-vite/294c8de4c4f46d9cf12780f896002fe6f69c2d04/src/assets/welcome.png -------------------------------------------------------------------------------- /src/components/ColorPicker/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 39 | 47 | -------------------------------------------------------------------------------- /src/components/Layout/components/appmain/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /src/components/Layout/components/navbar/components/breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 78 | 79 | 96 | -------------------------------------------------------------------------------- /src/components/Layout/components/navbar/components/hamburger.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 38 | 39 | 51 | -------------------------------------------------------------------------------- /src/components/Layout/components/navbar/components/userInfo.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/Layout/components/navbar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/Layout/components/sidebar/components/filterRouter.js: -------------------------------------------------------------------------------- 1 | export default (children = []) => { 2 | const filterChildren = children.filter((item) => { 3 | if (!item.hidden) { 4 | return true; 5 | } 6 | }); 7 | return filterChildren; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/Layout/components/sidebar/components/item.vue: -------------------------------------------------------------------------------- 1 | 5 | 18 | -------------------------------------------------------------------------------- /src/components/Layout/components/sidebar/components/logo.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/Layout/components/sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/components/Layout/components/sidebar/sidebarItem.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/components/Layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | -------------------------------------------------------------------------------- /src/core/addRoutePermission.js: -------------------------------------------------------------------------------- 1 | import { asyncRoutes } from "@/router/index"; 2 | export default function filterAsyncRoutes(roles, routes = asyncRoutes) { 3 | const res = []; 4 | routes.forEach((route) => { 5 | const tmp = { ...route }; 6 | if (hasPermission(roles, tmp)) { 7 | if (tmp.children) { 8 | tmp.children = filterAsyncRoutes(roles, tmp.children); 9 | } 10 | res.push(tmp); 11 | } 12 | }); 13 | return res; 14 | } 15 | 16 | function hasPermission(roles, route) { 17 | if (route.meta && route.meta.role) { 18 | return roles.some((role) => role === route.meta.role); 19 | } else { 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/core/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * * * * * * * * * * * * * * * 3 | * author: 马丰彦 4 | * date: 2021-06-22 5 | * function: 封装js-cookie 6 | * * * * * * * * * * * * * * * 7 | */ 8 | import Cookies from "js-cookie"; 9 | 10 | const Token = "epgms-token"; 11 | const UserName = "epgms-username"; 12 | 13 | export function getToken() { 14 | return Cookies.get(Token); 15 | } 16 | 17 | export function setToken(token) { 18 | Cookies.set(Token, token); 19 | } 20 | 21 | export function removeToken() { 22 | Cookies.remove(Token); 23 | } 24 | 25 | export function getUserName() { 26 | return Cookies.get(UserName); 27 | } 28 | 29 | export function setUserName(username) { 30 | Cookies.set(UserName, username); 31 | } 32 | -------------------------------------------------------------------------------- /src/core/get-page-title.js: -------------------------------------------------------------------------------- 1 | const title = import.meta.env.VITE_SYSTEM_NAME; 2 | 3 | export default (pageTitle) => { 4 | return pageTitle ? `${pageTitle} - ${title}` : `${title}`; 5 | }; 6 | -------------------------------------------------------------------------------- /src/core/permission.js: -------------------------------------------------------------------------------- 1 | /** 2 | * * * * * * * * * * * * * * * 3 | * author: 马丰彦 4 | * date: 2021-06-22 5 | * function: 全局权限 6 | * * * * * * * * * * * * * * * 7 | */ 8 | 9 | import { getToken } from "@/core/auth"; 10 | import { router } from "@/router/index"; 11 | import store from "@/store"; 12 | import NProgress from "nprogress"; 13 | import "nprogress/nprogress.css"; // css初始化 14 | import getPageTitle from "./get-page-title"; 15 | const whiteList = ["/login"]; // 不重定向白名单 16 | const powerType = import.meta.env.VITE_POWER_TYPE; // 权限类型 17 | 18 | NProgress.configure({ showSpinner: false }); // 进度条 19 | 20 | router.beforeEach(async (to, from, next) => { 21 | // 进度条开始 22 | NProgress.start(); 23 | 24 | document.title = getPageTitle(to.meta.title); 25 | const token = getToken(); 26 | 27 | // 有token,已经登录 28 | if (token) { 29 | // 动态路由权限 30 | if (powerType === "2") { 31 | const { routes } = store.getters; 32 | // 权限正常 33 | if (Array.isArray(routes) && routes.length > 0) { 34 | next(); 35 | } else { 36 | // 已经登录但是强制刷新页面 37 | const result = await store.dispatch("GetUserInfo", token); 38 | if (Array.isArray(result) && result.length > 0) { 39 | // 与路由表过滤权限 40 | const routes = await store.dispatch("GenerateRoutes", result); 41 | routes.forEach((item) => router.addRoute(item)); 42 | 43 | next({ ...to, replace: true }); 44 | } else { 45 | alert("无权限"); 46 | return false; 47 | } 48 | } 49 | } else { 50 | // 静态路由权限 51 | next(); 52 | } 53 | } else { 54 | if (whiteList.indexOf(to.path) !== -1) { 55 | next(); 56 | } else { 57 | next(`/login?redirect=${to.path}`); // 否则全部重定向到登录页 58 | } 59 | } 60 | NProgress.done(); 61 | }); 62 | -------------------------------------------------------------------------------- /src/core/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * * * * * * * * * * * * * * * 3 | * author: 马丰彦 4 | * date: 2021-06-18 5 | * function: 封装axios类 6 | * * * * * * * * * * * * * * * 7 | */ 8 | import axios from "axios"; 9 | 10 | class Axios { 11 | constructor(url, requestTime) { 12 | this.url = url; 13 | this.axiosInstance = null; 14 | this.createService(url, requestTime); 15 | } 16 | 17 | createService(url, requestTime) { 18 | const timeout = requestTime || 10000; 19 | this.axiosInstance = axios.create({ 20 | baseURL: url, 21 | withCredentials: true, 22 | timeout: timeout, 23 | }); 24 | this.interceptorsRequestToken(); 25 | this.interceptorsResponseToken(); 26 | } 27 | 28 | interceptorsRequestToken() { 29 | this.axiosInstance.interceptors.request.use( 30 | (config) => { 31 | config.url = this.handleJsonSuffix(config.url); 32 | return config; 33 | }, 34 | (error) => { 35 | return Promise.reject(error); 36 | }, 37 | ); 38 | } 39 | 40 | interceptorsResponseToken() { 41 | this.axiosInstance.interceptors.response.use( 42 | (response) => { 43 | // 相应拦截预留处理 自定义错误码 44 | return response.data; 45 | }, 46 | (error) => { 47 | return Promise.reject(error); 48 | }, 49 | ); 50 | } 51 | 52 | getUrl() { 53 | return this.url; 54 | } 55 | 56 | getInstance() { 57 | return this.axiosInstance; 58 | } 59 | 60 | // 处理.json后缀 61 | handleJsonSuffix(url) { 62 | // 不排除真的有请求.json的情况,偷懒嫌疑 63 | if (!import.meta.env.DEV && url.indexOf(".json") !== -1) { 64 | return url.replace(".json", ""); 65 | } else { 66 | return url; 67 | } 68 | } 69 | } 70 | 71 | export default Axios; 72 | -------------------------------------------------------------------------------- /src/directive/index.js: -------------------------------------------------------------------------------- 1 | export default (app) => { 2 | app.directive("focus", { 3 | mounted(el, binding) { 4 | el.focus(); 5 | }, 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/elementui.js: -------------------------------------------------------------------------------- 1 | import * as ElementPlusIconsVue from "@element-plus/icons-vue"; 2 | import { 3 | ElButton, 4 | ElForm, 5 | ElFormItem, 6 | ElInput, 7 | ElIcon, 8 | ElColorPicker, 9 | ElTag, 10 | ElScrollbar, 11 | ElMenu, 12 | ElMenuItem, 13 | ElSubMenu, 14 | ElBreadcrumb, 15 | ElBreadcrumbItem, 16 | ElDropdown, 17 | ElDropdownItem, 18 | ElDropdownMenu, 19 | } from "element-plus"; 20 | 21 | const useElementUi = (app) => { 22 | for (const [key, component] of Object.entries( 23 | ElementPlusIconsVue, 24 | )) { 25 | app.component(key, component); 26 | } 27 | app.component(ElButton.name, ElButton); 28 | app.component(ElForm.name, ElForm); 29 | app.component(ElFormItem.name, ElFormItem); 30 | app.component(ElInput.name, ElInput); 31 | app.component(ElIcon.name, ElIcon); 32 | app.component(ElColorPicker.name, ElColorPicker); 33 | app.component(ElTag.name, ElTag); 34 | app.component(ElScrollbar.name, ElScrollbar); 35 | app.component(ElMenu.name, ElMenu); 36 | app.component(ElMenuItem.name, ElMenuItem); 37 | app.component(ElSubMenu.name, ElSubMenu); 38 | app.component(ElBreadcrumb.name, ElBreadcrumb); 39 | app.component(ElBreadcrumbItem.name, ElBreadcrumbItem); 40 | app.component(ElDropdown.name, ElDropdown); 41 | app.component(ElDropdownItem.name, ElDropdownItem); 42 | app.component(ElDropdownMenu.name, ElDropdownMenu); 43 | }; 44 | 45 | export default useElementUi; 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | import { router } from "./router/index"; // router 5 | 6 | import store from "./store"; // store 7 | 8 | import directive from "./directive/index"; // directive 9 | 10 | import useElementUi from "./elementui"; // 按需引入element-ui 11 | 12 | import "./core/permission"; // 动态校验路由 13 | 14 | import "./stylus/index.styl"; // css文件 15 | 16 | import svgIcon from "./plugin/icons/svgIcon.vue"; // svg文件 17 | 18 | const app = createApp(App); 19 | 20 | directive(app); // 放在mount上面,有顺序的 21 | 22 | useElementUi(app); // 引用element-ui 23 | 24 | app.component("svg-icon", svgIcon); 25 | 26 | app.use(store); 27 | 28 | app.use(router); 29 | 30 | app.mount("#app"); 31 | -------------------------------------------------------------------------------- /src/mixin/global.js: -------------------------------------------------------------------------------- 1 | const globalProperty = { 2 | computed: { 3 | bgColor() { 4 | return this.$store.getters.bgColor; 5 | }, 6 | }, 7 | watch: { 8 | bgColor(newValue) { 9 | this.backgroundColor = newValue; 10 | }, 11 | }, 12 | }; 13 | 14 | export default globalProperty; 15 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/logo1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/nest.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/portal-create.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/portal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/template-audit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/template-create.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/template.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/user-paint.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugin/icons/svgBuilder.js: -------------------------------------------------------------------------------- 1 | import { readFileSync, readdirSync } from "fs"; 2 | 3 | let idPerfix = ""; 4 | const svgTitle = /+].*?)>/; 5 | const clearHeightWidth = /(width|height)="([^>+].*?)"/g; 6 | 7 | const hasViewBox = /(viewBox="[^>+].*?")/g; 8 | 9 | const clearReturn = /(\r)|(\n)/g; 10 | 11 | function findSvgFile(dir) { 12 | const svgRes = []; 13 | const dirents = readdirSync(dir, { 14 | withFileTypes: true, 15 | }); 16 | for (const dirent of dirents) { 17 | if (dirent.isDirectory()) { 18 | svgRes.push(...findSvgFile(dir + dirent.name + "/")); 19 | } else { 20 | const svg = readFileSync(dir + dirent.name) 21 | .toString() 22 | .replace(clearReturn, "") 23 | .replace(svgTitle, ($1, $2) => { 24 | let width = 0; 25 | let height = 0; 26 | let content = $2.replace(clearHeightWidth, (s1, s2, s3) => { 27 | if (s2 === "width") { 28 | width = s3; 29 | } else if (s2 === "height") { 30 | height = s3; 31 | } 32 | return ""; 33 | }); 34 | if (!hasViewBox.test($2)) { 35 | content += `viewBox="0 0 ${width} ${height}"`; 36 | } 37 | return ``; 38 | }) 39 | .replace("", ""); 40 | svgRes.push(svg); 41 | } 42 | } 43 | return svgRes; 44 | } 45 | 46 | export const svgBuilder = (path, perfix = "icon") => { 47 | if (path === "") return; 48 | idPerfix = perfix; 49 | const res = findSvgFile(path); 50 | return { 51 | name: "svg-transform", 52 | transformIndexHtml(html) { 53 | return html.replace( 54 | "", 55 | ` 56 | 57 | 58 | ${res.join("")} 59 | 60 | ` 61 | ); 62 | }, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /src/plugin/icons/svgIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | 36 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import Layout from "@/components/Layout/index.vue"; 3 | 4 | const powerType = import.meta.env.VITE_POWER_TYPE; // 权限类型 5 | /** 6 | * hidden: true 如果为true,侧边栏显示,否则是为通用隐藏路由 7 | * alwaysShow: true 如果为true,将总显示 8 | * 如果设置了,路由数量必须大于1 9 | * redirect: noRedirect 如果设置noRedirect,面包屑不重定向标题 10 | * meta : { 11 | role: 'admin' 路由权限唯一标识 12 | title: 'title' 路由标题,面包屑展示标题 13 | icon: 'svg-name' 路由图标 14 | breadcrumb: false 如果为false,面包屑标题隐藏 15 | } 16 | */ 17 | 18 | // 静态路由 19 | 20 | const constantRouters = [ 21 | { 22 | path: "/", 23 | name: "index", 24 | redirect: "/dashboard/index", 25 | }, 26 | { 27 | path: "/dashboard", 28 | name: "Dashboard", 29 | component: Layout, 30 | redirect: "/dashboard/index", 31 | children: [ 32 | { 33 | name: "Index", 34 | path: "index", 35 | component: () => import("@/views/dashboard/index.vue"), 36 | }, 37 | ], 38 | }, 39 | { 40 | path: "/login", 41 | name: "login", 42 | component: () => import("@/views/login/index.vue"), 43 | }, 44 | ]; 45 | 46 | // 动态路由 47 | export const asyncRoutes = [ 48 | { 49 | path: "/nested", 50 | // hidden: true, 51 | component: Layout, 52 | name: "Nested", 53 | meta: { 54 | title: "Nested", 55 | icon: "nest", 56 | }, 57 | children: [ 58 | { 59 | path: "menu1", 60 | component: () => import("@/views/template/create/index.vue"), // Parent router-view 61 | name: "Menu1", 62 | meta: { 63 | title: "Menu1", 64 | }, 65 | children: [ 66 | { 67 | path: "menu1-1", 68 | hidden: true, 69 | component: () => 70 | import("@/views/template/create/index.vue"), 71 | name: "Menu1-1", 72 | meta: { 73 | title: "Menu1-1", 74 | }, 75 | }, 76 | { 77 | path: "menu1-2", 78 | component: () => 79 | import("@/views/template/create/index.vue"), 80 | name: "Menu1-2", 81 | meta: { 82 | title: "Menu1-2", 83 | }, 84 | children: [ 85 | { 86 | path: "menu1-2-1", 87 | component: () => 88 | import("@/views/template/create/index.vue"), 89 | name: "Menu1-2-1", 90 | meta: { 91 | title: "Menu1-2-1", 92 | }, 93 | }, 94 | { 95 | path: "menu1-2-2", 96 | component: () => 97 | import("@/views/template/create/index.vue"), 98 | name: "Menu1-2-2", 99 | meta: { 100 | title: "Menu1-2-2", 101 | }, 102 | }, 103 | ], 104 | }, 105 | { 106 | path: "menu1-3", 107 | component: () => 108 | import("@/views/template/create/index.vue"), 109 | name: "Menu1-3", 110 | meta: { 111 | title: "Menu1-3", 112 | }, 113 | }, 114 | ], 115 | }, 116 | { 117 | path: "menu2", 118 | component: () => import("@/views/template/create/index.vue"), 119 | meta: { 120 | title: "menu2", 121 | }, 122 | }, 123 | ], 124 | }, 125 | { 126 | path: "/template", 127 | component: Layout, 128 | name: "Template", 129 | redirect: "/template/create", 130 | meta: { 131 | title: "模板管理", 132 | role: "template", 133 | icon: "template", 134 | }, 135 | children: [ 136 | { 137 | path: "create", 138 | name: "template-create", 139 | component: () => import("@/views/template/create/index.vue"), 140 | meta: { 141 | title: "模板创建", 142 | icon: "template-create", 143 | role: "template-create", 144 | }, 145 | }, 146 | { 147 | path: "audit", 148 | name: "template-audit", 149 | component: () => import("@/views/template/audit/index.vue"), 150 | meta: { 151 | title: "模板审核", 152 | icon: "template-audit", 153 | role: "template-audit", 154 | }, 155 | }, 156 | ], 157 | }, 158 | { 159 | path: "/portal", 160 | component: Layout, 161 | name: "Portal", 162 | redirect: "/portal/create", 163 | meta: { 164 | role: "portal", 165 | icon: "portal", 166 | }, 167 | children: [ 168 | { 169 | path: "create", 170 | name: "portal-create", 171 | component: () => import("@/views/portal/create/index.vue"), 172 | meta: { 173 | title: "门户创建", 174 | icon: "portal-create", 175 | role: "portal-create", 176 | }, 177 | }, 178 | ], 179 | }, 180 | { 181 | path: "/extra", 182 | name: "Extra", 183 | component: () => import("@/views/404/index.vue"), 184 | children: [ 185 | { 186 | path: "https://github.com/Maxfengyan/vue3-admin-template-vite", 187 | meta: { 188 | title: "外链", 189 | icon: "link", 190 | }, 191 | }, 192 | ], 193 | }, 194 | // 重定向放最后 195 | { 196 | path: "/:pathMatch(.*)*", 197 | hidden: true, 198 | name: "404", 199 | component: () => import("@/views/404/index.vue"), 200 | }, 201 | ]; 202 | 203 | const allRouter = 204 | powerType == "1" 205 | ? constantRouters.concat(asyncRoutes) 206 | : constantRouters; 207 | 208 | export const router = createRouter({ 209 | history: createWebHashHistory(), 210 | routes: allRouter, 211 | }); 212 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | bgColor: (state) => state.skin.bgColor, 3 | token: (state) => state.user.token, 4 | userName: (state) => state.user.userName, 5 | account: (state) => state.user.account, 6 | avatar: (state) => state.user.avatar, 7 | routes: (state) => state.user.routes, 8 | sidebar: (state) => state.user.sidebar, 9 | }; 10 | 11 | export default getters; 12 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import getters from "./getters"; 3 | import skin from "./modules/skin"; 4 | import user from "./modules/user"; 5 | 6 | export default createStore({ 7 | modules: { 8 | skin, 9 | user 10 | }, 11 | getters, 12 | }); 13 | -------------------------------------------------------------------------------- /src/store/modules/skin.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | bgColor: "#2d3a4b", 3 | contentColor: "#fff", 4 | }; 5 | 6 | const mutations = { 7 | SET_BGCOLOR: (state, bgcolor) => { 8 | state.bgColor = bgcolor; 9 | }, 10 | }; 11 | 12 | const actions = { 13 | Changebgcolor(context, bgColor) { 14 | context.commit("SET_BGCOLOR", bgColor); 15 | }, 16 | }; 17 | 18 | export default { 19 | namespaced: true, 20 | state, 21 | mutations, 22 | actions, 23 | }; 24 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login } from "@/api/epgms/account/login"; 2 | import { getUserInfo } from "@/api/epgms/account/getUserInfo"; 3 | import { logout } from "@/api/epgms/account/logout"; 4 | import filterAsyncRoutes from "@/core/addRoutePermission"; 5 | import { 6 | setToken, 7 | getToken, 8 | setUserName, 9 | getUserName, 10 | removeToken, 11 | } from "@/core/auth"; 12 | 13 | const state = { 14 | token: getToken(), 15 | userName: getUserName(), 16 | account: "", 17 | avatar: "", 18 | sidebar: true, 19 | routes: [], 20 | }; 21 | 22 | const mutations = { 23 | SET_TOKEN: (state, token) => { 24 | state.token = token; 25 | }, 26 | SET_NAME: (state, userName) => { 27 | state.userName = userName; 28 | }, 29 | SET_ACCOUNT: (state, account) => { 30 | state.account = account; 31 | }, 32 | SET_AVATAR: (state, avatar) => { 33 | state.avatar = avatar; 34 | }, 35 | SET_SIDEBAR: (state) => { 36 | state.sidebar = !state.sidebar; 37 | }, 38 | SET_ROUTES: (state, routes) => { 39 | state.routes = routes; 40 | }, 41 | }; 42 | 43 | const actions = { 44 | // 登录 45 | async LoginAction(context, userInfo) { 46 | const { account, password } = userInfo; 47 | var result = await login({ 48 | account: account.trim(), 49 | password: password.trim(), 50 | }); 51 | const { data } = result; 52 | const { token, userName, avator, userAccount } = data; 53 | context.commit("SET_TOKEN", token); 54 | context.commit("SET_NAME", userName); 55 | context.commit("SET_AVATAR", avator); 56 | context.commit("SET_ACCOUNT", userAccount); 57 | setToken(token); 58 | setUserName(userName); 59 | }, 60 | 61 | // 获取用户信息 62 | async GetUserInfo(context, token) { 63 | const result = await getUserInfo(token); 64 | const { data } = result; 65 | const { roles } = data; 66 | return roles; 67 | }, 68 | 69 | // 退出登录 70 | async Logout(context) { 71 | logout().then(() => { 72 | context.commit("SET_TOKEN", ""); 73 | context.commit("SET_NAME", ""); 74 | context.commit("SET_AVATAR", ""); 75 | context.commit("SET_ACCOUNT", ""); 76 | removeToken(); 77 | }); 78 | }, 79 | 80 | // sidebar status 81 | ChangeSidebar(context) { 82 | context.commit("SET_SIDEBAR"); 83 | }, 84 | 85 | async GenerateRoutes(context, roles) { 86 | const accessedRoutes = await filterAsyncRoutes(roles); 87 | context.commit("SET_ROUTES", accessedRoutes); 88 | return accessedRoutes; 89 | }, 90 | }; 91 | 92 | export default { 93 | namespace: true, 94 | state, 95 | mutations, 96 | actions, 97 | }; 98 | -------------------------------------------------------------------------------- /src/stylus/chrome.styl: -------------------------------------------------------------------------------- 1 | input:-webkit-autofill { 2 | // 字体颜色 3 | -webkit-text-fill-color: #ededed !important; 4 | // 背景颜色 5 | background-color:transparent; 6 | // 背景图片 7 | background-image: none; 8 | // 过渡 9 | transition: background-color 50000s ease-in-out 0s; 10 | } -------------------------------------------------------------------------------- /src/stylus/elementUI.styl: -------------------------------------------------------------------------------- 1 | .color-picker .el-color-picker__trigger { 2 | border: none !important; 3 | } 4 | 5 | .el-breadcrumb__inner, 6 | .el-breadcrumb__inner a { 7 | font-weight: 400 !important; 8 | } 9 | 10 | .el-sub-menu__title { 11 | padding-right: 0; 12 | } -------------------------------------------------------------------------------- /src/stylus/index.styl: -------------------------------------------------------------------------------- 1 | @import "normalize.css"; 2 | @import "variables.styl"; 3 | @import "chrome.styl"; 4 | @import "elementUI.styl"; 5 | @import "sidebar.styl"; 6 | @import "transition.styl"; 7 | @import "mixin.styl"; 8 | body { 9 | height: 100%; 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-font-smoothing: antialiased; 12 | text-rendering: optimizeLegibility; 13 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 14 | } 15 | 16 | label { 17 | font-weight: 700; 18 | } 19 | 20 | html { 21 | height: 100%; 22 | box-sizing: border-box; 23 | } 24 | 25 | *, 26 | *:before, 27 | *:after { 28 | box-sizing: inherit; 29 | } 30 | 31 | a, 32 | a:focus, 33 | a:hover { 34 | cursor: pointer; 35 | color: inherit; 36 | outline: none; 37 | text-decoration: none; 38 | } 39 | 40 | div:focus{ 41 | outline: none; 42 | } 43 | 44 | a:focus, 45 | a:active { 46 | outline: none; 47 | } 48 | 49 | a, 50 | a:focus, 51 | a:hover { 52 | cursor: pointer; 53 | color: inherit; 54 | text-decoration: none; 55 | } 56 | 57 | //main-container全局样式 58 | .app-main{ 59 | min-height: 100%; 60 | } 61 | 62 | .app-container { 63 | padding: 20px; 64 | } 65 | -------------------------------------------------------------------------------- /src/stylus/mixin.styl: -------------------------------------------------------------------------------- 1 | ellipsis() 2 | overflow: hidden 3 | white-space: nowrap 4 | text-overflow: ellipsis -------------------------------------------------------------------------------- /src/stylus/sidebar.styl: -------------------------------------------------------------------------------- 1 | #app { 2 | .main-container { 3 | min-height: 100%; 4 | transition: margin-left .28s; 5 | margin-left: $sideBarWidth; 6 | position: relative; 7 | } 8 | .sidebar-container { 9 | transition: width 0.28s; 10 | height: 100%; 11 | position: fixed; 12 | top: 0; 13 | left: 0; 14 | bottom: 0; 15 | overflow: hidden; 16 | z-index: 1001; 17 | width: 180px; 18 | background-color: $menuBg; 19 | 20 | // reset element-ui css 21 | .horizontal-collapse-transition { 22 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 23 | } 24 | 25 | .scrollbar-wrapper { 26 | overflow-x: hidden !important; 27 | } 28 | 29 | .el-scrollbar__bar.is-vertical { 30 | right: 0px; 31 | } 32 | 33 | .el-scrollbar { 34 | height: 100%; 35 | } 36 | 37 | &.has-logo { 38 | .el-scrollbar { 39 | height: calc(100% - 50px); 40 | } 41 | } 42 | 43 | .is-horizontal { 44 | display: none; 45 | } 46 | 47 | a { 48 | // display: inline-block; 49 | width: 100%; 50 | overflow: hidden; 51 | } 52 | 53 | .svg-icon { 54 | margin-right: 16px; 55 | } 56 | 57 | .el-menu { 58 | border: none; 59 | height: 100%; 60 | width: 100% !important; 61 | } 62 | 63 | // menu hover 64 | .submenu-title-noDropdown, 65 | .el-submenu__title { 66 | &:hover { 67 | background-color: $menuHover !important; 68 | } 69 | } 70 | 71 | .is-active>.el-submenu__title { 72 | color: $subMenuActiveText !important; 73 | } 74 | 75 | & .nest-menu .el-submenu>.el-submenu__title, 76 | & .el-submenu .el-menu-item { 77 | min-width: $sideBarWidth !important; 78 | background-color: $subMenuBg !important; 79 | &:hover { 80 | background-color: $subMenuHover !important; 81 | } 82 | } 83 | } 84 | 85 | .hideSidebar { 86 | .sidebar-container { 87 | width: 54px !important; 88 | } 89 | 90 | .main-container { 91 | margin-left: 54px; 92 | } 93 | 94 | .svg-icon { 95 | margin-right: 0px; 96 | } 97 | 98 | .submenu-title-noDropdown { 99 | padding: 0 !important; 100 | position: relative; 101 | 102 | .svg-icon { 103 | margin-left: 20px; 104 | } 105 | } 106 | 107 | .el-submenu { 108 | overflow: hidden; 109 | 110 | &>.el-submenu__title { 111 | padding: 0 !important; 112 | 113 | .svg-icon { 114 | margin-left: 20px; 115 | } 116 | 117 | .el-submenu__icon-arrow { 118 | display: none; 119 | } 120 | } 121 | } 122 | 123 | .el-menu--collapse { 124 | .el-submenu { 125 | &>.el-submenu__title { 126 | &>span { 127 | height: 0; 128 | width: 0; 129 | overflow: hidden; 130 | visibility: hidden; 131 | display: inline-block; 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | .el-menu--collapse .el-menu .el-submenu { 139 | min-width: $sideBarWidth !important; 140 | } 141 | 142 | // mobile responsive 143 | /* .mobile { 144 | .main-container { 145 | margin-left: 0px; 146 | } 147 | 148 | .sidebar-container { 149 | transition: transform .28s; 150 | width: $sideBarWidth !important; 151 | } 152 | 153 | &.hideSidebar { 154 | .sidebar-container { 155 | pointer-events: none; 156 | transition-duration: 0.3s; 157 | transform: translate3d(-$sideBarWidth, 0, 0); 158 | } 159 | } 160 | } 161 | 162 | .withoutAnimation { 163 | 164 | .main-container, 165 | .sidebar-container { 166 | transition: none; 167 | } 168 | } */ 169 | 170 | } 171 | 172 | 173 | 174 | // when menu collapsed 175 | .el-menu--vertical { 176 | &>.el-menu { 177 | .svg-icon { 178 | margin-right: 16px; 179 | } 180 | } 181 | 182 | .nest-menu .el-submenu>.el-submenu__title, 183 | .el-menu-item { 184 | &:hover { 185 | // you can use $subMenuHover 186 | background-color: $menuHover !important; 187 | } 188 | } 189 | 190 | // the scroll bar appears when the subMenu is too long 191 | >.el-menu--popup { 192 | max-height: 100vh; 193 | overflow-y: auto; 194 | 195 | &::-webkit-scrollbar-track-piece { 196 | background: #d3dce6; 197 | } 198 | 199 | &::-webkit-scrollbar { 200 | width: 6px; 201 | } 202 | 203 | &::-webkit-scrollbar-thumb { 204 | background: #99a9bf; 205 | border-radius: 20px; 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/stylus/transition.styl: -------------------------------------------------------------------------------- 1 | 2 | /* fade */ 3 | .fade-enter-active, 4 | .fade-leave-active { 5 | transition: opacity 0.28s; 6 | } 7 | 8 | .fade-enter-from, 9 | .fade-leave-active { 10 | opacity: 0; 11 | } 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-from { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | 31 | /* breadcrumb transition */ 32 | .breadcrumb-enter-active, 33 | .breadcrumb-leave-active { 34 | transition: all .5s; 35 | } 36 | 37 | .breadcrumb-enter-from, 38 | .breadcrumb-leave-active { 39 | opacity: 0; 40 | transform: translateX(20px); 41 | } 42 | 43 | .breadcrumb-move-from { 44 | transition: all .5s; 45 | } 46 | 47 | .breadcrumb-leave-active { 48 | position: absolute; 49 | } 50 | -------------------------------------------------------------------------------- /src/stylus/variables.styl: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText = #bfcbd9; 3 | $menuActiveText = #409EFF; 4 | $subMenuActiveText = #f4f4f5; 5 | 6 | $menuBg = #304156; 7 | $menuHover = #263445; 8 | 9 | $subMenuBg = #1f2d3d; 10 | $subMenuHover = #001528; 11 | 12 | $sideBarWidth = 180px; 13 | 14 | $backgroundColor = #2d3a4b; 15 | /* export { 16 | menuText: $menuText; 17 | menuActiveText: $menuActiveText; 18 | subMenuActiveText: $subMenuActiveText; 19 | menuBg: $menuBg; 20 | menuHover: $menuHover; 21 | subMenuBg: $subMenuBg; 22 | subMenuHover: $subMenuHover; 23 | sideBarWidth: $sideBarWidth; 24 | } */ -------------------------------------------------------------------------------- /src/views/404/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 33 | 34 | 254 | 260 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 126 | 127 | 206 | -------------------------------------------------------------------------------- /src/views/portal/create/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | 27 | -------------------------------------------------------------------------------- /src/views/template/audit/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | -------------------------------------------------------------------------------- /src/views/template/create/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | const ip = require("ip"); 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | import AutoImport from "unplugin-auto-import/vite"; 6 | import Components from "unplugin-vue-components/vite"; 7 | import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; 8 | import { svgBuilder } from "./src/plugin/icons/svgBuilder"; 9 | import ElementPlus from "unplugin-element-plus/vite"; 10 | 11 | const path = require("path"); 12 | 13 | function _resolve(dir) { 14 | return path.resolve(__dirname, dir); 15 | } 16 | // https://vitejs.dev/config/ 17 | export default defineConfig({ 18 | resolve: { 19 | alias: { 20 | "@": _resolve("src"), 21 | }, 22 | }, 23 | plugins: [ 24 | vue(), 25 | ElementPlus({}), 26 | AutoImport({ 27 | resolvers: [ElementPlusResolver()], 28 | }), 29 | Components({ 30 | resolvers: [ElementPlusResolver()], 31 | }), 32 | svgBuilder("./src/plugin/icons/svg/"), 33 | ], 34 | server: { 35 | port: 9000, 36 | cors: true, 37 | host: ip.address(), 38 | }, 39 | }); 40 | --------------------------------------------------------------------------------