├── .cz-config.cjs ├── .gitignore ├── README.md ├── img.png ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── api │ └── api.js ├── assets │ ├── 401_images │ │ └── 401.gif │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── demo.wav │ ├── login-background.jpeg │ ├── logo.png │ ├── snowb3.jpg │ └── star.jpg ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── Guide │ │ ├── index.vue │ │ └── step.js │ ├── Hamburger │ │ └── index.vue │ ├── HeaderSearch │ │ ├── FuseData.js │ │ └── index.vue │ ├── Pagination │ │ └── index.vue │ ├── RightToolbar │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── TagsView │ │ ├── ContextMenu.vue │ │ └── index.vue │ └── UploadExcel │ │ └── index.vue ├── constant │ ├── formula.json │ └── index.js ├── directives │ ├── common │ │ └── copyText.js │ ├── index.js │ └── permission.js ├── filters │ └── index.js ├── icons │ ├── index.js │ └── svg │ │ ├── 404.svg │ │ ├── article-create.svg │ │ ├── article-ranking.svg │ │ ├── article.svg │ │ ├── bug.svg │ │ ├── change-theme.svg │ │ ├── chart.svg │ │ ├── clipboard.svg │ │ ├── component.svg │ │ ├── dashboard.svg │ │ ├── documentation.svg │ │ ├── drag.svg │ │ ├── edit.svg │ │ ├── education.svg │ │ ├── email.svg │ │ ├── example.svg │ │ ├── excel.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── fullscreen.svg │ │ ├── guide.svg │ │ ├── hamburger-closed.svg │ │ ├── hamburger-opened.svg │ │ ├── home.svg │ │ ├── icon.svg │ │ ├── international.svg │ │ ├── introduce.svg │ │ ├── language.svg │ │ ├── link.svg │ │ ├── list.svg │ │ ├── lock.svg │ │ ├── message.svg │ │ ├── money.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── pdf.svg │ │ ├── people.svg │ │ ├── peoples.svg │ │ ├── permission.svg │ │ ├── personnel-info.svg │ │ ├── personnel-manage.svg │ │ ├── personnel.svg │ │ ├── qq.svg │ │ ├── reward.svg │ │ ├── role.svg │ │ ├── search.svg │ │ ├── shopping.svg │ │ ├── size.svg │ │ ├── skill.svg │ │ ├── star.svg │ │ ├── tab.svg │ │ ├── table.svg │ │ ├── theme.svg │ │ ├── tree-table.svg │ │ ├── tree.svg │ │ ├── user.svg │ │ ├── wechat.svg │ │ └── zip.svg ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ └── Sidebar │ │ │ ├── MenuItem.vue │ │ │ ├── SidebarItem.vue │ │ │ ├── SidebarMenu.vue │ │ │ └── index.vue │ └── index.vue ├── main.js ├── mock │ └── index.js ├── permission.js ├── plugins │ ├── element.js │ ├── svgicon.js │ └── vite-plugin-git-info.js ├── router │ ├── index.js │ └── modules │ │ ├── cssAnimation.js │ │ ├── permissions.js │ │ ├── third.js │ │ └── vueUse.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── permission.js │ │ └── user.js ├── styles │ ├── element.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.module.scss ├── utils │ ├── auth.js │ ├── axios.js │ ├── index.js │ ├── route.js │ ├── scroll-to.js │ ├── storage.js │ ├── tags.js │ └── validate.js └── views │ ├── css-animation │ ├── bubbleFloat.vue │ ├── clock.vue │ ├── downBtn.vue │ ├── filpCard.vue │ ├── fullscreenMenu.vue │ ├── hoverBorderBtn.vue │ ├── hoverFillText.vue │ ├── hoverShiningBtn.vue │ ├── hoverSlideMenu.vue │ ├── jumpBlock.vue │ ├── shootingStar.vue │ ├── slidePic.vue │ ├── snowScratch.vue │ ├── tabs.vue │ ├── videoMaskText.vue │ └── waveloading.vue │ ├── error-page │ ├── 401.vue │ └── 404.vue │ ├── home │ └── index.vue │ ├── login │ ├── index.vue │ └── rules.js │ ├── permissions-page │ ├── accountDetail.vue │ ├── accountList.vue │ ├── components │ │ ├── distributePermission.vue │ │ └── roles.vue │ ├── permissionList.vue │ └── roleList.vue │ ├── third-page │ ├── components │ │ ├── Editor.vue │ │ └── Markdown.vue │ ├── editor │ │ └── index.vue │ └── markdown │ │ └── index.vue │ └── vue-use │ ├── component │ └── createReusableTemplate.vue │ └── elements │ ├── useDraggable.vue │ ├── useDropZone.vue │ └── useIntersectionObserver.vue ├── vite.config.js └── yarn.lock /.cz-config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 可选类型 3 | types: [ 4 | { value: 'feat', name: 'feat: 新增功能' }, 5 | { value: 'fix', name: 'fix: 修复功能' }, 6 | { value: 'docs', name: 'docs: 更新文档' }, 7 | { value: 'style', name: 'style: 代码格式变更' }, 8 | { value: 'refactor',name: 'refactor: 代码重构:非新增功能非修改功能' }, 9 | { value: 'perf', name: 'perf: 性能优化' }, 10 | { value: 'test', name: 'test: 增加测试用例' }, 11 | { value: 'chore', name: 'chore: 构建过程或辅助工具的变动' }, 12 | { value: 'revert', name: 'revert: 代码回退' }, 13 | ], 14 | // 消息步骤 15 | messages: { 16 | type: '请选择提交类型:', 17 | customScope: '请输入修改范围(可选):', 18 | subject: '请简要描述提交(必填):', 19 | body: '请输入详细描述(可选):', 20 | footer: '请输入要关闭的issue(可选):', 21 | confirmCommit: '确认使用以上信息提交?(y/n/e/h)' 22 | }, 23 | // 跳过问题 24 | skipQuestions: ['body', 'footer'], 25 | // subject文字长度默认是72 26 | subjectLimit: 72 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | .vercel 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 一个极简的后台基础模板,企业级!开箱即用! 3 | 4 | ## 1、[在线体验地址](http://3thousand.top/admin) 5 | ## 2、[《企业级项目框架从0搭建教程》](https://haohuo.jinritemai.com/ecommerce/trade/detail/index.html?id=3673977559881744710&origin_type=604) 6 | image 7 | 8 | ## 3、[《配套 企业级微信小程序教程 与java后端教程》](https://haohuo.jinritemai.com/ecommerce/trade/detail/index.html?id=3673977559881744710&origin_type=604) 9 | image 10 | 11 | 12 | ## 4、全网同名:程序员三千 (抖音、b站、视频号) 13 | 14 | ## 项目技术栈:Vue3 + JavaScript + Vite4 + Element-plus2.3.5 15 | ![image](https://github.com/wudengyao/admin_vue3_vite/assets/9073383/a206b4c9-c25d-4a5b-b6a1-bded7276a9c6) 16 | ![image](https://github.com/wudengyao/admin_vue3_vite/assets/9073383/01cc0469-3a92-4f1b-970e-01bcb51fc576) 17 | ![image](https://github.com/wudengyao/admin_vue3_vite/assets/9073383/b14733b1-af44-4e0c-a6fb-fc3bdd996c66) 18 | ![image](https://github.com/wudengyao/admin_vue3_vite/assets/9073383/39d23687-ab7b-4752-aeae-7246b8d3e7e1) 19 | ![image](https://github.com/wudengyao/admin_vue3_vite/assets/9073383/4606d851-a799-492c-9985-fccd155efbf4) 20 | ![image](https://github.com/wudengyao/admin_vue3_vite/assets/9073383/5ce8b75f-fb20-4291-92c9-d6def3b24906) 21 | ![image](https://github.com/wudengyao/admin_vue3_vite/assets/9073383/2e34dd6a-325b-491a-a3cf-46c34896562d) 22 | 23 | ## 简介 24 | 对于后台系统而言,相信只要是前端开发的工程师,那么就不陌生了。 25 | 根据网上有效数据统计和本人的工作经验,在 初、中级的前端开发者中,日常的工作主要内容,基本都是书写后台管理系统(搭建和扩展)。 26 | 后台管理系统为前端开发中最为重要的工作方向。 27 | 28 | ## 项目功能介绍 29 | 本次项目,我们将抽离出几十个业务组件模型,争取可以制作出覆盖大家大部分后台开发业务场景的综合性解决方案,以便大家可以在日后的工作中复用。 30 | 31 | ##### 主要包括: 32 | 33 | - 接口模块封装方案 34 | - 请求动作封装方案 35 | - token 处理方案 36 | - 登录鉴权方案 37 | - 动态路由表处理方案 38 | - 动态菜单项处理方案(支持三级及其以上的菜单) 39 | - 动态面包屑处理方案 40 | - 历史打开页面TagsView 处理方案 41 | - RBAC 的权限分控体系 42 | - 页面权限处理方案 43 | - 功能权限处理方案 44 | - 辅助库选择标准 45 | - 富文本编辑器处理方案 46 | - keepAlive页面缓存处理方案 47 | - 多种企业级别组件的封装 48 | - 多种有趣的CSS动画效果 49 | 50 | ## 最新内容持续更新中... 51 | 52 | 53 | - 依赖安装:yarn 54 | - 项目运行:npm run dev 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/img.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue3后台系统 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "types": ["vite/client"], 15 | "baseUrl": "./", 16 | "paths":{ 17 | "@": ["src"], 18 | "@/*": ["src/*"] 19 | }, 20 | "exclude": ["node_modules", "dist"] 21 | }, 22 | "include": [ 23 | "*.js", 24 | "src/**/*.js", 25 | "src/**/*.d.js", 26 | "src/**/*.tsx", 27 | "src/**/*.vue", 28 | "public/static/config.js" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin_vue3_vite", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "deploy": "gh-pages -d dist", 11 | "commit": "git-cz" 12 | 13 | }, 14 | "homepage": "http://wudengyao.github.io/admin_vue3_vite", 15 | "dependencies": { 16 | "@element-plus/icons": "^0.0.11", 17 | "@toast-ui/editor": "^3.0.2", 18 | "@vueuse/core": "^10.3.0", 19 | "@wangeditor/editor": "^5.1.23", 20 | "axios": "^1.3.4", 21 | "core-js": "^3.6.5", 22 | "dayjs": "^1.10.6", 23 | "driver.js": "^0.9.8", 24 | "echarts": "^5.4.2", 25 | "element-plus": "^2.3.5", 26 | "fast-glob": "^3.2.12", 27 | "fuse.js": "^6.4.6", 28 | "gh-pages": "^6.0.0", 29 | "moment": "^2.29.4", 30 | "path-browserify": "^1.0.1", 31 | "sass-loader": "^13.2.2", 32 | "screenfull": "^5.1.0", 33 | "vue": "^3.2.8", 34 | "vue-cropper": "1.0.3", 35 | "vue-router": "^4.0.11", 36 | "vue3-print-nb": "^0.1.4", 37 | "vuex": "^4.0.2", 38 | "wangeditor": "^4.7.6", 39 | "wavesurfer.js": "^6.6.3" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^18.16.0", 43 | "@vitejs/plugin-vue": "^4.0.0", 44 | "commitizen": "^4.3.0", 45 | "cz-conventional-changelog": "^3.3.0", 46 | "cz-customizable": "^7.0.0", 47 | "mockjs": "^1.1.0", 48 | "sass": "^1.62.0", 49 | "vite": "^4.1.4", 50 | "vite-plugin-mock": "^2.9.8", 51 | "vite-plugin-svg-icons": "^2.0.1" 52 | }, 53 | "config": { 54 | "commitizen": { 55 | "path": "node_modules/cz-customizable" 56 | }, 57 | "cz-customizable": { 58 | "config": ".cz-config.cjs" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/axios"; 2 | import moment from "moment"; 3 | 4 | /** 5 | * 登录 6 | */ 7 | export function login(params) { 8 | return axios({ 9 | url: "/Index/login", 10 | method: "post", 11 | data: params 12 | }); 13 | } 14 | /** 15 | * 获取图形验证码 16 | */ 17 | export function getCode(params) { 18 | return axios({ 19 | url: "/Index/getCaptchaCode", 20 | method: "post", 21 | data: params 22 | }); 23 | } 24 | 25 | /** 26 | * 权限列表(侧边栏权限和按钮权限) 27 | * @param params 28 | */ 29 | export function getPermission(params) { 30 | return axios({ 31 | url: "/Index/getPermission", 32 | method: "post", 33 | data: params 34 | }); 35 | } 36 | 37 | /** 38 | * 账号列表 39 | * @param params 40 | */ 41 | export function getAdmintorList(params) { 42 | return axios({ 43 | url: "/adminAuth/adminList", 44 | method: "post", 45 | data: params 46 | }); 47 | } 48 | 49 | /** 50 | * 角色列表 51 | * @param params 52 | */ 53 | export function getRoleList(params) { 54 | return axios({ 55 | url: "/adminAuth/getRoleList", 56 | method: "post", 57 | data: params 58 | }); 59 | } 60 | 61 | // 上传图片 62 | export function publicUploadFile(params) { 63 | return axios({ 64 | url: "/public/uploadFile", 65 | method: "post", 66 | data: params 67 | }); 68 | } -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/demo.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/src/assets/demo.wav -------------------------------------------------------------------------------- /src/assets/login-background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/src/assets/login-background.jpeg -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/snowb3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/src/assets/snowb3.jpg -------------------------------------------------------------------------------- /src/assets/star.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wudengyao/admin_vue3_vite/5be195e4ba25dda12d95a80344055c09350b7e31/src/assets/star.jpg -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/Guide/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Guide/step.js: -------------------------------------------------------------------------------- 1 | const steps = [ 2 | { 3 | element: "#guide-start", 4 | popover: { 5 | title: "引导", 6 | description: "打开引导功能", 7 | position: "bottom-right" 8 | } 9 | }, 10 | { 11 | element: "#guide-hamburger", 12 | popover: { 13 | title: "菜单收缩按钮", 14 | description: "指示当前页面位置" 15 | } 16 | }, 17 | { 18 | element: "#guide-breadcrumb", 19 | popover: { 20 | title: "面包屑", 21 | description: "指示当前页面位置" 22 | } 23 | }, 24 | 25 | { 26 | element: "#guide-full", 27 | popover: { 28 | title: "全屏", 29 | description: "页面显示切换", 30 | position: "bottom-right" 31 | } 32 | }, 33 | { 34 | element: "#guide-sidebar", 35 | popover: { 36 | title: "菜单", 37 | description: "项目功能菜单", 38 | position: "right-center" 39 | } 40 | } 41 | ]; 42 | 43 | export default steps; -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/FuseData.js: -------------------------------------------------------------------------------- 1 | import path from "path-browserify"; 2 | 3 | /** 4 | * 筛选出可供搜索的路由对象 5 | * @param routes 路由表 6 | * @param basePath 基础路径,默认为 / 7 | * @param prefixTitle 8 | */ 9 | export const generateRoutes = (routes, basePath = "/", prefixTitle = []) => { 10 | // 创建 result 数据 11 | let res = []; 12 | // 循环 routes 路由 13 | for (const route of routes) { 14 | // 创建包含 path 和 title 的 item 15 | const data = { 16 | path: path.resolve(basePath, route.path), 17 | title: [...prefixTitle] 18 | }; 19 | // 动态路由不允许被搜索 20 | // 匹配动态路由的正则 21 | // 不显示在左侧菜单栏的路由要过滤掉 22 | const re = /.*\/:.*/; 23 | if ( 24 | route.meta 25 | && route.meta.title 26 | && !route.hidden 27 | && !re.exec(route.path) 28 | && !res.find(item => item.path === data.path) 29 | ) { 30 | data.title = [...data.title, route.meta.title]; 31 | res.push(data); 32 | } 33 | 34 | // 存在 children 时,迭代调用 35 | if (route.children) { 36 | const tempRoutes = generateRoutes(route.children, data.path, data.title); 37 | if (tempRoutes.length >= 1) { 38 | res = [...res, ...tempRoutes]; 39 | } 40 | } 41 | } 42 | return res; 43 | }; -------------------------------------------------------------------------------- /src/components/HeaderSearch/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 65 | 66 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/RightToolbar/index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 80 | 81 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/TagsView/ContextMenu.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/UploadExcel/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/constant/formula.json: -------------------------------------------------------------------------------- 1 | { 2 | "shade-1": "color(primary shade(10%))", 3 | "light-1": "color(primary tint(10%))", 4 | "light-2": "color(primary tint(20%))", 5 | "light-3": "color(primary tint(30%))", 6 | "light-4": "color(primary tint(40%))", 7 | "light-5": "color(primary tint(50%))", 8 | "light-6": "color(primary tint(60%))", 9 | "light-7": "color(primary tint(70%))", 10 | "light-8": "color(primary tint(80%))", 11 | "light-9": "color(primary tint(90%))", 12 | "subMenuHover": "color(primary tint(70%))", 13 | "subMenuBg": "color(primary tint(80%))", 14 | "menuHover": "color(primary tint(90%))", 15 | "menuBg": "color(primary)" 16 | } 17 | -------------------------------------------------------------------------------- /src/constant/index.js: -------------------------------------------------------------------------------- 1 | // 0测试环境,1模测环境, 2预发布线上环境, 3线上开发, 4本地开发 2 | export const SERVER_TYPE = 4; 3 | // 0测试环境 4 | export const TEST_URL = "0测试环境接口域名"; 5 | // 1模测环境 6 | export const MO_URL = " 1模测环境接口域名"; 7 | // 2预发布环境 8 | export const YFB_URL = "2预发布环境接口域名"; 9 | // 3线上环境 10 | export const PRO_URL = "3线上环境接口域名"; 11 | // 4本地开发,会触发代理 12 | export const DEV_URL = "/api"; 13 | // 接口版本号 14 | export const VERSION = "10000"; 15 | // 接口测试版本号s 16 | export const MODEL_TEST_VERSION = "666666"; 17 | // token 18 | export const TOKEN = "token"; 19 | // userInfo 20 | export const USERINFO = "userInfo"; 21 | // token 时间戳 22 | export const TIME_STAMP = "timeStamp"; 23 | // 超时时长(毫秒) 两小时 24 | export const TOKEN_TIMEOUT_VALUE = 2 * 3600 * 1000; 25 | // 国际化 26 | export const LANG = "language"; 27 | // 主题色保存的 key 28 | export const MAIN_COLOR = "mainColor"; 29 | // 默认色值 30 | export const DEFAULT_COLOR = "#409eff"; 31 | // tags 32 | export const TAGS_VIEW = "tagsView"; 33 | // axios请求超时时间 34 | export const AXIOS_TIMEOUT = 50000; -------------------------------------------------------------------------------- /src/directives/common/copyText.js: -------------------------------------------------------------------------------- 1 | /** 2 | * v-copyText 复制文本内容 3 | */ 4 | 5 | export default { 6 | beforeMount(el, { value, arg }) { 7 | if (arg === "callback") { 8 | el.$copyCallback = value; 9 | } else { 10 | el.$copyValue = value; 11 | const handler = () => { 12 | copyTextToClipboard(el.$copyValue); 13 | if (el.$copyCallback) { 14 | el.$copyCallback(el.$copyValue); 15 | } 16 | }; 17 | el.addEventListener("click", handler); 18 | el.$destroyCopy = () => el.removeEventListener("click", handler); 19 | } 20 | } 21 | }; 22 | 23 | function copyTextToClipboard(input, { target = document.body } = {}) { 24 | const element = document.createElement("textarea"); 25 | const previouslyFocusedElement = document.activeElement; 26 | 27 | element.value = input; 28 | 29 | // Prevent keyboard from showing on mobile 30 | element.setAttribute("readonly", ""); 31 | 32 | element.style.contain = "strict"; 33 | element.style.position = "absolute"; 34 | element.style.left = "-9999px"; 35 | element.style.fontSize = "12pt"; // Prevent zooming on iOS 36 | 37 | const selection = document.getSelection(); 38 | const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0); 39 | 40 | target.append(element); 41 | element.select(); 42 | 43 | // Explicit selection workaround for iOS 44 | element.selectionStart = 0; 45 | element.selectionEnd = input.length; 46 | 47 | let isSuccess = false; 48 | try { 49 | isSuccess = document.execCommand("copy"); 50 | } catch { } 51 | 52 | element.remove(); 53 | 54 | if (originalRange) { 55 | selection.removeAllRanges(); 56 | selection.addRange(originalRange); 57 | } 58 | 59 | // Get the focus back on the previously focused element, if any 60 | if (previouslyFocusedElement) { 61 | previouslyFocusedElement.focus(); 62 | } 63 | 64 | return isSuccess; 65 | } -------------------------------------------------------------------------------- /src/directives/index.js: -------------------------------------------------------------------------------- 1 | import permission from "./permission"; 2 | import print from "vue3-print-nb"; 3 | import copyText from "./common/copyText"; 4 | 5 | export default app => { 6 | app.use(print); 7 | app.directive("auth", permission); 8 | app.directive("copyText", copyText); 9 | }; -------------------------------------------------------------------------------- /src/directives/permission.js: -------------------------------------------------------------------------------- 1 | import store from "@/store"; 2 | import { lowerCase } from "@/utils/index"; 3 | 4 | function checkPermission(el, binding) { 5 | // 获取绑定的值,此处为权限 6 | const value = lowerCase(binding.value); 7 | const auths = store.getters.buttons || []; 8 | if (!auths.includes(value)) { 9 | el.parentNode.removeChild(el); 10 | } 11 | } 12 | 13 | export default { 14 | // 在绑定元素的父组件被挂载后调用 15 | mounted(el, binding) { 16 | checkPermission(el, binding); 17 | }, 18 | // 在包含组件的 VNode 及其子组件的 VNode 更新后调用 19 | update(el, binding) { 20 | checkPermission(el, binding); 21 | } 22 | }; -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | 3 | // val:毫秒级时间戳 4 | const dateFilter = (val, format = "YYYY-MM-DD") => { 5 | if (!isNaN(val)) { 6 | val = parseInt(val); 7 | } 8 | 9 | return dayjs(val).format(format); 10 | }; 11 | 12 | export default app => { 13 | app.config.globalProperties.$filters = { 14 | dateFilter 15 | }; 16 | }; -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import SvgIcon from "@/components/SvgIcon"; 2 | 3 | // https://webpack.docschina.org/guides/dependency-management/#requirecontext 4 | // 通过 require.context() 函数来创建自己的 context 5 | const svgRequire = require.context("./svg", false, /\.svg$/); 6 | // 此时返回一个 require 的函数,可以接受一个 request 的参数,用于 require 的导入。 7 | // 该函数提供了三个属性,可以通过 require.keys() 获取到所有的 svg 图标 8 | // 遍历图标,把图标作为 request 传入到 require 导入函数中,完成本地 svg 图标的导入 9 | svgRequire.keys().forEach(svgIcon => svgRequire(svgIcon)); 10 | 11 | export default app => { 12 | app.component("svg-icon", SvgIcon); 13 | }; -------------------------------------------------------------------------------- /src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/article-create.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/article-ranking.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/article.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/change-theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/hamburger-closed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/hamburger-opened.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/introduce.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/permission.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/personnel-info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/personnel-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/personnel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/qq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/reward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/role.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarMenu.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | 36 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 45 | 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import store from "./store"; 4 | 5 | import router from "./router"; 6 | 7 | // 导入权限控制模块 8 | import "./permission"; 9 | import "@/styles/index.scss"; 10 | 11 | // import axios from '@/utils/axios' 12 | // app.config.globalProperties.$axios = axios // 使用globalProperties挂载 13 | 14 | // element 15 | import installElementPlus from "./plugins/element"; 16 | // directives 17 | import installDirective from "@/directives"; 18 | // filter 19 | import installFilter from "@/filters"; 20 | 21 | // 自定义表格工具组件 22 | import RightToolbar from "@/components/RightToolbar"; 23 | // 分页组件 24 | import Pagination from "@/components/Pagination"; 25 | // svg组件 26 | import svgIcon from "@/components/SvgIcon/index.vue"; 27 | const app = createApp(App); 28 | installElementPlus(app); 29 | installDirective(app); 30 | installFilter(app); 31 | // 全局组件挂载 32 | app.component("RightToolbar", RightToolbar); 33 | app.component("Pagination", Pagination); 34 | app.component("svg-icon", svgIcon); 35 | 36 | app 37 | .use(store) 38 | .use(router) 39 | .mount("#app"); -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from "./router"; 2 | import store from "./store"; 3 | 4 | // 白名单 5 | const whiteList = ["/login"]; 6 | 7 | // 递归,将icon替换成服务端的数据 8 | function filterRoutesIcon(list1, list2) { 9 | list1.forEach(item1 => { 10 | list2.forEach(item2 => { 11 | if (item1.path === item2.url) { 12 | item1.meta.icon = item2.icon; 13 | } 14 | }); 15 | if (item1.children) { 16 | filterRoutesIcon(item1.children, list2); 17 | } 18 | }); 19 | } 20 | /** 21 | * 路由前置守卫 22 | */ 23 | router.beforeEach(async(to, from, next) => { 24 | // 存在 token ,进入主页 25 | if (store.getters.token) { // 当前存在token,如果此时去登录界面,自动跳转到主页 26 | if (to.path === "/login") { 27 | next("/"); 28 | } else { // 如果此时,去除了登录页面的其他页面,就去要去的目标页面 29 | if (!store.getters.hasRoles) { 30 | const { roles } = await store.dispatch("user/getPermissionData"); 31 | // 处理用户权限,筛选出需要添加的权限 32 | const accessRoutes = await store.dispatch("permission/generateRoutes", roles); 33 | console.log("筛选出需要addRoute的路由", accessRoutes); 34 | // 将左侧菜单的icon改为服务端数据 35 | filterRoutesIcon(accessRoutes, roles); 36 | // 利用 addRoute 循环添加 37 | accessRoutes.forEach(item => { 38 | router.addRoute(item); 39 | }); 40 | next({ ...to, replace: true }); 41 | } 42 | next(); 43 | } 44 | } else { 45 | // 没有token的情况下,可以进入白名单(不需要登录的界面) 46 | if (whiteList.indexOf(to.path) > -1) { 47 | next(); 48 | } else { // ,如果是需要登录的界面,去登录界面 49 | next("/login"); 50 | } 51 | } 52 | }); -------------------------------------------------------------------------------- /src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import ElementPlus from "element-plus"; 2 | import "element-plus/dist/index.css"; 3 | import locale from "element-plus/lib/locale/lang/zh-cn"; 4 | // 注册全部的svg图标 5 | import elementIcons from "@/plugins/svgicon"; 6 | import "virtual:svg-icons-register"; 7 | 8 | export default app => { 9 | app 10 | .use(ElementPlus, { locale }) 11 | .use(elementIcons); // 全局注册element svg图标 12 | }; -------------------------------------------------------------------------------- /src/plugins/svgicon.js: -------------------------------------------------------------------------------- 1 | import * as components from "@element-plus/icons-vue"; 2 | 3 | export default { 4 | install: (app) => { 5 | for (const key in components) { 6 | const componentConfig = components[key]; 7 | app.component(componentConfig.name, componentConfig); 8 | } 9 | } 10 | }; -------------------------------------------------------------------------------- /src/plugins/vite-plugin-git-info.js: -------------------------------------------------------------------------------- 1 | // getGitInfo.js 2 | import { execSync } from "child_process"; 3 | 4 | const BRANCH_COMMAND = "git rev-parse --abbrev-ref HEAD";// 获取分支名 5 | const LAST_COMMIT_HASH_COMMAND = "git rev-parse HEAD";// 获取最后一次提交的 hash 6 | const LAST_COMMIT_TIME_COMMAND = "git log -1 --pretty=format:%cd";// 获取最后一次提交的时间 7 | const LAST_COMMIT_MSG_COMMAND = "git log -1 --pretty=format:%s"; // 获取最后一次提交的 message 8 | const LAST_COMMIT_USER_COMMAND = "git log -1 --pretty=format:%an"; // 获取最后一次提交的提交者 9 | 10 | // //执行git命令 11 | const runGitCommand = async(command) => { 12 | try { 13 | const stdout = await execSync(command).toString().trim(); 14 | return stdout; 15 | } catch (error) { 16 | console.error(`Failed to run git command: ${error}`); 17 | return "Failed to run git command"; 18 | } 19 | }; 20 | 21 | const getGitInfo = async() => { 22 | try { 23 | return { 24 | branch: await runGitCommand(BRANCH_COMMAND), 25 | lastCommitHash: await runGitCommand(LAST_COMMIT_HASH_COMMAND), 26 | lastCommitMsg: await runGitCommand(LAST_COMMIT_MSG_COMMAND), 27 | lastCommitTime: await runGitCommand(LAST_COMMIT_TIME_COMMAND), 28 | lastCommitUser: await runGitCommand(LAST_COMMIT_USER_COMMAND) 29 | }; 30 | } catch (error) { 31 | console.error(`Failed to getGitInfo ${error}`); 32 | } 33 | }; 34 | 35 | const plugin = () => { 36 | return { 37 | name: "vite-plugin-git-info", 38 | async transformIndexHtml(html) { 39 | const res = await getGitInfo(); 40 | // 在 HTML 中插入一段 JavaScript 代码 41 | const scriptToAdd = JSON.stringify(res); 42 | // 在 标签里插入代码 43 | return [ 44 | { 45 | attrs: { defer: true }, 46 | children: `window._GIT_INFO=${scriptToAdd}`, 47 | inject: "head", 48 | tag: "script" 49 | } 50 | ]; 51 | } 52 | }; 53 | }; 54 | 55 | export default plugin; -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter, 3 | createWebHashHistory 4 | } from "vue-router"; 5 | import store from "@/store"; 6 | 7 | import layout from "@/layout"; 8 | import permissions from "./modules/permissions"; 9 | import third from "./modules/third"; 10 | import cssAnimation from "./modules/cssAnimation"; 11 | import vueUse from "./modules/vueUse"; 12 | 13 | /** 14 | * 私有路由表 15 | */ 16 | export const privateRoutes = [ 17 | permissions, 18 | third, 19 | cssAnimation, 20 | vueUse 21 | ]; 22 | /** 23 | * 公开路由表 24 | */ 25 | export const publicRoutes = [ 26 | { 27 | path: "/login", 28 | component: () => import("@/views/login/index") 29 | }, 30 | { 31 | path: "/", 32 | // 注意:带有路径“/”的记录中的组件“默认”是一个不返回 Promise 的函数 33 | component: layout, 34 | redirect: "/home", 35 | children: [ 36 | { 37 | path: "/home", 38 | name: "home", 39 | component: () => import("@/views/home/index"), 40 | meta: { title: "首页", icon: "home", affix: true }, // affix=true,tagViews右侧没有关闭按钮 41 | hidden: true// true不显示在侧边栏 42 | }, 43 | { 44 | path: "/404", 45 | name: "404", 46 | component: () => import("@/views/error-page/404") 47 | }, 48 | { 49 | path: "/401", 50 | name: "401", 51 | component: () => import("@/views/error-page/401") 52 | } 53 | ] 54 | } 55 | // 测试页面 56 | // { 57 | // path: '/test', 58 | // component: () => import('@/views/test-page/test'), 59 | // 60 | // }, 61 | ]; 62 | 63 | /** 64 | * 初始化路由表 65 | */ 66 | export function resetRouter() { 67 | if (store.getters.hasRoles) { 68 | const menus = store.getters.roles; 69 | // removeRoute是根据路由的name去删除路由的,所以我们要对路由的名字进行截取 70 | // const menus = ['getRoleList','admintorList','adminAuth'] 71 | // console.log("menus==",menus) 72 | // console.log("router==",router.getRoutes()) 73 | menus.forEach(menu => { 74 | const url = menu.url; 75 | const i = url.lastIndexOf("/"); 76 | const name = url.substring(i + 1, url.length); 77 | router.removeRoute(name); 78 | }); 79 | } 80 | } 81 | 82 | const router = createRouter({ 83 | history: createWebHashHistory(), 84 | // routes: [...publicRoutes, ...privateRoutes] 85 | routes: publicRoutes 86 | 87 | }); 88 | 89 | export default router; -------------------------------------------------------------------------------- /src/router/modules/permissions.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules**/ 2 | 3 | import Layout from "@/layout"; 4 | 5 | export default { 6 | path: "/adminAuth", 7 | component: Layout, 8 | redirect: "/adminAuth/getRoleList", 9 | alwaysShow: true, // will always show the root menu 10 | name: "adminAuth", 11 | meta: { 12 | title: "权限管理", 13 | icon: "permission" 14 | }, 15 | children: [ 16 | { 17 | path: "/adminAuth/getRoleList", 18 | component: () => import("@/views/permissions-page/roleList.vue"), 19 | name: "getRoleList", 20 | meta: { title: "角色列表", icon: "role" } 21 | }, 22 | { 23 | path: "/adminAuth/adminList", 24 | component: () => import("@/views/permissions-page/accountList.vue"), 25 | name: "adminList", 26 | meta: { title: "账号列表", icon: "personnel" } 27 | }, 28 | { 29 | path: "/adminAuth/permissionList", 30 | component: () => import("@/views/permissions-page/permissionList.vue"), 31 | name: "permissionList", 32 | meta: { title: "权限列表", icon: "permission" } 33 | }, 34 | { 35 | path: "/account/detail", 36 | name: "accountDetail", 37 | component: () => import("@/views/permissions-page/accountDetail.vue"), 38 | meta: { title: "账号详情", icon: "personnel" }, 39 | hidden: true// true不显示在侧边栏 40 | 41 | } 42 | 43 | ] 44 | }; -------------------------------------------------------------------------------- /src/router/modules/third.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules**/ 2 | 3 | import Layout from "@/layout"; 4 | import layout from "@/layout"; 5 | 6 | export default { 7 | path: "/third", 8 | component: layout, 9 | redirect: "/third/editor", 10 | alwaysShow: true, // will always show the root menu 11 | name: "third", 12 | meta: { 13 | title: "三方库", 14 | icon: "article" 15 | }, 16 | children: [ 17 | { 18 | path: "/third/editor", 19 | component: () => import("@/views/third-page/editor/index.vue"), 20 | name: "editor", 21 | meta: { 22 | title: "富文本", icon: "article-ranking" 23 | } 24 | }, 25 | { 26 | path: "/third/markdown", 27 | component: () => import("@/views/third-page/markdown/index.vue"), 28 | name: "markdown", 29 | meta: { 30 | title: "markdown", icon: "article-create" 31 | } 32 | } 33 | 34 | ] 35 | }; -------------------------------------------------------------------------------- /src/router/modules/vueUse.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules**/ 2 | 3 | import Layout from "@/layout"; 4 | 5 | export default { 6 | path: "/vueUse", 7 | component: Layout, 8 | redirect: "/vueUse/elements", 9 | alwaysShow: true, // will always show the root menu 10 | name: "vueUse", 11 | meta: { 12 | title: "vueUse学习", 13 | icon: "personnel" 14 | }, 15 | children: [ 16 | { 17 | path: "/vueUse/elements", 18 | redirect: "/elements/useDraggable", 19 | name: "elements", 20 | meta: { title: "elements", icon: "example" }, 21 | children: [ 22 | { 23 | path: "/elements/useDraggable", 24 | component: () => import("@/views/vue-use/elements/useDraggable.vue"), 25 | name: "useDraggable", 26 | meta: { title: "useDraggable", icon: "star" } 27 | }, 28 | { 29 | path: "/elements/useDropZone", 30 | component: () => import("@/views/vue-use/elements/useDropZone.vue"), 31 | name: "useDropZone", 32 | meta: { title: "useDropZone", icon: "star" } 33 | }, 34 | { 35 | path: "/elements/useIntersectionObserver", 36 | component: () => import("@/views/vue-use/elements/useIntersectionObserver.vue"), 37 | name: "useIntersectionObserver", 38 | meta: { title: "useIntersectionO", icon: "star" } 39 | } 40 | ] 41 | }, 42 | { 43 | path: "/vueUse/component", 44 | redirect: "/component/createReusableTemplate", 45 | name: "component", 46 | meta: { title: "component", icon: "example" }, 47 | children: [ 48 | { 49 | path: "/component/createReusableTemplate", 50 | component: () => import("@/views/vue-use/component/createReusableTemplate.vue"), 51 | name: "createReusableTemplate", 52 | meta: { title: "createReusableT", icon: "star" } 53 | } 54 | 55 | ] 56 | } 57 | ] 58 | }; -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | // import variables from '@/styles/variables.scss' 2 | import variables from "@/styles/variables.module.scss"; 3 | 4 | const getters = { 5 | 6 | token: state => state.user.token, 7 | userInfo: state => state.user.userInfo, 8 | cssVar: state => variables, 9 | sidebarOpened: state => state.app.sidebarOpened, 10 | tagsViewList: state => state.app.tagsViewList, 11 | roles: state => state.user.roles, 12 | buttons: state => state.user.buttons, 13 | hasRoles: state => { 14 | return state.user.roles && state.user.roles.length > 0; 15 | } 16 | 17 | }; 18 | export default getters; -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import getters from "./getters"; 3 | import user from "./modules/user"; 4 | import app from "./modules/app"; 5 | import permission from "./modules/permission"; 6 | 7 | export default createStore({ 8 | getters, 9 | modules: { 10 | user, 11 | app, 12 | permission 13 | } 14 | }); -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import { TAGS_VIEW } from "@/constant"; 2 | import { getItem, setItem } from "@/utils/storage"; 3 | 4 | export default { 5 | namespaced: true, 6 | state: () => ({ 7 | sidebarOpened: true, 8 | tagsViewList: getItem(TAGS_VIEW) || [] 9 | }), 10 | mutations: { 11 | triggerSidebarOpened(state) { 12 | state.sidebarOpened = !state.sidebarOpened; 13 | }, 14 | /** 15 | * 添加 tags 16 | */ 17 | addTagsViewList(state, tag) { 18 | const isFind = state.tagsViewList.find(item => { 19 | return item.path === tag.path; 20 | }); 21 | // 处理重复 22 | if (!isFind) { 23 | state.tagsViewList.push(tag); 24 | setItem(TAGS_VIEW, state.tagsViewList); 25 | } 26 | }, 27 | /** 28 | * 删除 tag 29 | * @param {type: 'other'||'right'||'index', index: index} payload 30 | */ 31 | removeTagsView(state, payload) { 32 | if (payload.type === "index") { 33 | state.tagsViewList.splice(payload.index, 1); 34 | } else if (payload.type === "other") { 35 | state.tagsViewList.splice( 36 | payload.index + 1, 37 | state.tagsViewList.length - payload.index + 1 38 | ); 39 | state.tagsViewList.splice(0, payload.index); 40 | if (payload.index != 0) { 41 | // list第一位加入删除了的首页tag 42 | state.tagsViewList.unshift({ 43 | fullPath: "/home", 44 | meta: { title: "首页", affix: true }, 45 | name: "home", 46 | params: {}, 47 | path: "/home", 48 | query: {}, 49 | title: "首页" 50 | }); 51 | } 52 | } else if (payload.type === "right") { 53 | state.tagsViewList.splice( 54 | payload.index + 1, 55 | state.tagsViewList.length - payload.index + 1 56 | ); 57 | } else if (payload.type === "all") { 58 | state.tagsViewList = []; 59 | } 60 | setItem(TAGS_VIEW, state.tagsViewList); 61 | } 62 | 63 | } 64 | }; -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | // 专门处理权限路由的模块 2 | import { privateRoutes, publicRoutes } from "@/router"; 3 | 4 | /** 5 | * 检查当前的路由是否有权限 6 | * @param roles 接口数据 7 | * @param route 8 | * @returns {boolean} 9 | */ 10 | function hasPermission(roles, route) { 11 | let hasRouter = false; 12 | for (let i = 0; i < roles.length; i++) { 13 | if (roles[i].url === route.path || "/" + roles[i].url === route.path) { 14 | hasRouter = true; 15 | break; 16 | } 17 | } 18 | 19 | return hasRouter; 20 | } 21 | 22 | /** 23 | * 根据服务端返回的路由数据,筛选过滤本地的路由数据 24 | * @param routes 本地的路由数据 25 | * @param roles 接口获取的路由数据 26 | */ 27 | export function filterPrivateRoutes(routes, roles) { 28 | const res = []; 29 | routes.forEach(route => { 30 | const tmp = { ...route }; 31 | if (hasPermission(roles, tmp)) { 32 | if (tmp.children) { 33 | tmp.children = filterPrivateRoutes(tmp.children, roles); 34 | } 35 | res.push(tmp); 36 | } 37 | }); 38 | 39 | return res; 40 | } 41 | 42 | export default { 43 | namespaced: true, 44 | state: { 45 | // 路由表:初始拥有静态路由权限 46 | routes: publicRoutes 47 | }, 48 | mutations: { 49 | /** 50 | * 增加路由 51 | */ 52 | setRoutes(state, newRoutes) { 53 | // 永远在静态路由的基础上增加新路由 54 | state.routes = [...publicRoutes, ...newRoutes]; 55 | } 56 | }, 57 | actions: { 58 | /** 59 | * 根据权限筛选路由 60 | */ 61 | generateRoutes({ commit }, roles) { 62 | return new Promise(resolve => { 63 | const accessedRoutes = filterPrivateRoutes(privateRoutes, roles); 64 | accessedRoutes.push({ 65 | path: "/:catchAll(.*)", 66 | redirect: "/404" 67 | }); 68 | commit("setRoutes", accessedRoutes); 69 | resolve(accessedRoutes); 70 | }); 71 | } 72 | } 73 | }; -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, getPermission } from "@/api/api"; 2 | import { setItem, getItem, removeAllItem } from "@/utils/storage"; 3 | import { TOKEN, USERINFO } from "@/constant"; 4 | import { setTimeStamp } from "@/utils/auth"; 5 | import { formatPermissionList, lowerCase } from "@/utils/index"; 6 | import router, { resetRouter } from "@/router"; 7 | import { ElMessage } from "element-plus"; 8 | 9 | export default { 10 | namespaced: true, 11 | state: () => ({ 12 | // token的初始值从storage里取 13 | token: getItem(TOKEN) || "", 14 | userInfo: getItem(USERINFO) || {}, 15 | roles: [], 16 | buttons: [] 17 | }), 18 | mutations: { 19 | setToken(state, token) { 20 | state.token = token; 21 | setItem(TOKEN, token); 22 | }, 23 | setUserInfo(state, userInfo) { 24 | state.userInfo = userInfo; 25 | setItem(USERINFO, userInfo); 26 | }, 27 | setRoles: (state, roles) => { 28 | state.roles = roles; 29 | }, 30 | setButtons: (state, buttons) => { 31 | state.buttons = buttons; 32 | } 33 | }, 34 | actions: { 35 | login(context, userInfo) { 36 | const { username, password, captcha_code, code_key } = userInfo; 37 | return new Promise((resolve, reject) => { 38 | login({ 39 | username, 40 | password, 41 | captcha_code, 42 | code_key 43 | }) 44 | .then(data => { 45 | this.commit("user/setToken", data.obj.sys_token); 46 | this.commit("user/setUserInfo", data.obj); 47 | // 保存登录时间 48 | setTimeStamp(); 49 | resolve(); 50 | }) 51 | .catch(err => { 52 | reject(err); 53 | }); 54 | // 本地模拟数据 55 | // console.log("----模拟【登录】接口数据,真实数据需要填写constant.js里的接口域名------") 56 | // const loginData =import.meta.glob('@/api/loginData.json', { eager: true }) 57 | // let obj = loginData['/src/api/loginData.json'].default 58 | // this.commit('user/setToken', obj.sys_token) 59 | // this.commit('user/setUserInfo', obj) 60 | // // 保存登录时间 61 | // setTimeStamp() 62 | // resolve() 63 | }); 64 | }, 65 | getPermissionData(context) { 66 | return new Promise((resolve, reject) => { 67 | getPermission() 68 | .then(data => { 69 | const obj = formatPermissionList(data.obj); 70 | const role_arr = obj.role_arr;// 菜单权限 71 | const button_arr = obj.button_arr;// button权限 72 | const info = { 73 | roles: role_arr 74 | }; 75 | if (role_arr.length == 0) { 76 | ElMessage.error("您登录的账号暂无权限!"); // 提示错误信息 77 | this.dispatch("user/logout"); 78 | } 79 | this.commit("user/setRoles", role_arr); 80 | this.commit("user/setButtons", button_arr); 81 | resolve(info); 82 | }) 83 | .catch(err => { 84 | 85 | }); 86 | }); 87 | }, 88 | logout() { 89 | resetRouter(); 90 | this.commit("user/setToken", ""); 91 | this.commit("user/setUserInfo", {}); 92 | this.commit("user/setRoles", []); 93 | this.commit("user/setButtons", []); 94 | this.commit("app/removeTagsView", { 95 | type: "all" 96 | }); 97 | removeAllItem(); 98 | router.push("/login"); 99 | } 100 | } 101 | }; -------------------------------------------------------------------------------- /src/styles/element.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | .el-breadcrumb__inner, 3 | .el-breadcrumb__inner a { 4 | font-weight: 400 !important; 5 | } 6 | .el-upload { 7 | input[type="file"] { 8 | display: none !important; 9 | } 10 | } 11 | .el-upload__input { 12 | display: none; 13 | } 14 | .cell { 15 | .el-tag { 16 | margin-right: 0; 17 | } 18 | } 19 | .small-padding { 20 | .cell { 21 | padding-left: 5px; 22 | padding-right: 5px; 23 | } 24 | } 25 | .fixed-width { 26 | .el-button--mini { 27 | padding: 7px 10px; 28 | min-width: 60px; 29 | } 30 | } 31 | .status-col { 32 | .cell { 33 | padding: 0 10px; 34 | text-align: center; 35 | .el-tag { 36 | margin-right: 0; 37 | } 38 | } 39 | } 40 | 41 | // to fixed https://github.com/ElemeFE/element/issues/2461 42 | .el-dialog { 43 | position: relative; 44 | left: 0; 45 | margin: 0 auto; 46 | transform: none; 47 | } 48 | 49 | // refine element ui upload 50 | .upload-container { 51 | .el-upload { 52 | width: 100%; 53 | .el-upload-dragger { 54 | width: 100%; 55 | height: 200px; 56 | } 57 | } 58 | } 59 | 60 | // dropdown 61 | .el-dropdown-menu { 62 | a { 63 | display: block; 64 | } 65 | } 66 | 67 | // fix date-picker ui bug in filter-item 68 | .el-range-editor.el-input__inner { 69 | display: inline-flex !important; 70 | } 71 | 72 | // to fix el-date-picker css-animation style 73 | .el-range-separator { 74 | box-sizing: content-box; 75 | } 76 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "variables.module"; 2 | @import "./mixin.scss"; 3 | @import "./sidebar.scss"; 4 | @import "./element.scss"; 5 | @import "./transition.scss"; 6 | html, 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | height: 100%; 11 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", 12 | "Microsoft YaHei", Arial, sans-serif; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-font-smoothing: antialiased; 15 | text-rendering: optimizelegibility; 16 | } 17 | #app { 18 | height: 100%; 19 | } 20 | *, 21 | *::before, 22 | *::after { 23 | box-sizing: inherit; 24 | margin: 0; 25 | padding: 0; 26 | } 27 | a:focus, 28 | a:active { 29 | outline: none; 30 | } 31 | a, 32 | a:focus, 33 | a:hover { 34 | cursor: pointer; 35 | text-decoration: none; 36 | color: inherit; 37 | } 38 | div:focus { 39 | outline: none; 40 | } 41 | .clearfix { 42 | &::after { 43 | display: block; 44 | visibility: hidden; 45 | clear: both; 46 | height: 0; 47 | font-size: 0; 48 | content: " "; 49 | } 50 | } 51 | .avatar-upload-preview { 52 | overflow: hidden; 53 | position: absolute; 54 | top: 50%; 55 | border-radius: 50%; 56 | width: 200px; 57 | height: 200px; 58 | box-shadow: 0 0 4px #ccc; 59 | transform: translate(50%, -50%); 60 | } 61 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | //定义通用的 `css-animation` 2 | @mixin clearfix { 3 | &::after { 4 | display: table; 5 | clear: both; 6 | content: ""; 7 | } 8 | } 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | &::-webkit-scrollbar { 14 | width: 6px; 15 | } 16 | &::-webkit-scrollbar-thumb { 17 | border-radius: 20px; 18 | background: #99a9bf; 19 | } 20 | } 21 | @mixin relative { 22 | position: relative; 23 | width: 100%; 24 | height: 100%; 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | //处理 `menu` 菜单的样式 2 | #app { 3 | .main-container { 4 | position: relative; 5 | margin-left: $sideBarWidth; 6 | min-height: 100%; 7 | background: #f7f7f7; 8 | transition: margin-left #{$sideBarDuration}; 9 | } 10 | .sidebar-container { 11 | overflow: hidden; 12 | position: fixed; 13 | left: 0; 14 | top: 0; 15 | bottom: 0; 16 | z-index: 1001; 17 | width: $sideBarWidth !important; 18 | height: 100%; 19 | background-color: $menuBg; 20 | box-shadow: 1px 1px 4px rgb(0 21 41 / 0.08); 21 | font-size: 0; 22 | transition: width #{$sideBarDuration}; 23 | 24 | // 重置 element-plus 的css 25 | .horizontal-collapse-transition { 26 | transition: 27 | 0s width ease-in-out, 28 | 0s padding-left ease-in-out, 29 | 0s padding-right ease-in-out; 30 | } 31 | .scrollbar-wrapper { 32 | overflow-x: hidden !important; 33 | margin-top: 2px; 34 | } 35 | .el-scrollbar__bar.is-vertical { 36 | right: 0; 37 | } 38 | .el-scrollbar { 39 | height: 100%; 40 | } 41 | &.has-logo { 42 | .el-scrollbar { 43 | height: calc(100% - 50px); 44 | } 45 | } 46 | .is-horizontal { 47 | display: none; 48 | } 49 | a { 50 | display: inline-block; 51 | overflow: hidden; 52 | width: 100%; 53 | } 54 | .svg-icon { 55 | margin-right: 16px; 56 | } 57 | .sub-el-icon { 58 | margin-left: -2px; 59 | margin-right: 12px; 60 | } 61 | .el-menu { 62 | border: none; 63 | width: 100% !important; 64 | height: 100%; 65 | } 66 | .el-sub-menu:last-child { 67 | margin-bottom: 80px !important; 68 | } 69 | .el-menu .el-sub-menu__title { 70 | height: 50px !important; 71 | line-height: 50px !important; 72 | font-weight: bold; 73 | font-size: 13px !important; 74 | } 75 | .el-menu .el-menu-item { 76 | height: 40px !important; 77 | line-height: 40px !important; 78 | font-size: 13px !important; 79 | } 80 | .is-active > .el-sub-menu__title { 81 | color: $subMenuActiveText !important; 82 | } 83 | & .nest-menu .el-sub-menu > .el-sub-menu__title, 84 | & .el-sub-menu .el-menu-item { 85 | min-width: $sideBarWidth !important; 86 | 87 | //background-color: $subMenuBg !important; 88 | } 89 | } 90 | .hideSidebar { 91 | .sidebar-container { 92 | width: 54px !important; 93 | } 94 | .main-container { 95 | margin-left: 54px; 96 | } 97 | .sub-menu-title-noDropdown { 98 | position: relative; 99 | padding: 0 !important; 100 | .el-tooltip { 101 | padding: 0 !important; 102 | .svg-icon { 103 | margin-left: 20px; 104 | } 105 | .sub-el-icon { 106 | margin-left: 19px; 107 | } 108 | } 109 | } 110 | .el-sub-menu { 111 | overflow: hidden; 112 | & > .el-sub-menu__title { 113 | padding: 0 !important; 114 | .svg-icon { 115 | margin-left: 20px; 116 | } 117 | .sub-el-icon { 118 | margin-left: 19px; 119 | } 120 | .el-sub-menu__icon-arrow { 121 | display: none; 122 | } 123 | } 124 | } 125 | .el-menu--collapse { 126 | .el-sub-menu { 127 | & > .el-sub-menu__title { 128 | & > span { 129 | display: inline-block; 130 | visibility: hidden; 131 | overflow: hidden; 132 | width: 0; 133 | height: 0; 134 | } 135 | } 136 | } 137 | } 138 | } 139 | .el-menu--collapse .el-menu .el-sub-menu { 140 | min-width: $sideBarWidth !important; 141 | } 142 | .withoutAnimation { 143 | .main-container, 144 | .sidebar-container { 145 | transition: 0; 146 | } 147 | } 148 | } 149 | .el-menu--vertical { 150 | & > .el-menu { 151 | .svg-icon { 152 | margin-right: 16px; 153 | } 154 | .sub-el-icon { 155 | margin-left: -2px; 156 | margin-right: 12px; 157 | } 158 | } 159 | 160 | // 菜单项过长时 161 | > .el-menu--popup { 162 | overflow-y: auto; 163 | max-height: 100vh; 164 | &::-webkit-scrollbar-track-piece { 165 | background: #d3dce6; 166 | } 167 | &::-webkit-scrollbar { 168 | width: 6px; 169 | } 170 | &::-webkit-scrollbar-thumb { 171 | border-radius: 20px; 172 | background: #99a9bf; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb-enter-active, 2 | .breadcrumb-leave-active { 3 | transition: all 0.5s; 4 | } 5 | .breadcrumb-enter-from, 6 | .breadcrumb-leave-active { 7 | opacity: 0; 8 | transform: translateX(20px); 9 | } 10 | .breadcrumb-leave-active { 11 | position: absolute; 12 | } 13 | /* fade-transform */ 14 | .fade-transform-leave-active, 15 | .fade-transform-enter-active { 16 | transition: all 0.5s; 17 | } 18 | .fade-transform-enter-from { 19 | opacity: 0; 20 | transform: translateX(-30px); 21 | } 22 | .fade-transform-leave-to { 23 | opacity: 0; 24 | transform: translateX(30px); 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/variables.module.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText: #bfcbd9; 3 | $menuActiveText: #fff; 4 | $subMenuActiveText: #fff; 5 | 6 | $menuBg: #304156; 7 | $menuHover: #263445; 8 | 9 | $subMenuBg: #1f2d3d; 10 | $subMenuHover: #001528; 11 | 12 | $sideBarWidth: 210px; 13 | $hideSideBarWidth: 54px; 14 | $sideBarDuration: 0.28s; 15 | 16 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 17 | // JS 与 scss 共享变量,在 scss 中通过 :export 进行导出,在 js 中可通过 ESM 进行导入 18 | :export { 19 | menuText: $menuText; 20 | menuActiveText: $menuActiveText; 21 | subMenuActiveText: $subMenuActiveText; 22 | menuBg: $menuBg; 23 | menuHover: $menuHover; 24 | subMenuBg: $subMenuBg; 25 | subMenuHover: $subMenuHover; 26 | sideBarWidth: $sideBarWidth; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import { TIME_STAMP, TOKEN_TIMEOUT_VALUE } from "@/constant"; 2 | import { setItem, getItem } from "@/utils/storage"; 3 | /** 4 | * 获取时间戳 5 | */ 6 | export function getTimeStamp() { 7 | return getItem(TIME_STAMP); 8 | } 9 | /** 10 | * 设置时间戳 11 | */ 12 | export function setTimeStamp() { 13 | setItem(TIME_STAMP, Date.now()); 14 | } 15 | /** 16 | * 是否超时 17 | */ 18 | export function isCheckTimeout() { 19 | // 当前时间戳 20 | const currentTime = Date.now(); 21 | // 缓存时间戳 22 | const timeStamp = getTimeStamp(); 23 | return currentTime - timeStamp > TOKEN_TIMEOUT_VALUE; 24 | } -------------------------------------------------------------------------------- /src/utils/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "@/store"; 3 | import { ElMessage } from "element-plus"; 4 | import { 5 | VERSION, 6 | MODEL_TEST_VERSION, 7 | SERVER_TYPE, 8 | AXIOS_TIMEOUT 9 | 10 | } from "@/constant"; 11 | 12 | import { switchServerUrl } from "@/utils/index"; 13 | 14 | /** 15 | * axios请求拦截器 16 | * @param {object} config axios请求配置对象 17 | * @return {object} 请求成功或失败时返回的配置对象或者promise error对象 18 | **/ 19 | axios.interceptors.request.use(config => { 20 | return config; 21 | }, error => { 22 | return Promise.reject(error); 23 | }); 24 | 25 | /** 26 | * axios 响应拦截器 27 | * @param {object} response 从服务端响应的数据对象或者error对象 28 | * @return {object} 响应成功或失败时返回的响应对象或者promise error对象 29 | **/ 30 | axios.interceptors.response.use(response => { 31 | return response; 32 | }, error => { 33 | return Promise.reject(error); 34 | }); 35 | 36 | export default function http(options) { 37 | // 获取不同环境的请求域名 38 | const server_url = switchServerUrl(); 39 | 40 | let opt = {}; 41 | const method = options.method || "post"; 42 | const url = options.url; 43 | const data = options.data || {}; 44 | if (!options.url) { 45 | console.error("url参数缺失"); 46 | return; 47 | } 48 | if (store.getters.token) { 49 | data.sys_token = store.getters.token; 50 | } 51 | if (method == "get") { 52 | opt = { 53 | method, 54 | baseURL: "", 55 | url: url.indexOf("//") > -1 ? url : (server_url + url), 56 | params: data, 57 | timeout: AXIOS_TIMEOUT 58 | }; 59 | } else if (method == "post") { 60 | opt = { 61 | method, 62 | baseURL: "", 63 | url: url.indexOf("//") > -1 ? url : (server_url + url), 64 | data, // qs.stringify(data) 65 | timeout: AXIOS_TIMEOUT 66 | }; 67 | } 68 | return new Promise((resolve, reject) => { 69 | axios(opt).then(res => { 70 | if (res && (res.status === 200 || res.status === 304 || res.status === 400)) { 71 | const data = res.data; 72 | if (data.status && data.status.error_code == 0) { 73 | resolve(data); 74 | } else if (data.status && (data.status.error_code == 101 || data.status.error_code == 102 || data.status.error_msg == "您还没有登录")) { // 101请获取权限 102登录失效 75 | ElMessage.error(data.status.error_msg); // 提示错误信息 76 | // 登出操作 77 | store.dispatch("user/logout"); 78 | } else { 79 | ElMessage.error(data.status.error_msg || "网络异常,请稍后重试!"); // 提示错误信息 80 | reject(data); 81 | } 82 | } else { 83 | ElMessage.error(res || "网络异常,请稍后重试!"); // 提示错误信息 84 | reject("网络异常,请稍后重试"); 85 | } 86 | }, err => { 87 | ElMessage.error(err); // 提示错误信息 88 | reject(err); 89 | }); 90 | }); 91 | } -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import store from "@/store"; 3 | import { 4 | VERSION, 5 | MODEL_TEST_VERSION, 6 | SERVER_TYPE, 7 | TEST_URL, 8 | MO_URL, 9 | YFB_URL, 10 | PRO_URL, 11 | DEV_URL 12 | } from "@/constant"; 13 | 14 | // 将字符串的字符全部转换为小写字符 15 | export function lowerCase(str) { 16 | const arr = str.split(""); 17 | let newStr = ""; 18 | // 通过for循环遍历数组 19 | for (let i = 0; i < arr.length; i++) { 20 | if (arr[i] >= "A" && arr[i] <= "Z") { newStr += arr[i].toLowerCase(); } else { newStr += arr[i]; } 21 | } 22 | return newStr; 23 | } 24 | 25 | // 数据导出(要求接口是get方法啊) 26 | export function exportDataFormatUrl(request_url, request_params, is_new) { 27 | const server_url = switchServerUrl(); 28 | 29 | let url = server_url + request_url; 30 | 31 | if (SERVER_TYPE == 3) { 32 | url = url + "/version/" + VERSION; 33 | } else { 34 | url = url + "/version/" + MODEL_TEST_VERSION; 35 | } 36 | 37 | const params = JSON.parse(JSON.stringify(request_params)); 38 | if (store.getters.token) { 39 | params.sys_token = store.getters.token; 40 | } 41 | let data = "?"; 42 | for (const key in params) { 43 | data = data + "&" + key + "=" + params[key]; 44 | } 45 | 46 | url = url + data; 47 | 48 | console.log(url); 49 | 50 | if (is_new) { 51 | // 打开新窗口 52 | window.open(url); 53 | } else { 54 | // 在本窗口打开 55 | window.location.href = url; 56 | } 57 | } 58 | 59 | // 获取当前服务器的请求url 60 | export function switchServerUrl() { 61 | let server_url = ""; 62 | switch (SERVER_TYPE) { 63 | case 0: 64 | server_url = TEST_URL; 65 | break; 66 | case 1: 67 | server_url = MO_URL; 68 | break; 69 | case 2: 70 | server_url = YFB_URL; 71 | break; 72 | case 3: 73 | server_url = PRO_URL; 74 | break; 75 | case 4: 76 | server_url = DEV_URL; 77 | break; 78 | } 79 | return server_url; 80 | } 81 | 82 | /** 83 | * 格式换权限菜单返回数据 84 | * @param data 85 | */ 86 | export function formatPermissionList(data) { 87 | const list = data; 88 | const role_arr = [];// 菜单权限 89 | const button_arr = [];// button权限 90 | // 循环一级列表 91 | for (const i in list) { 92 | const i_item = list[i].children; 93 | // 循环2级列表 94 | for (const j in i_item) { 95 | const j_item = i_item[j]; 96 | if (j_item.url) { 97 | if (j == 0) { 98 | role_arr.push({ 99 | url: "/" + list[i].url.split("/")[1], 100 | icon: list[i].icon 101 | }); 102 | } 103 | role_arr.push({ 104 | url: j_item.url, 105 | icon: j_item.icon 106 | 107 | }); 108 | // button权限赋值存起来 109 | const k_item = j_item.buttonList; 110 | for (const k in k_item) { 111 | if (k_item[k].url) { 112 | button_arr.push(lowerCase(k_item[k].url)); 113 | } 114 | } 115 | } 116 | const i_item_c = j_item.children; 117 | // 循环3级列表 118 | for (const z in i_item_c) { 119 | const z_item = i_item_c[z]; 120 | if (z_item.url) { 121 | role_arr.push({ 122 | url: z_item.url, 123 | icon: z_item.icon 124 | 125 | }); 126 | } 127 | } 128 | } 129 | } 130 | return { role_arr, button_arr }; 131 | } -------------------------------------------------------------------------------- /src/utils/route.js: -------------------------------------------------------------------------------- 1 | import path from "path-browserify"; 2 | 3 | /** 4 | * 返回所有子路由 5 | */ 6 | const getChildrenRoutes = routes => { 7 | const result = []; 8 | routes.forEach(route => { 9 | if (route.children && route.children.length > 0) { 10 | result.push(...route.children); 11 | } 12 | }); 13 | return result; 14 | }; 15 | /** 16 | * 处理脱离层级的路由:某个一级路由为其他子路由,则剔除该一级路由,保留路由层级 17 | * @param {*} routes router.getRoutes() 18 | */ 19 | export const filterRouters = routes => { 20 | const childrenRoutes = getChildrenRoutes(routes); 21 | return routes.filter(route => { 22 | return !childrenRoutes.find(childrenRoute => { 23 | return childrenRoute.path === route.path; 24 | }); 25 | }); 26 | }; 27 | 28 | /** 29 | * 判断数据是否为空值 30 | */ 31 | function isNull(data) { 32 | if (!data) return true; 33 | if (JSON.stringify(data) === "{}") return true; 34 | if (JSON.stringify(data) === "[]") return true; 35 | return false; 36 | } 37 | /** 38 | * 根据 routes 数据,返回对应 menu 规则数组 39 | */ 40 | export function generateMenus(routes, basePath = "") { 41 | const result = []; 42 | // 遍历路由表 43 | routes.forEach(item => { 44 | // 不存在 children && 不存在 meta 直接 return 45 | if (isNull(item.meta) && isNull(item.children)) return; 46 | // 存在 children 不存在 meta,进入迭代 47 | if (isNull(item.meta) && !isNull(item.children)) { 48 | result.push(...generateMenus(item.children)); 49 | return; 50 | } 51 | // 合并 path 作为跳转路径 52 | const routePath = path.resolve(basePath, item.path); 53 | // 路由分离之后,存在同名父路由的情况,需要单独处理 54 | let route = result.find(item => item.path === routePath); 55 | if (!route) { 56 | route = { 57 | ...item, 58 | path: routePath, 59 | children: [] 60 | }; 61 | 62 | // title 必须存在 63 | if (route.meta.title) { 64 | // meta 存在生成 route 对象,放入 arr 65 | result.push(route); 66 | } 67 | } 68 | 69 | // 存在 children 进入迭代到children 70 | if (item.children) { 71 | route.children.push(...generateMenus(item.children, route.path)); 72 | } 73 | }); 74 | return result; 75 | } -------------------------------------------------------------------------------- /src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2; 3 | if (t < 1) { 4 | return c / 2 * t * t + b; 5 | } 6 | t--; 7 | return -c / 2 * (t * (t - 2) - 1) + b; 8 | }; 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | const requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60); }; 13 | })(); 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount; 21 | document.body.parentNode.scrollTop = amount; 22 | document.body.scrollTop = amount; 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop; 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position(); 36 | const change = to - start; 37 | const increment = 20; 38 | let currentTime = 0; 39 | duration = (typeof (duration) === "undefined") ? 500 : duration; 40 | const animateScroll = function() { 41 | // increment the time 42 | currentTime += increment; 43 | // find the value with the quadratic in-out easing function 44 | const val = Math.easeInOutQuad(currentTime, start, change, duration); 45 | // move the document.body 46 | move(val); 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll); 50 | } else { 51 | if (callback && typeof (callback) === "function") { 52 | // the animation is done so lets callback 53 | callback(); 54 | } 55 | } 56 | }; 57 | animateScroll(); 58 | } -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 存储数据 3 | */ 4 | export const setItem = (key, value) => { 5 | // 将数组、对象类型的数据转化为 JSON 字符串进行存储 6 | if (typeof value === "object") { 7 | value = JSON.stringify(value); 8 | } 9 | window.localStorage.setItem(key, value); 10 | }; 11 | 12 | /** 13 | * 获取数据 14 | */ 15 | export const getItem = key => { 16 | const data = window.localStorage.getItem(key); 17 | try { 18 | return JSON.parse(data); 19 | } catch (err) { 20 | return data; 21 | } 22 | }; 23 | 24 | /** 25 | * 删除数据 26 | */ 27 | export const removeItem = key => { 28 | window.localStorage.removeItem(key); 29 | }; 30 | 31 | /** 32 | * 删除所有数据 33 | */ 34 | export const removeAllItem = key => { 35 | window.localStorage.clear(); 36 | }; -------------------------------------------------------------------------------- /src/utils/tags.js: -------------------------------------------------------------------------------- 1 | const whiteList = ["/login", "/import", "/404", "/401"]; 2 | 3 | /** 4 | * path 是否需要被缓存 ,404这些界面都不需要被保存 5 | * @param {*} path 6 | * @returns 7 | */ 8 | export function isTags(path) { 9 | return !whiteList.includes(path); 10 | } -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断是否为外部资源 3 | */ 4 | export function isExternal(path) { 5 | return /^(https?:|mailto:|tel:)/.test(path); 6 | } -------------------------------------------------------------------------------- /src/views/css-animation/bubbleFloat.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 132 | 133 | -------------------------------------------------------------------------------- /src/views/css-animation/clock.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 81 | 82 | -------------------------------------------------------------------------------- /src/views/css-animation/filpCard.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 79 | 80 | -------------------------------------------------------------------------------- /src/views/css-animation/hoverFillText.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 52 | 53 | -------------------------------------------------------------------------------- /src/views/css-animation/hoverShiningBtn.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 78 | 79 | -------------------------------------------------------------------------------- /src/views/css-animation/hoverSlideMenu.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 112 | 113 | -------------------------------------------------------------------------------- /src/views/css-animation/slidePic.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 61 | 62 | -------------------------------------------------------------------------------- /src/views/css-animation/videoMaskText.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 47 | 48 | -------------------------------------------------------------------------------- /src/views/css-animation/waveloading.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 61 | 62 | -------------------------------------------------------------------------------- /src/views/error-page/401.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 101 | 102 | -------------------------------------------------------------------------------- /src/views/error-page/404.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 101 | 102 | -------------------------------------------------------------------------------- /src/views/login/rules.js: -------------------------------------------------------------------------------- 1 | export const validatePassword = () => { 2 | return (rule, value, callback) => { 3 | if (value.length < 6) { 4 | callback(new Error("密码不能少于6位")); 5 | } else { 6 | callback(); 7 | } 8 | }; 9 | }; 10 | 11 | export const validateCode = () => { 12 | return (rule, value, callback) => { 13 | if (value.length < 4) { 14 | callback(new Error("验证码不能少于4位")); 15 | } else { 16 | callback(); 17 | } 18 | }; 19 | }; -------------------------------------------------------------------------------- /src/views/permissions-page/accountDetail.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 110 | 111 | 148 | -------------------------------------------------------------------------------- /src/views/permissions-page/components/distributePermission.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/permissions-page/components/roles.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/views/permissions-page/permissionList.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 99 | 100 | 137 | 138 | -------------------------------------------------------------------------------- /src/views/third-page/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /src/views/third-page/components/Markdown.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | -------------------------------------------------------------------------------- /src/views/third-page/editor/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/views/third-page/markdown/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/views/vue-use/component/createReusableTemplate.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 50 | 52 | 53 | -------------------------------------------------------------------------------- /src/views/vue-use/elements/useDraggable.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | -------------------------------------------------------------------------------- /src/views/vue-use/elements/useDropZone.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 38 | -------------------------------------------------------------------------------- /src/views/vue-use/elements/useIntersectionObserver.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | // import path from 'path-browserify' 4 | import path from "path"; 5 | 6 | import { viteMockServe } from "vite-plugin-mock"; 7 | import vitePluginGitInfo from "./src/plugins/vite-plugin-git-info.js"; 8 | 9 | // svg-icon插件 10 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; 11 | export default defineConfig({ 12 | base: "./", // 打包路径s 13 | plugins: [ 14 | vue(), 15 | createSvgIconsPlugin({ 16 | // 指定要缓存的图标文件夹 17 | iconDirs: [path.resolve("./src/icons/svg")], 18 | // 执行icon name的格式 19 | symbolId: "icon-[name]" 20 | }), 21 | viteMockServe({ 22 | enable: false, 23 | logger: true, 24 | mockPath: "./src/mock/", 25 | supportTs: false 26 | 27 | }), 28 | vitePluginGitInfo() 29 | 30 | ], 31 | resolve: { 32 | alias: { 33 | "@": path.resolve("./src") 34 | }, 35 | extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"] 36 | }, 37 | server: { 38 | cors: true, // 允许跨域 39 | host: "0.0.0.0", 40 | open: true, // 服务启动时是否自动打开浏览器 41 | port: 9999, // 服务端口号 42 | proxy: { 43 | "/api": { 44 | changeOrigin: true, 45 | rewrite: (path) => path.replace(/^\/api/, ""), 46 | target: "http://127.0.0.1:9999/" 47 | // target: "http://localhost:8080/" 48 | 49 | } 50 | } 51 | 52 | } 53 | 54 | }); --------------------------------------------------------------------------------