├── .editorconfig ├── .env.development ├── .env.production ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── deploy └── auto-upload.js ├── index.html ├── mock ├── _mockProdServer.js ├── _util.js └── data.js ├── package.json ├── prettier.config.js ├── public └── favicon.ico ├── scripts └── preinstall.js ├── src ├── api │ └── user.js ├── assets │ ├── dio.jpg │ ├── element-logo.svg │ ├── logo2.ico │ ├── vlogo.png │ └── year2022.svg ├── components │ ├── AppEmotion │ │ ├── EmotionBox.vue │ │ ├── EmotionTab.vue │ │ └── index.vue │ ├── AppExplain │ │ └── index.vue │ ├── AppIcon │ │ └── index.vue │ ├── AppLink │ │ └── index.vue │ ├── CompareBox │ │ └── index.vue │ ├── EasyNav │ │ └── index.vue │ ├── EmotionBox │ │ ├── emotion-list.js │ │ ├── index.vue │ │ └── useEmotions.js │ ├── ResizeBox │ │ └── index.vue │ ├── SilkRibbon │ │ └── index.vue │ └── Todo │ │ ├── TodoAdd.vue │ │ ├── TodoFilter.vue │ │ ├── TodoList.vue │ │ ├── TodoListItem.vue │ │ └── index.vue ├── directives │ ├── click-outside.js │ ├── default-img.js │ ├── drag.js │ └── role.js ├── hooks │ ├── emotion │ │ ├── emotion-list.js │ │ └── useEmotions.js │ ├── usePageFn.js │ └── useTracking.js ├── icons │ ├── QQ.svg │ ├── calendar.svg │ ├── github.svg │ ├── heart.svg │ ├── rank.svg │ ├── wechat.svg │ ├── 笑哭.svg │ └── 酷.svg ├── layout │ ├── components │ │ ├── AppMain │ │ │ └── index.vue │ │ ├── Navbar │ │ │ ├── AvatarMenu.vue │ │ │ ├── Breadcrumb.vue │ │ │ ├── Hamburger.vue │ │ │ └── index.vue │ │ ├── Settings │ │ │ ├── SettingItem.vue │ │ │ └── index.vue │ │ ├── Sidebar │ │ │ ├── SidebarItem.vue │ │ │ ├── SidebarLogo.vue │ │ │ ├── index.vue │ │ │ └── useMenu.js │ │ ├── TabBar │ │ │ ├── TarBarItem.vue │ │ │ ├── index.vue │ │ │ └── useTabBar.js │ │ └── index.js │ └── index.vue ├── main.js ├── plugin │ ├── createSVGSprites.js │ └── mockServe.js ├── router │ ├── CONSTANT.js │ ├── README.md │ ├── helper.js │ ├── index.js │ └── modules │ │ ├── async.js │ │ ├── basic.js │ │ ├── const.js │ │ └── nested.js ├── store │ ├── example.js │ ├── index.js │ ├── layout.js │ ├── style.js │ └── user.js ├── styles │ ├── _mixins.scss │ ├── _variables.scss │ ├── common.scss │ ├── element-plus.scss │ └── vars.module.scss ├── utils │ ├── .playground.js │ ├── compRegister.js │ ├── convert.js │ ├── refreshToken.ts │ ├── request.js │ ├── storage.js │ └── util.js └── views │ ├── about │ └── index.vue │ ├── dashboard │ ├── components │ │ ├── BilibiliState │ │ │ └── index.vue │ │ ├── Cards.js │ │ ├── EarningPosition │ │ │ └── index.vue │ │ ├── GithubState │ │ │ ├── githubCat.svg │ │ │ └── index.vue │ │ └── WeChatWallet │ │ │ ├── index.vue │ │ │ └── wechat-receiving.jpg │ ├── index.vue │ └── options │ │ ├── navList.js │ │ └── pie1option.js │ ├── demo │ ├── example-page │ │ ├── scroll-page.vue │ │ └── watermark-page.vue │ ├── example │ │ ├── compare-demo │ │ │ └── index.vue │ │ ├── emotion-demo │ │ │ └── index.vue │ │ ├── file-download │ │ │ └── index.vue │ │ ├── file-upload │ │ │ └── index.vue │ │ └── text-editor │ │ │ └── index.vue │ ├── icons │ │ └── index.vue │ ├── nested │ │ ├── menu1 │ │ │ ├── index.vue │ │ │ ├── menu1-1 │ │ │ │ └── index.vue │ │ │ ├── menu1-2 │ │ │ │ ├── index.vue │ │ │ │ ├── menu1-2-1 │ │ │ │ │ └── index.vue │ │ │ │ └── menu1-2-2 │ │ │ │ │ └── index.vue │ │ │ └── menu1-3 │ │ │ │ └── index.vue │ │ └── menu2 │ │ │ └── index.vue │ ├── permission │ │ ├── admin.vue │ │ ├── page.vue │ │ └── test.vue │ └── profile │ │ ├── cnblogs-logo.svg │ │ ├── index.vue │ │ └── juejin-logo.svg │ ├── sys │ ├── error-page │ │ ├── 401.vue │ │ ├── 404.vue │ │ └── building.vue │ ├── login │ │ ├── index.vue │ │ ├── loginBackgronds.js │ │ └── useLogin.js │ └── redirect │ │ └── index.vue │ └── test │ └── test1.vue ├── stylelint.config.js └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | # 编辑器代码格式规范 VSCode需要插件支持 3 | 4 | # 表明是最顶层的配置文件,发现设为 true 时,才会停止查找.editorconfig 文件 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | # Tips: 如要避免 warning: LF will be replaced by CRLF: 12 | # 命令行: git config --global core.autocrlf false 13 | end_of_line = lf 14 | insert_final_newline = true # 去除行首任意空白字符 15 | trim_trailing_whitespace = true # 始终在文件末尾插入一个新行 16 | 17 | [*.md] 18 | max_line_length = off 19 | insert_final_newline = false 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # docs https://cn.vitejs.dev/guide/env-and-mode.html#env-variables 2 | # use: import.meta.env.VITE_XXX 3 | 4 | # public path 5 | VITE_PUBLIC_PATH = / 6 | 7 | # vue router history mode : hash | history 8 | VITE_ROUTER_HISTORY = history 9 | 10 | # 默认标题 11 | VITE_DEFAULT_TITLE = "Vue Admin Dev" 12 | 13 | # 请求根路径 作用于/src/utils/request.js中 14 | VITE_BASE_URL = /api 15 | 16 | # 展示设置按钮 17 | VITE_SHOW_SETTINGS = true 18 | 19 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # docs https://cn.vitejs.dev/guide/env-and-mode.html#env-variables 2 | # use: import.meta.env.VITE_XXX 3 | 4 | # public path 5 | VITE_PUBLIC_PATH = / 6 | 7 | # vue router history mode : hash | history 8 | VITE_ROUTER_HISTORY = hash 9 | 10 | # 默认标题 11 | VITE_DEFAULT_TITLE = "Vue Admin Prod" 12 | 13 | # 请求根路径 作用于/src/utils/request.js中 14 | VITE_BASE_URL = /api 15 | 16 | # 展示设置按钮 17 | VITE_SHOW_SETTINGS = true 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # 参考 https://docs.github.com/cn/get-started 2 | # 建立一个名为 CI 的工作流(非必要属性,默认值为文件名) “在什么事件触发,在哪个环境下,要做哪些任务” 3 | name: ci 4 | # 监听Github仓库 main 分支上的push事件 5 | on: 6 | push: 7 | branches: [ main ] 8 | # 定义要做哪些任务 ★ 9 | jobs: 10 | build-and-deploy: 11 | runs-on: ubuntu-latest # 指定虚拟机版本 12 | steps: # ★ 指定该任务有哪些步骤 13 | - uses: actions/checkout@v2 # 1. 获取项目源码 14 | 15 | - uses: actions/setup-node@v2.5.1 # 2. 设置 node.js 环境 16 | with: 17 | node-version: 16.x 18 | 19 | # yarn缓存 https://github.com/actions/cache/blob/main/examples.md#node---yarn 20 | - name: Get yarn cache directory path 21 | id: yarn-cache-dir-path 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | - uses: actions/cache@v2 24 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 25 | with: 26 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 27 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-yarn- 30 | 31 | - run: yarn install && npx vite build --base=/admin/ # 3. 安装依赖并打包构建 32 | 33 | - name: Deploy to Server (ALI SAS) 🚀 # 4. 部署到阿里轻量级服务器上 34 | uses: easingthemes/ssh-deploy@v2.2.11 # https://github.com/easingthemes/ssh-deploy 35 | with: 36 | SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }} 37 | REMOTE_USER: root 38 | REMOTE_HOST: 47.100.95.40 39 | SOURCE: ./dist/ 40 | TARGET: /www/html/admin/ 41 | 42 | - run: npx vite build --base=/vue-lite-admin/ # 5. 重新构建一份打包文件 指定base为 43 | 44 | - name: Deploy to Github page 🚀 # 6. 部署到github pages上 45 | uses: JamesIves/github-pages-deploy-action@v4.2.3 46 | with: 47 | branch: gh-pages 48 | folder: dist 49 | single-commit: true 50 | 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .DS_Store 4 | dist 5 | dist-ssr 6 | *.local 7 | HELP.md 8 | **/target/ 9 | **/dist/ 10 | /dist 11 | /target 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### VS Code ### 20 | .vscode/ 21 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn run lint:css 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | public 3 | **/assets 4 | node_modules 5 | 6 | **/*.svg 7 | yarn.lock 8 | index.html 9 | 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.3 (2022-02-27) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * 补全commitlint.config.js的注释说明 ([c439107](https://github.com/someGenki/vue-lite-admin/commit/c4391075e04398cb568f8e248176c05ce3d7fafe)) 7 | * 局部刷新页面导致icons页面el图标丢失 ([4a84825](https://github.com/someGenki/vue-lite-admin/commit/4a84825f4247111d1ade5dc097bfbc3b772bc16a)) 8 | * 修复登录页验证码图标无法正常显示 ([4d54dc5](https://github.com/someGenki/vue-lite-admin/commit/4d54dc59e01115511c3d8c6f11b52e69545fa177)) 9 | * 修复头部下拉菜单显示异常和编译时间格式化 ([7be2b0a](https://github.com/someGenki/vue-lite-admin/commit/7be2b0a7831f2861b91f548596e5586022f83db7)) 10 | * 修复ci部署到github pages时,base路径错误 ([d959572](https://github.com/someGenki/vue-lite-admin/commit/d9595722c6d980528d730a0370a32fb1cb540a25)) 11 | * 修复getSetting方法读取null值导致异常属性 ([514d303](https://github.com/someGenki/vue-lite-admin/commit/514d3034ed88e3db1fe5ae9689478068e7010255)) 12 | * dashbord card no gutter ([f4f2c6b](https://github.com/someGenki/vue-lite-admin/commit/f4f2c6b064e32e7dc046b23ec600d0872c2568d6)) 13 | * fix some bug and adjust refresh-btn style ([7e5eb75](https://github.com/someGenki/vue-lite-admin/commit/7e5eb759af9c69ce682853a9b5de46d2edb0b8e8)) 14 | * fix typing error ([85e51a5](https://github.com/someGenki/vue-lite-admin/commit/85e51a53255ad136c3179a69de6595633e939127)) 15 | * **profile:** profile page div collapse ([265806f](https://github.com/someGenki/vue-lite-admin/commit/265806f688fb669e7609fa87d31980409bdae034)) 16 | * store/layout上边距同步scss变量 ([394f19e](https://github.com/someGenki/vue-lite-admin/commit/394f19e9e6d62bc80636bf49969c6a788ca7053d)) 17 | 18 | 19 | ### Features 20 | 21 | * 便捷导航组件 ([f294408](https://github.com/someGenki/vue-lite-admin/commit/f294408c08fad8673d9a958005e627a82f2f05c7)) 22 | * 使用pinia代替以前的store ([1f235ae](https://github.com/someGenki/vue-lite-admin/commit/1f235ae068788729bb35e6589de0ce87a42fab61)) 23 | * 添加了一些无用的特性,同时修复layout的八阿哥 ([6faef2b](https://github.com/someGenki/vue-lite-admin/commit/6faef2b41517ef2a97d952a2a2e1ec647cb10a55)) 24 | * 添加水印功能 ([84c1b21](https://github.com/someGenki/vue-lite-admin/commit/84c1b214582adb5321762b225631eb385a81c14e)) 25 | * 添加commitlint规范git commit ([2ae709b](https://github.com/someGenki/vue-lite-admin/commit/2ae709be9ed22aa6025eca109adc75962c7b6a6a)) 26 | * 添加github action 工作流功能 ([fa82c80](https://github.com/someGenki/vue-lite-admin/commit/fa82c8026ebbd0d74219df1237b4f767793f4013)) 27 | * 添加husky在git执行操作时触发对应钩子 ([a8d048a](https://github.com/someGenki/vue-lite-admin/commit/a8d048a9b190add0b9fbd9d02972deb2c62edce2)) 28 | * 添加Todo组件,同时准备编写快捷导航组件 ([7c3ab70](https://github.com/someGenki/vue-lite-admin/commit/7c3ab70f61b1a768fbbe991d62c8881078bb565c)) 29 | * 添加v-role指令 ([6b79a45](https://github.com/someGenki/vue-lite-admin/commit/6b79a45b08d3f79470642ac886be586e3834fefc)) 30 | * 新增权限功能示例页面 ([b462a1b](https://github.com/someGenki/vue-lite-admin/commit/b462a1b1570f153972c6f5777e02ac0e503a4632)) 31 | * 在设置面板更改主题色 ([f0966dd](https://github.com/someGenki/vue-lite-admin/commit/f0966dd7f6976cd82fc61fdc10af4f2b8965cfeb)) 32 | * add md-editor demo ([8e75fc7](https://github.com/someGenki/vue-lite-admin/commit/8e75fc7bf26bba344ab2351856bbab9a40ef8e44)) 33 | * add useRefresh hook ([2cba00a](https://github.com/someGenki/vue-lite-admin/commit/2cba00ab10f0d6bbc4fe65d2d9c0b3e5653b6004)) 34 | * changelog自动化生成工具 ([7120345](https://github.com/someGenki/vue-lite-admin/commit/712034594d777751896fcd5d2a18a83bb29568a2)) 35 | 36 | 37 | ### Performance Improvements 38 | 39 | * 优化登录页面 ([e326b04](https://github.com/someGenki/vue-lite-admin/commit/e326b0459fdc9101104d3f28e88906f25ffca7bc)) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 禾几元 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 | # Vuejs3+Vite2+ElementPlus后台管理系统模板 2 | 3 | ## 简介 4 | 5 | 一个免费开源的后台管理系统模板。使用最新的主流技术开发,开箱即用(主要还是用于学习参考!),主要向以下两个高star的后台管理系统模板进行学习,并根据需求进行取舍和优化改进。 6 | 7 | - [vue-vben-admin](https://github.com/anncwb/vue-vben-admin) 使用了最新的`vue3` 8 | ,`vite2`,`TypeScript`,`antdv`等主流技术开发 (代码量非常庞大和复杂...) 9 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 10 | 是一个后台前端解决方案,它基于`vuu2` 和 `element ui` (作者还没开发出vue3版本) 11 | 12 | 没有TypeScript,没有Vuex,不支持IE11 13 | 14 | 15 | 16 | 目前大四,会的技术和开发经验也不多,项目刚刚起步,非常欢迎提出意见~:heart: 17 | 18 | **在线预览地址**: http://fanyibar.top/admin/index.html 👈戳它戳他 19 | 20 | 其他地址:https://somegenki.github.io/vue-lite-admin/ 21 | 22 | ## 技术 23 | 24 | (列出来方便写报告呢!) 25 | 26 | - [Vue.js 3](https://v3.cn.vuejs.org/) : 一套用于构建用户界面的**渐进式框架** 27 | - [Vite 2](https://cn.vitejs.dev/) :基于`ESM` 的新型前端构建工具,能够显著提升前端开发体验 28 | - [Vue Router 4](https://next.router.vuejs.org/zh/) :Vue.js 的官方路由。它与 Vue.js 29 | 核心深度集成 30 | - [Pinia](https://pinia.esm.dev/) :状态管理库,Vuex的替代者 (已成为官方项目) 31 | - [Element Plus](https://element-plus.gitee.io/) :基于 Vue 3.0 的桌面端组件库 32 | - [axios](https://echarts.apache.org/zh/index.html) :基于`promise`的HTTP请求库 33 | - [echarts](https://axios-http.com/zh/) :基于 JavaScript 的开源可视化图表库 34 | - [mockjs](http://mockjs.com/) :生成随机数据,拦截 Ajax 请求 35 | - [SCSS](https://www.sass.hk/docs/) :动态样式语言,是强化CSS的辅助工具 36 | - [prettier](https://prettier.io/) :可配置化的代码格式化工具,支持多种语言 37 | - [stylelint](https://stylelint.io/) : CSS代码检查规范工具 38 | - [commitlint](https://commitlint.js.org/) : 帮助团队遵守commit约定 39 | 40 | ## 特性 41 | 42 | - 使最新的前端主流技术栈进行开发 43 | - **没有TypeScript** 让代码更加轻量级也便于快速上手 (对于初学者,代码多了难看下去) 44 | - **没有Vuex** 这个用起来是真的麻烦!在vue3中更没必要加入它(个人看法) 45 | - **详细的代码注释** 注释多多益善,有总比没有好(个人看法) 46 | - **少依赖** 能减少依赖项就尽量减少,能自己实现就自己实现,依赖多了安装都可能出问题 47 | - 常用组件 组件源码内自带详细的使用案例 48 | - 花里胡哨,但又没那么花里胡哨 49 | - Github Action 自动部署 50 | - 代码规范以及commit消息规范 51 | - 自动生成CHANGELOG.md (🐞) 52 | - SVG Sprites 插件 53 | - Mock数据 54 | - 权限功能 55 | - 快捷导航 56 | 57 | ## 功能 58 | 59 | ### 按钮权限、页面权限、组件权限等 60 | 61 | 1. 使用v-if+自定义过滤函数 62 | 2. 使用v-auth 就是对上面方式的封装 63 | 3. 定义权限组件,有权限才展示 64 | 65 | ### 待加入 -2022.2.8 66 | 67 | - [ ] 核心-路由重置 68 | - [ ] 组件-卡片悬浮遮罩效果 69 | - [ ] 组件-图片预览 70 | - [ ] 功能-Loading 71 | - [ ] 功能-搜索菜单 72 | - [ ] 案例-表格合并示例 73 | - [ ] 功能-tabbar-item固定 74 | - [ ] 页面-多功能表单组件封装 75 | - [ ] 功能-更改动态菜单生成方式 76 | - [ ] 优化-图片懒加载 https://juejin.cn/post/7004460061984555021 77 | 78 | 79 | 80 | ## 启动项目 81 | 82 | - 需要node和git 83 | 84 | - 获取项目代码 85 | 86 | ````sh 87 | git clone https://github.com/someGenki/vue-lite-admin.git 88 | # 对于上不了github的用户可以使用fastgit 89 | git clone https://hub.fastgit.org/someGenki/vue-lite-admin.git 90 | ```` 91 | - 添加上游仓库 92 | ````sh 93 | git remote add upstream https://github.com/someGenki/vue-lite-admin.git 94 | ```` 95 | - Fetch 上游仓库的新的提交并merge更变 96 | ````sh 97 | git fetch upstream 98 | git checkout main 99 | git merge upstream/main 100 | ```` 101 | 102 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // git commit 提交检查 git commit -m 'feat(login): remember user name' 2 | // 如果使用git commit提交完,发现有东西还要该,且还未push。可以使用 git commit --amend -a 进行附加 3 | // 校验最新一条commit-msg是否正确 npx commitlint --from HEAD~1 --to HEAD --verbose 4 | // 如果要撤回一次commit,且原来的代码保留 git reset --soft HEAD^ 5 | 6 | // https://github.com/vuejs/core/blob/main/scripts/verifyCommit.js 7 | // "gitHooks": { 8 | // "pre-commit": "lint-staged", 9 | // "commit-msg": "node scripts/verifyCommit.js" 10 | // }, 11 | module.exports = { 12 | extends: ['@commitlint/config-conventional'], 13 | rules: { 14 | 'body-leading-blank': [2, 'always'], 15 | 'footer-leading-blank': [1, 'always'], 16 | 'header-max-length': [2, 'always', 108], 17 | 'subject-empty': [2, 'never'], 18 | 'type-empty': [2, 'never'], 19 | 'subject-case': [0], 20 | 'type-enum': [ 21 | 2, // 报错级别 可选 0 ,1 ,2 0为disable,1为warning,2为error 22 | 'always', // 应用与否 可选always|never 23 | [ 24 | 'feat', // 新增特性 25 | 'fix', // 修补bug 26 | 'perf', // 优化相关,比如提升性能或者使用体验 27 | 'style', // 代码格式优化,如空格,缩进,逗号等 28 | 'docs', // 文档变动 29 | 'test', // 增加测试 30 | 'refactor', // 代码重构 31 | 'build', // 编译相关的修改,例如发布版本、对项目构建或者依赖的改动 32 | 'ci', // 编译相关的修改,例如发布版本、对项目构建或者依赖的改动 33 | 'chore', // 改变构建流程、或者增加依赖库、工具等 34 | 'revert', // 回滚到上个版本 35 | 'update', // 更新某个功能 文件 36 | 'wip', // Work In Process 表示代码还在开发,暂时不能合入 37 | 'types', // typescript类型添加、变动 38 | 'release', // 发布新的版本 39 | ], 40 | ], 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /deploy/auto-upload.js: -------------------------------------------------------------------------------- 1 | const [user, ip, path] = ['root', '47.100.95.40', '/www/html/admin'] 2 | // 需要win10,配置密钥可以免输入密码并快速部署打包好的前端项目到指定服务器中 3 | try { 4 | require('child_process').execSync(`scp -r ./dist/* ${user}@${ip}:${path}`) 5 | console.log('upload success!') 6 | } catch (err) { 7 | console.log(err) 8 | } 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /mock/_mockProdServer.js: -------------------------------------------------------------------------------- 1 | // 用于生产环境中也启用mock以及mock文件夹下的模块 2 | // 逐一导入mock.js文件,并加入createProdMockServer的参数中 3 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' 4 | import data from './data' 5 | 6 | export function setupProdMockServer() { 7 | createProdMockServer([...data]) 8 | } 9 | -------------------------------------------------------------------------------- /mock/_util.js: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' 2 | 3 | const Code_Enum = { 4 | SUCC: 200, 5 | FAIL: 1000, 6 | } 7 | 8 | const Msg_Enum = { 9 | SUCC: '操作成功', 10 | FAIL: '操作失败', 11 | } 12 | 13 | /** 14 | * 返回结果标准化包裹类 15 | * @example Result.succ({name:'jojo'}) 16 | */ 17 | export class Result { 18 | static succ(data) { 19 | return { code: Code_Enum.SUCC, msg: Msg_Enum.SUCC, data } 20 | } 21 | 22 | static fail(data, code = Code_Enum.FAIL, msg = Msg_Enum.FAIL) { 23 | return { data, code, msg } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mock/data.js: -------------------------------------------------------------------------------- 1 | // 用法文档 https://github.com/anncwb/vite-plugin-mock/blob/HEAD/README.zh_CN.md 2 | import { Result } from './_util' 3 | 4 | export default [ 5 | { 6 | url: '/api/test01', 7 | method: 'get', 8 | response: ({ query }) => { 9 | return { code: 0, data: { name: 'jojo1', query } } 10 | }, 11 | }, 12 | { 13 | url: '/api/user/login', 14 | method: 'post', 15 | response: Result.succ({ 16 | name: 'jojo', 17 | token: 'THIS_IS_TOKEN', 18 | roles: ['admin'], 19 | }), 20 | }, 21 | { 22 | url: '/api/user/info', 23 | method: 'get', 24 | response: Result.succ({ 25 | name: 'jojo', 26 | token: 'THIS_IS_TOKEN', 27 | roles: ['admin'], 28 | }), 29 | }, 30 | { 31 | url: '/api/demo/download1', // 文件流下载 dev模式下可以地址栏直接访问 32 | method: 'get', 33 | rawResponse: async (req, res) => { 34 | // 没什么意义的延迟半秒才返回数据 35 | await new Promise((resolve) => setTimeout(() => resolve(), 500)) 36 | // 告知客户端资源的类型 octet-stream为未知 37 | res.setHeader('Content-Type', 'application/octet-stream;charset=utf-8') 38 | // attachment 表示以附件方式下载 inline 表示在线打开 jojo.txt是客户端保存的默认文件名 39 | res.setHeader('Content-Disposition', 'attachment;filename=jojo.txt') 40 | // 写入文件数据 41 | res.write('TEXT') 42 | // 记得flush前端才能接收到 43 | res.end() 44 | }, 45 | }, 46 | ] 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-lite-admin", 3 | "version": "0.4.1", 4 | "private": true, 5 | "author": "禾几元", 6 | "packageManager": "yarn@1.0.0", 7 | "homepage": "http://fanyibar.top/admin", 8 | "scripts": { 9 | "dev": "vite", 10 | "build": "vite build", 11 | "serve": "vite preview", 12 | "prepare": "husky install", 13 | "preinstall": "node ./scripts/preinstall.js", 14 | "upload": "node ./deploy/auto-upload.js", 15 | "lint:css": "stylelint **/*.{vue,css,sass,scss} --fix", 16 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 17 | "changelog:init": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 18 | }, 19 | "dependencies": { 20 | "@element-plus/icons-vue": "^2.0.6", 21 | "@vueuse/core": "^9.0.2", 22 | "axios": "^0.27.2", 23 | "dayjs": "^1.11.4", 24 | "echarts": "^5.3.3", 25 | "element-plus": "^2.2.11", 26 | "js-cookie": "^3.0.1", 27 | "md-editor-v3": "^2.2.1", 28 | "mockjs": "^1.1.0", 29 | "pinia": "^2.0.17", 30 | "vue": "^3.2.37", 31 | "vue-router": "^4.1.3" 32 | }, 33 | "devDependencies": { 34 | "@commitlint/cli": "^17.0.3", 35 | "@commitlint/config-conventional": "^17.0.3", 36 | "@vitejs/plugin-vue": "^3.0.1", 37 | "@vitejs/plugin-vue-jsx": "^2.0.0", 38 | "@vue/compiler-sfc": "^3.2.37", 39 | "conventional-changelog-cli": "^2.2.2", 40 | "cross-env": "^7.0.3", 41 | "husky": "^8.0.1", 42 | "postcss-html": "^1.5.0", 43 | "prettier": "^2.7.1", 44 | "sass": "^1.54.0", 45 | "stylelint": "^14.9.1", 46 | "stylelint-config-prettier": "^9.0.3", 47 | "stylelint-config-recess-order": "^3.0.0", 48 | "stylelint-config-recommended-scss": "^7.0.0", 49 | "stylelint-config-recommended-vue": "^1.4.0", 50 | "vite": "3.0.4", 51 | "vite-plugin-mock": "^2.9.6" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/someGenki/vue-lite-admin" 56 | }, 57 | "engines": { 58 | "node": ">= 14.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // prettier前端代码格式化工具,需要配合插件 [官网] https://prettier.io/docs/en/index.html 2 | // 使用命令格式化所有文件 npx prettier --write . 3 | module.exports = { 4 | tabWidth: 2, // 缩进宽度 5 | printWidth: 80, // 换行宽度 6 | semi: false, // 代码结尾不加分号 7 | useTabs: false, // 缩进不使用制表符 8 | singleQuote: true, // 使用单引号包裹字符串 9 | bracketSpacing: true, // true: { foo: bar } | false: {foo: bar} 10 | trailingComma: 'es5', // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号) 11 | arrowParens: 'always', // 单个箭头的函数参数是否加() 12 | proseWrap: 'never', // 不要换行 13 | endOfLine: 'auto', // 结尾换行符 14 | vueIndentScriptAndStyle: false, //.vue文件中不缩进script和style标签内容 15 | htmlWhitespaceSensitivity: 'strict', // html文件中的全局空白区域敏感度:空格被认为是敏感的。 16 | } 17 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/someGenki/vue-lite-admin/b130f6f49740ea76cd93aaede3860fb5b1a57f27/public/favicon.ico -------------------------------------------------------------------------------- /scripts/preinstall.js: -------------------------------------------------------------------------------- 1 | // npm|yarn|pnpm|cnpm install 会先自动执行 package.json 中的 preinstall钩子(如果有) 2 | // 可以用来限定使用的包管理器类型 3 | if (!/yarn/.test(process.env.npm_execpath || '')) { 4 | console.warn( 5 | `\u001b[33m This project requires using yarn as the package manager ` + 6 | ` for scripts to work properly. \u001b[39m\n` 7 | ) 8 | process.exit(1) 9 | } -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '/src/utils/request' 2 | 3 | export function login(data) { 4 | return request.post('/user/login', data) 5 | } 6 | export function getInfo(data) { 7 | return request.get('/user/info', data) 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/dio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/someGenki/vue-lite-admin/b130f6f49740ea76cd93aaede3860fb5b1a57f27/src/assets/dio.jpg -------------------------------------------------------------------------------- /src/assets/element-logo.svg: -------------------------------------------------------------------------------- 1 | element plus-logo-small 副本 2 | -------------------------------------------------------------------------------- /src/assets/logo2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/someGenki/vue-lite-admin/b130f6f49740ea76cd93aaede3860fb5b1a57f27/src/assets/logo2.ico -------------------------------------------------------------------------------- /src/assets/vlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/someGenki/vue-lite-admin/b130f6f49740ea76cd93aaede3860fb5b1a57f27/src/assets/vlogo.png -------------------------------------------------------------------------------- /src/components/AppEmotion/EmotionBox.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | 46 | -------------------------------------------------------------------------------- /src/components/AppEmotion/EmotionTab.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | 61 | -------------------------------------------------------------------------------- /src/components/AppEmotion/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 65 | 66 | 136 | -------------------------------------------------------------------------------- /src/components/AppExplain/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 33 | -------------------------------------------------------------------------------- /src/components/AppIcon/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 49 | 50 | 66 | -------------------------------------------------------------------------------- /src/components/AppLink/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | 40 | 54 | -------------------------------------------------------------------------------- /src/components/CompareBox/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | 35 | 66 | -------------------------------------------------------------------------------- /src/components/EasyNav/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | 40 | 90 | -------------------------------------------------------------------------------- /src/components/EmotionBox/emotion-list.js: -------------------------------------------------------------------------------- 1 | export default { 2 | weChatList: [ 3 | '微笑', 4 | '撇嘴', 5 | '色', 6 | '发呆', 7 | '得意', 8 | '流泪', 9 | '害羞', 10 | '闭嘴', 11 | '睡', 12 | '大哭', 13 | '尴尬', 14 | '发怒', 15 | '调皮', 16 | '呲牙', 17 | '惊讶', 18 | '难过', 19 | '酷', 20 | '冷汗', 21 | '抓狂', 22 | '吐', 23 | '偷笑', 24 | '可爱', 25 | '白眼', 26 | '傲慢', 27 | '饥饿', 28 | '困', 29 | '惊恐', 30 | '流汗', 31 | '憨笑', 32 | '大兵', 33 | '奋斗', 34 | '咒骂', 35 | '疑问', 36 | '嘘', 37 | '晕', 38 | '折磨', 39 | '衰', 40 | '骷髅', 41 | '敲打', 42 | '再见', 43 | '擦汗', 44 | '抠鼻', 45 | '鼓掌', 46 | '糗大了', 47 | '坏笑', 48 | '左哼哼', 49 | '右哼哼', 50 | '哈欠', 51 | '鄙视', 52 | '委屈', 53 | '快哭了', 54 | '阴险', 55 | '亲亲', 56 | '吓', 57 | '可怜', 58 | '菜刀', 59 | '西瓜', 60 | '啤酒', 61 | '篮球', 62 | '乒乓', 63 | '咖啡', 64 | '饭', 65 | '猪头', 66 | '玫瑰', 67 | '凋谢', 68 | '示爱', 69 | '爱心', 70 | '心碎', 71 | '蛋糕', 72 | '闪电', 73 | '炸弹', 74 | '刀', 75 | '足球', 76 | '瓢虫', 77 | '便便', 78 | '月亮', 79 | '太阳', 80 | '礼物', 81 | '拥抱', 82 | '强', 83 | '弱', 84 | '握手', 85 | '胜利', 86 | '抱拳', 87 | '勾引', 88 | '拳头', 89 | '差劲', 90 | '爱你', 91 | '不', 92 | '好', 93 | '爱情', 94 | '飞吻', 95 | '跳跳', 96 | '发抖', 97 | '怄火', 98 | '转圈', 99 | '磕头', 100 | '回头', 101 | '跳绳', 102 | '挥手', 103 | '激动', 104 | '街舞', 105 | '献吻', 106 | '左太极', 107 | ], 108 | kaomojiList: [ 109 | '(=。=)', 110 | '(ーー゛)', 111 | '(⊙﹏⊙)', 112 | '(ノへ ̄、)', 113 | '(#`O′)', 114 | '(°ー°〃)', 115 | '(ー`´ー)', 116 | '(#`O′)', 117 | '(@_@;)', 118 | 'w(゚Д゚)w', 119 | '( ̄_, ̄ )', 120 | '凸(艹皿艹 )', 121 | '(* ̄rǒ ̄)', 122 | '︿( ̄︶ ̄)︿', 123 | '♪(^∇^*)', 124 | 'o(≧口≦)o', 125 | '(o_ _)ノ', 126 | 'ヽ(✿゚▽゚)ノ', 127 | 'φ(≧ω≦*)♪', 128 | '(u‿ฺu✿ฺ)', 129 | '(○` 3′○)', 130 | '○| ̄|_ =3', 131 | 'o( ̄ヘ ̄o#)', 132 | 'o(一︿一+)o', 133 | ], 134 | } 135 | -------------------------------------------------------------------------------- /src/components/EmotionBox/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 62 | 63 | 130 | -------------------------------------------------------------------------------- /src/components/EmotionBox/useEmotions.js: -------------------------------------------------------------------------------- 1 | import emotionList from './emotion-list' 2 | 3 | // 将原生表情文本变成img表情 [呲牙] => 4 | export function processEmotionText(str) { 5 | return str.replace(/\[[\u4E00-\u9FA5]{1,3}\]/gi, (words) => { 6 | let word = words.replace(/\[|\]/gi, '') 7 | let index = emotionList.weChatList.indexOf(word) 8 | return index !== -1 9 | ? `` 10 | : words 11 | }) 12 | } 13 | 14 | export function useEmotions(type) { 15 | if (type === 'wechat') { 16 | return emotionList.weChatList.map((item, index) => { 17 | return { 18 | name: `[${item}]`, 19 | url: ``, 20 | } 21 | }) 22 | } else if (type === 'kaomoji') { 23 | return emotionList.kaomojiList 24 | } else return null 25 | } 26 | -------------------------------------------------------------------------------- /src/components/SilkRibbon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 188 | -------------------------------------------------------------------------------- /src/components/Todo/TodoAdd.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 38 | 39 | 83 | -------------------------------------------------------------------------------- /src/components/Todo/TodoFilter.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 30 | 31 | 52 | -------------------------------------------------------------------------------- /src/components/Todo/TodoList.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /src/components/Todo/TodoListItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 27 | 28 | 80 | -------------------------------------------------------------------------------- /src/components/Todo/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | 57 | 74 | -------------------------------------------------------------------------------- /src/directives/click-outside.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mounted(el, binding) { 3 | function documentHandler(e) { 4 | if (el.style.display === 'none' || el.contains(e.target)) return 5 | else binding.value() 6 | } 7 | el.__vueClickOutsie__ = documentHandler 8 | document.addEventListener('click', documentHandler) 9 | }, 10 | unmounted(el) { 11 | document.removeEventListener('click', el.__vueClickOutsie__) 12 | delete el.__vueClickOutsie__ 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/directives/default-img.js: -------------------------------------------------------------------------------- 1 | import defaultAvatar from '/src/assets/images/dio.jpg' 2 | import defaultBackground from '/src/assets/images/cute.jpg' 3 | import { getStrColor } from '../utils/process' 4 | 5 | /** 6 | * 项目难点标记:检测图片存在,如果不存在则用用户的昵称的首字符作为头像,绘制svg 7 | * 检测图片是否存在 8 | * @param url 9 | */ 10 | const imageIsExist = function (url) { 11 | return new Promise((resolve) => { 12 | let img = new Image() 13 | img.src = url 14 | img.onload = () => { 15 | if (img.complete === true) { 16 | resolve(true) 17 | img = null 18 | } 19 | } 20 | img.onerror = () => { 21 | resolve(false) 22 | img = null 23 | } 24 | }) 25 | } 26 | 27 | function genSvgImg(text, color, size = 36) { 28 | text = text.substring(0, 1) 29 | color = encodeURIComponent(color) 30 | return `data:image/svg+xml;utf8, 31 | 32 | 33 | ${text} 34 | ` 35 | } 36 | 37 | /** 38 | * 当图片加载失败时,显示默认图片 39 | * 参数可选:'avatar' | 'background' | string 40 | * 41 | */ 42 | export default async function defaultImg(el, binding) { 43 | // 需要显示默认图片(当图片原本的src属性有错时)的类型 44 | const { value, modifiers } = binding 45 | // 图片原本的src 46 | const realURL = el.src 47 | // 当原本图片不存在时,根据参数返回不同的图片url 48 | const exist = await imageIsExist(realURL) 49 | if (exist) return // 图片可正常加载,不做任何处理 50 | if (value) { 51 | el.setAttribute('src', genSvgImg(value, getStrColor(value))) 52 | } else if (modifiers.avatar) { 53 | el.setAttribute('src', defaultAvatar) 54 | } else if (modifiers.background) { 55 | el.setAttribute('src', defaultBackground) 56 | } else { 57 | el.remove() // 什么都不加 v-default-img 时 移除图片 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/directives/drag.js: -------------------------------------------------------------------------------- 1 | /**把一个组件变成可拖拽的组件 前提:position: absolute; 不用。。 */ 2 | function drag(el, binding) { 3 | el.onmousedown = (e) => { 4 | let disx = e.pageX - el.offsetLeft 5 | let disy = e.pageY - el.offsetTop 6 | document.onmousemove = (e) => { 7 | moveElement(el, e, binding.value, disx, disy) 8 | } 9 | document.onmouseup = () => 10 | (document.onmousemove = document.onmouseup = null) 11 | } 12 | el.ontouchstart = (e) => { 13 | const t = e.changedTouches[0] 14 | let disx = t.pageX - el.offsetLeft 15 | let disy = t.pageY - el.offsetTop 16 | document.ontouchmove = (e) => { 17 | e.stopPropagation() 18 | const t = e.changedTouches[0] 19 | moveElement(el, t, binding.value, disx, disy) 20 | } 21 | document.ontouchend = () => 22 | (document.ontouchmove = document.ontouchend = null) 23 | } 24 | } 25 | 26 | function moveElement(el, event, direction, x, y) { 27 | if (direction === 'vertical') { 28 | el.style.top = event.pageY - y + 'px' 29 | } else if (direction === 'horizontal') { 30 | el.style.left = event.pageX - x + 'px' 31 | } else { 32 | el.style.left = event.pageX - x + 'px' 33 | el.style.top = event.pageY - y + 'px' 34 | } 35 | } 36 | 37 | export default drag 38 | -------------------------------------------------------------------------------- /src/directives/role.js: -------------------------------------------------------------------------------- 1 | // 自定义指令 https://v3.cn.vuejs.org/guide/custom-directive.html 2 | // 其他使用指令 https://juejin.cn/post/7067051410671534116 3 | // 图片懒加载指令文章 https://mp.weixin.qq.com/s/OO7jVd2kIlkRtNNaRjGLuQ 4 | // v-role="'ADMIN'" | v-role="['ADMIN','TEST']" 5 | import { useUserStore } from '/src/store/user' 6 | 7 | const userStore = useUserStore() 8 | const concat = Array.prototype.concat.bind([]) 9 | 10 | const roleDirective = (el, binding) => { 11 | const { value, modifiers } = binding 12 | if (!value) return 13 | // 字母全大写比较,不满足权限则隐藏或者添加禁用样式 14 | if (!hasPermission(value)) { 15 | if (modifiers.keep) { 16 | el.style.textDecoration = 'line-through' 17 | el.style.color = '#d0d0d0' 18 | el.style.cursor = 'not-allowed' 19 | el.style.pointerEvents = 'none' 20 | } else { 21 | el.remove() 22 | } 23 | } 24 | } 25 | 26 | /* 27 | const roleDirective1 = { 28 | mounted: directiveHook, 29 | updated: directiveHook, 30 | } 31 | */ 32 | 33 | /** 34 | * 35 | * @param value string | string[] 36 | * @param defVal 默认值 37 | * @returns {boolean} 38 | */ 39 | function hasPermission(value, defVal = true) { 40 | if (!value) return defVal 41 | 42 | // 基于getPermCodeList ... (差不多) 43 | // ... 44 | 45 | // 基于store/user.roles[] 46 | // 判断value跟user.roles是否有交集(全大写比较) 利用concat将非数组变成数组 47 | return hasIntersection(concat(value), userStore['roles']) 48 | } 49 | 50 | // 判断数组是否有交集 51 | function hasIntersection(arr1, arr2, upper = true) { 52 | if (upper) { 53 | arr1 = arr1.map((r) => r.toUpperCase()) 54 | arr2 = arr2.map((r) => r.toUpperCase()) 55 | } 56 | return arr1.some((a1) => arr2.includes(a1)) 57 | } 58 | 59 | export default roleDirective 60 | -------------------------------------------------------------------------------- /src/hooks/emotion/emotion-list.js: -------------------------------------------------------------------------------- 1 | export default { 2 | weChatList: [ 3 | '微笑', 4 | '撇嘴', 5 | '色', 6 | '发呆', 7 | '得意', 8 | '流泪', 9 | '害羞', 10 | '闭嘴', 11 | '睡', 12 | '大哭', 13 | '尴尬', 14 | '发怒', 15 | '调皮', 16 | '呲牙', 17 | '惊讶', 18 | '难过', 19 | '酷', 20 | '冷汗', 21 | '抓狂', 22 | '吐', 23 | '偷笑', 24 | '可爱', 25 | '白眼', 26 | '傲慢', 27 | '饥饿', 28 | '困', 29 | '惊恐', 30 | '流汗', 31 | '憨笑', 32 | '大兵', 33 | '奋斗', 34 | '咒骂', 35 | '疑问', 36 | '嘘', 37 | '晕', 38 | '折磨', 39 | '衰', 40 | '骷髅', 41 | '敲打', 42 | '再见', 43 | '擦汗', 44 | '抠鼻', 45 | '鼓掌', 46 | '糗大了', 47 | '坏笑', 48 | '左哼哼', 49 | '右哼哼', 50 | '哈欠', 51 | '鄙视', 52 | '委屈', 53 | '快哭了', 54 | '阴险', 55 | '亲亲', 56 | '吓', 57 | '可怜', 58 | '菜刀', 59 | '西瓜', 60 | '啤酒', 61 | '篮球', 62 | '乒乓', 63 | '咖啡', 64 | '饭', 65 | '猪头', 66 | '玫瑰', 67 | '凋谢', 68 | '示爱', 69 | '爱心', 70 | '心碎', 71 | '蛋糕', 72 | '闪电', 73 | '炸弹', 74 | '刀', 75 | '足球', 76 | '瓢虫', 77 | '便便', 78 | '月亮', 79 | '太阳', 80 | '礼物', 81 | '拥抱', 82 | '强', 83 | '弱', 84 | '握手', 85 | '胜利', 86 | '抱拳', 87 | '勾引', 88 | '拳头', 89 | '差劲', 90 | '爱你', 91 | '不', 92 | '好', 93 | '爱情', 94 | '飞吻', 95 | '跳跳', 96 | '发抖', 97 | '怄火', 98 | '转圈', 99 | '磕头', 100 | '回头', 101 | '跳绳', 102 | '挥手', 103 | '激动', 104 | '街舞', 105 | '献吻', 106 | '左太极', 107 | ], 108 | kaomojiList: [ 109 | '(′Д`)', 110 | '(=。=)', 111 | '(ーー゛)', 112 | '(⊙﹏⊙)', 113 | '(ノへ ̄、)', 114 | '(°ー°〃)', 115 | '(ー`´ー)', 116 | '(#`O′)', 117 | '(@_@;)', 118 | 'w(゚Д゚)w', 119 | '( ̄_, ̄ )', 120 | '凸(艹皿艹 )', 121 | '(* ̄rǒ ̄)', 122 | '︿( ̄︶ ̄)︿', 123 | '♪(^∇^*)', 124 | 'o(≧口≦)o', 125 | '(o_ _)ノ', 126 | 'ヽ(✿゚▽゚)ノ', 127 | 'φ(≧ω≦*)♪', 128 | '(u‿ฺu✿ฺ)', 129 | '(○` 3′○)', 130 | '○| ̄|_ =3', 131 | 'o( ̄ヘ ̄o#)', 132 | 'o(一︿一+)o', 133 | ], 134 | emojiList: [ 135 | '🥰', 136 | '❤', 137 | '🥵', 138 | '😅', 139 | '🥺', 140 | '🤤', 141 | '🔞', 142 | '🪁', 143 | '💧', 144 | '💩', 145 | '🌚', 146 | '😂', 147 | '✅', 148 | '🚬', 149 | '🤡', 150 | '📍', 151 | '📢', 152 | '🖕', 153 | '☀', 154 | '🍎', 155 | '🙏', 156 | '🤏', 157 | '🙃', 158 | '😄', 159 | '✨', 160 | '🔥', 161 | '🖤', 162 | '🤔', 163 | '😊', 164 | '😜', 165 | '💙', 166 | '😍', 167 | '🚫', 168 | '🀄', 169 | '🙂', 170 | '😃', 171 | '🏠', 172 | '☺', 173 | '😀', 174 | '💪', 175 | '💢', 176 | '🌼', 177 | '🏳', 178 | '👋', 179 | '🌈', 180 | '➕', 181 | '🙇', 182 | '🕯', 183 | '🛰', 184 | '⭐', 185 | '😭', 186 | '💤', 187 | '⚠', 188 | '💜', 189 | '🚀', 190 | '🤞', 191 | '🤟', 192 | '☁', 193 | '💸', 194 | '🐴', 195 | '😇', 196 | '😶', 197 | '🍼', 198 | '💯', 199 | '🦋', 200 | '🥇', 201 | '🤗', 202 | '🤮', 203 | '👍', 204 | '💦', 205 | '💖', 206 | '💰', 207 | '🚗', 208 | '🍑', 209 | '😡', 210 | '🎂', 211 | '🔗', 212 | '🌷', 213 | '👗', 214 | '🎲', 215 | '☹', 216 | '🌊', 217 | '🧐', 218 | '🍾', 219 | '😓', 220 | '😑', 221 | '🌸', 222 | '🍉', 223 | ], 224 | } 225 | -------------------------------------------------------------------------------- /src/hooks/emotion/useEmotions.js: -------------------------------------------------------------------------------- 1 | import emotionList from './emotion-list' 2 | 3 | const wxReg = /\[wx_([\u4E00-\u9FA5]{1,3})]/g 4 | 5 | export function processWx(str, klass = 'wx-emoji') { 6 | return str?.replace(wxReg, (_, word) => { 7 | const index = emotionList.weChatList.indexOf(word) 8 | return index !== -1 9 | ? `` 10 | : _ 11 | }) 12 | } 13 | 14 | /** 15 | * emotions中的热门😃来自 [EMOJIAll](https://www.emojiall.com/zh-hans/top-daily/zh-hans) 16 | */ 17 | const emotions = { 18 | wx: { 19 | list: emotionList.weChatList, 20 | name: '微信表情', 21 | process: (text, index) => ({ 22 | text: `[wx_${text}]`, 23 | html: ``, 24 | }), 25 | getFirst: null, 26 | }, 27 | emoji: { 28 | name: '热门😃', 29 | list: emotionList.emojiList, 30 | process: (s) => ({ text: s, html: s }), 31 | }, 32 | kaomoji: { 33 | name: '颜文字', 34 | list: emotionList.kaomojiList, 35 | process: (s) => ({ text: s, html: s }), 36 | }, 37 | } 38 | 39 | function getTabs(emotions) { 40 | return Object.keys(emotions).map((key) => ({ 41 | key: key, 42 | name: emotions[key].name, 43 | first: emotions[key].process(emotions[key].list[0], 0).html, 44 | })) 45 | } 46 | 47 | export function useEmotion() { 48 | return { processWx, emotions, emotionTabs: getTabs(emotions) } 49 | } 50 | -------------------------------------------------------------------------------- /src/hooks/usePageFn.js: -------------------------------------------------------------------------------- 1 | import router from '../router' 2 | import { useLayoutStore } from '../store/layout' 3 | 4 | const waterMarkId = 'waterMark' 5 | const observerSym = Symbol() 6 | 7 | const VM_STYLE = ` 8 | position:fixed; 9 | top:0; left:0; bottom:0; right:0; 10 | pointer-events:none; 11 | background-repeat:repeat;` 12 | 13 | const default_option = { 14 | letterSpacing: 4, 15 | width: 400, 16 | height: 100, 17 | stroke: '#000', 18 | opacity: 0.1, 19 | fontSize: 16, 20 | rotate: -20, 21 | } 22 | 23 | function createWaterMarkImage(str, option) { 24 | const o = Object.assign(default_option, option) 25 | const SVGTemplate = 26 | `` + 27 | `${str} 33 | ` 34 | // 如果使用utf-代替base64,则出现 # 符号会导致内容无效 35 | return `url(data:image/svg+xml;base64,${window.btoa( 36 | unescape(encodeURIComponent(SVGTemplate)) 37 | )})` 38 | } 39 | 40 | export function usePageFn() { 41 | // 刷新当前页面 42 | function refreshPage() { 43 | const route = router.currentRoute.value 44 | useLayoutStore().removeCachedView(route) 45 | if (route.path.indexOf('/redirect') > -1) return 46 | router.replace('/redirect') 47 | } 48 | 49 | // 打印当前页面 50 | function printPage() {} 51 | 52 | // 页面全屏 53 | function fullScreen() { 54 | //判断dom元素是否全屏 没有则请求全屏 55 | if (!document.fullscreenElement) 56 | document.documentElement.requestFullscreen() 57 | else if (document.exitFullscreen) document.exitFullscreen() //退出全屏 58 | } 59 | 60 | // 移除水印 61 | function delWaterMark(container = document.body) { 62 | const elm = container.querySelector('#' + waterMarkId) 63 | if (elm) { 64 | elm[observerSym] && elm[observerSym].disconnect() 65 | elm.remove() 66 | } 67 | } 68 | 69 | // 添加水印 70 | function waterMark( 71 | str, 72 | imageOption = {}, 73 | container = document.body, 74 | protect = false 75 | ) { 76 | delWaterMark(container) 77 | const watermark = document.createElement('div') 78 | watermark.setAttribute('id', waterMarkId) 79 | watermark.setAttribute('style', VM_STYLE) 80 | watermark.style.backgroundImage = createWaterMarkImage(str, imageOption) 81 | container.appendChild(watermark) 82 | 83 | if (protect === true) { 84 | // 基于前端页面水印的保护措施,只能做到简易保护 85 | const observer = new MutationObserver(() => { 86 | if (!container.contains(watermark)) { 87 | // 这里只简单的监听下div是否被移除,没有监听属性的变化 88 | container.appendChild(watermark) 89 | } 90 | }) 91 | observer.observe(container, { 92 | attributes: true, 93 | childList: true, 94 | subtree: true, 95 | attributeFilter: ['style'], 96 | }) 97 | watermark[observerSym] = observer 98 | } 99 | } 100 | 101 | return { 102 | refreshPage, 103 | fullScreen, 104 | waterMark, 105 | delWaterMark, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/hooks/useTracking.js: -------------------------------------------------------------------------------- 1 | let hadErrorInit = false 2 | let vueApp = null 3 | const option = {} // 存放相关信息,比如要上报的url,项目名称等 4 | const errorType = ['VueError', 'Window', 'UnReject'] 5 | const errorMap = {} 6 | 7 | // 当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件 8 | function handleUnReject(event) { 9 | // const { reason } = event 10 | console.log(`UNHANDLED PROMISE REJECTION: ${event.reason}`) 11 | errorMap['UnReject'].push(event) 12 | } 13 | 14 | // 处理语法异常和运行时异常 15 | function handleWindowError(event) { 16 | // const { message, source, lineno, colno, error } = event 17 | console.log(event) 18 | errorMap['Window'].push(event) 19 | } 20 | 21 | // 处理组件渲染函数和侦听器执行期间抛出的未捕获错误 22 | function initHandleVueError(vueApp) { 23 | if (!vueApp) return 24 | const { errorHandler } = vueApp.config 25 | 26 | vueApp.config.errorHandler = (err, vm, info) => { 27 | if (typeof errorHandler === 'function') { 28 | errorHandler(err, vm, info) // 保留原有调用 29 | } 30 | console.log(err, vm, info) 31 | errorMap['VueError'].push({ err, vm, info }) 32 | } 33 | } 34 | 35 | function initError() { 36 | if (hadErrorInit) return 37 | window.addEventListener('error', handleWindowError) 38 | window.addEventListener('unhandledrejection', handleUnReject) 39 | initHandleVueError(vueApp) 40 | 41 | errorType.forEach((type) => (errorMap[type] = [])) 42 | 43 | hadErrorInit = true 44 | } 45 | 46 | export function useErrorTrack() { 47 | // 功能待补全 48 | return { errorMap } 49 | } 50 | 51 | /** 52 | * 页面埋点相关功能 待完善 53 | * 性能相关收集 54 | */ 55 | export function useTracking() { 56 | const nav = window.performance.getEntries()[0] 57 | // browser info 58 | // const start = 0 // 统计起始点 建议从nav.fetchStart开始 59 | const times = {} 60 | // dns解析时间 61 | times.dnsTime = nav.domainLookupEnd - nav.domainLookupStart 62 | // tcp建立时间 63 | times.tcpTime = nav.connectEnd - nav.connectStart 64 | // TTFB: 读取页面第一个字节的时间 65 | times.firstByteTime = nav.responseStart 66 | // 下载时间: 从接收到第一个字节的数据到最后一个字节数据的耗时(下载时间) 67 | times.downloadTime = nav.responseEnd - nav.responseStart 68 | // 白屏时间: 页面开始解析的时间,即将进入渲染环节 69 | times.blankTime = nav.domInteractive 70 | 71 | // 解析dom花费的时长: dom.readyState从interactive变成complete的耗时(X) 72 | // times.domReadyTime = nav.domContentLoadedEventEnd - nav.domInteractive 73 | 74 | // 当浏览器完成页面所有资源加载的耗时 75 | times.loadTime = nav.loadEventEnd 76 | 77 | // 用于发送一些统计和诊断,简单易用 78 | // navigator.sendBeacon("/log", data); 79 | return { times } 80 | } 81 | 82 | export default { 83 | install: (app, opt) => { 84 | vueApp = app 85 | option.url = opt?.url 86 | initError() 87 | }, 88 | } 89 | -------------------------------------------------------------------------------- /src/icons/QQ.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/rank.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/wechat.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | 30 | 56 | -------------------------------------------------------------------------------- /src/layout/components/Navbar/AvatarMenu.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /src/layout/components/Navbar/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /src/layout/components/Navbar/Hamburger.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 26 | 43 | -------------------------------------------------------------------------------- /src/layout/components/Navbar/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 76 | 77 | 113 | -------------------------------------------------------------------------------- /src/layout/components/Settings/SettingItem.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 37 | 38 | 46 | -------------------------------------------------------------------------------- /src/layout/components/Settings/index.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 96 | 97 | 144 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarLogo.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 62 | 63 | 69 | 111 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/useMenu.js: -------------------------------------------------------------------------------- 1 | // menu的格式 2 | export const menu_interface = { 3 | name: String, 4 | path: String, 5 | icon: [String, null], 6 | children: 'menu_interface', 7 | 8 | disabled: [Boolean, null], 9 | orderNo: [Number, null], 10 | roles: [String, null], 11 | meta: [Object, null], 12 | hideMenu: [Boolean, null], 13 | } 14 | 15 | // menu的创建 16 | // 1. 从useStore中的AddRoutes创建 17 | export function createMenuFromAddRoutes(routes) { 18 | const rr = [] 19 | 20 | // 方法定义:剥离被Layout包裹的路由 21 | function strippingLayoutRoute(route) { 22 | // 如果一个路由没有meta,而且只有一个子孩子,则直接返回这个孩子 23 | if (!route.meta && route.children && route.children.length === 1) { 24 | let c = route.children[0] 25 | if (c.meta && c.meta.title) { 26 | if (c.path[0] !== '/') c.path = route.path + '/' + c.path 27 | return c 28 | } 29 | } 30 | return null 31 | } 32 | 33 | // 方法定义:快速创建Menu对象从route对象 34 | function createMenuFromRoute(route) { 35 | return { 36 | title: route.meta.title, 37 | icon: route.meta.icon, 38 | path: route.path, 39 | name: route.name, 40 | } 41 | } 42 | 43 | function recursion(arr) { 44 | let result = [] 45 | for (const item of arr) { 46 | if (!item.hidden) { 47 | // 如果是外链形式的路由,拿出children 48 | if (item.path === '/ex-link') { 49 | result.push(createMenuFromRoute(item.children[0])) 50 | } 51 | // 如果有子路由则递归处理子路由 52 | else if (item.children && item.children.length > 0) { 53 | let child = recursion(item.children) 54 | let obj = createMenuFromRoute(item) 55 | if (child.length > 0) obj.children = child 56 | result.push(obj) 57 | } else { 58 | result.push(createMenuFromRoute(item)) 59 | } 60 | } 61 | } 62 | return result 63 | } 64 | 65 | // 第一层路由处理 66 | for (const item of routes) { 67 | if (!item.hidden) { 68 | if (item.path === '/ex-link') { 69 | rr.push(item.children[0]) 70 | continue 71 | } 72 | let r = strippingLayoutRoute(item) 73 | if (r) { 74 | rr.push(r) 75 | continue 76 | } 77 | if (!r && item.meta && item.meta.title) rr.push(item) 78 | } 79 | } 80 | 81 | return recursion(rr) 82 | } 83 | 84 | // 2. T0DO 从后台返回的数据创建 85 | -------------------------------------------------------------------------------- /src/layout/components/TabBar/TarBarItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | 26 | 70 | -------------------------------------------------------------------------------- /src/layout/components/TabBar/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 44 | 108 | -------------------------------------------------------------------------------- /src/layout/components/TabBar/useTabBar.js: -------------------------------------------------------------------------------- 1 | import { toRefs } from 'vue' 2 | import router from '/src/router' 3 | import { useLayoutStore } from '/src/store/layout' 4 | 5 | export default function useTabBar() { 6 | const { visitedViews } = toRefs(useLayoutStore()) 7 | 8 | /** 9 | * 根据操作来删除符合的tab项 10 | * @param tabItem 被选中要操作tab项, 11 | * @param operate 要进行的操作 12 | */ 13 | function delTabBarItem(tabItem, operate = 'self') { 14 | const arr = visitedViews.value 15 | for (let i = 0, len = arr.length; i < len; i++) { 16 | const item = arr[i] 17 | if (item.timeStamp === tabItem.timeStamp) { 18 | switch (operate) { 19 | case 'self': 20 | arr.splice(i, 1) 21 | // 删除tab item是当前路由时,尝试跳转到附近的tab item 22 | if (router.currentRoute.value.fullPath === item.fullPath) { 23 | const nearTab = arr[i - 1] || arr[i] 24 | nearTab ? router.push(nearTab.fullPath) : router.push('/') 25 | } 26 | break 27 | case 'all': 28 | arr.splice(0, len) 29 | router.push('/') 30 | break 31 | case 'left': 32 | arr.splice(0, i) 33 | break 34 | case 'other': 35 | arr.splice(0, arr.length, item) 36 | break 37 | case 'right': 38 | arr.splice(i + 1, len) 39 | break 40 | default: 41 | break 42 | } 43 | break 44 | } 45 | } 46 | } 47 | 48 | return { visitedViews, delTabBarItem } 49 | } 50 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Settings } from './Settings/index.vue' 2 | export { default as Sidebar } from './Sidebar/index.vue' 3 | export { default as AppMain } from './AppMain/index.vue' 4 | export { default as TabBar } from './TabBar/index.vue' 5 | export { default as NavBar } from './Navbar/index.vue' 6 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 66 | 67 | 93 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue' 2 | import { RouterView } from 'vue-router' 3 | import store from './store' 4 | import router from './router' 5 | 6 | // 引入Element Plus和Element icons 7 | import 'element-plus/dist/index.css' 8 | import ElementPlus from 'element-plus' 9 | import * as ElIcons from '@element-plus/icons-vue' 10 | 11 | // 引入自己的CSS、JS和Component 12 | import 'virtual:svg-sprites-create' 13 | import '/src/styles/common.scss' 14 | import tracking from '/src/hooks/useTracking' 15 | import AppIcon from '/src/components/AppIcon/index.vue' 16 | import AppLink from '/src/components/AppLink/index.vue' 17 | import AppExplain from '/src/components/AppExplain/index.vue' 18 | import { globalRegister } from './utils/compRegister' 19 | 20 | // 这里的替换掉了App.vue,因为里面暂时没啥东西,孤零零的就暂时把它放在这 addEventListener 21 | const app = createApp({ render: () => h(RouterView) }) 22 | 23 | // \\ // \\ // \\ // \\ // \\ // \\ 24 | 25 | globalRegister(app, ElIcons, { prefix: 'elIcon' }) 26 | 27 | // \\ // \\ // \\ // \\ // \\ // \\ 28 | 29 | app.use(router).use(store).use(ElementPlus).use(tracking) 30 | 31 | app.component('app-icon', AppIcon) 32 | app.component('app-link', AppLink) 33 | app.component('app-explain', AppExplain) 34 | 35 | app.mount('#app') 36 | -------------------------------------------------------------------------------- /src/plugin/createSVGSprites.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 加载指定目录下的所有.svg文件,生成一个个 标签来组成 SVG Sprites。 3 | * 利用虚拟模块机制加载一个模块文件,该模块文件包含上面所说的SVG Sprites字符串。 4 | * 并使用模块内的mount方法创建一个SVG标签并设置其内容为该字符串,并注入到DOM中。 5 | 6 | * 使用方法: 7 | * // at vite.config.js 8 | * import { createSVGSprites } from './src/plugin/createSVGSprites' 9 | * plugins: [ createSVGSprites() ] // 注册该插件 10 | * // at /src/main.js 11 | * import 'virtual:svg-symbol-create' //导入注册脚本即可加载该虚拟模块(ESM格式) 12 | * 13 | * 使用 , 14 | * 即可对已经生成的标签进行ID引用并展示 SVG 图标。 15 | * svg中的width和height属性即是SVG的画布大小,亦可使用 style="width: 36px;height: 36px;" 16 | * 17 | * 参考链接: 18 | * 关于SVG viewBox: https://blog.csdn.net/weixin_34080903/article/details/90158481 19 | * 文档:https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/symbol 20 | * Vite插件 API:https://cn.vitejs.dev/guide/api-plugin.html 21 | */ 22 | import {readFileSync, readdirSync} from 'fs' 23 | import path from 'path' 24 | 25 | const pluginName = 'rollup:svg-sprites-create' 26 | const virtualModuleId = 'virtual:svg-sprites-create' 27 | const resolvedVirtualModuleId = '\0' + virtualModuleId 28 | // 提取SVG的开始标签及其viewBox属性(viewBox属性代表可视区域) 29 | const svgTagExtract = /.*]*viewBox="([^"]*)"[^<>]*>/ 30 | // 默认配置项 31 | const defaultOption = { 32 | path: '/src/icons/', 33 | symbolIdPrefix: 'icon', 34 | separator: '-', 35 | customEleId: 'svgSpriteStats', 36 | } 37 | 38 | /** 39 | * 将对象内的属性视为模块的导出属性,生成一个字符串化的模块脚本 40 | * @example 41 | * const res = moduleStringify( {fn: function(arg){ ... }} ); 42 | * res === 'export const fn=function(){};export default {}'; // true 43 | * @param {object} module 对象中的属性视为要导出模块的属性 44 | * @param {string} runNow 声明完导出属性后,立即执行的代码段 45 | * @returns {string} 字符串形式的ESM模块脚本 46 | */ 47 | function moduleStringify(module, runNow = '') { 48 | const stringify = (key) => { 49 | const target = module[key] 50 | if (typeof target === 'function') { 51 | return `export const ${key}=${target.toString()};` 52 | } else { 53 | return `export const ${key}=${JSON.stringify(target)};` 54 | } 55 | } 56 | 57 | const code = Object.keys(module). 58 | reduce((prev, key) => prev + stringify(key), '') 59 | 60 | return `${code}\n${runNow}\n` + 'export default {}' 61 | } 62 | 63 | /** 64 | * 加载目标路径下的 svg 文件,生成标签集合 65 | * @param {string} path 目标绝对路径,形如 c:/xx/xx//src/... 66 | * @param {string} prefix 生成symbol标签的id的前缀 67 | * @param {string} separator id前缀和文件名之间的分隔符 68 | * @returns {string} 作用于 SVG Element 的 innerHTML 字符串 69 | */ 70 | function preloadSVGToHTML(path, prefix, separator) { 71 | let SVGInnerHTML = '' 72 | const getSVGFile = (_path) => { 73 | const dirs = readdirSync(_path, { withFileTypes: true }) // 读取目录的内容 74 | for (const dir of dirs) { 75 | if (dir.isDirectory()) { // 如果是目录则进进入下一层级进行递归处理 76 | getSVGFile(_path + dir.name + '/') 77 | } else { // 非svg后缀的文件跳过 78 | if (!dir.name.endsWith('.svg')) continue 79 | // 根据前缀、分隔符和文件名生成标签ID 80 | const symbolId = prefix + separator + dir.name.replace('.svg', '') 81 | // 读取文件内,提取出标签内容,并换成标签添加上属性,便于项目代码中引用 82 | SVGInnerHTML += readFileSync(_path + dir.name).toString() 83 | .replace(svgTagExtract, (substr, viewBoxAttr) => { 84 | return `` 85 | }). 86 | replace('', '') 87 | } 88 | } 89 | } 90 | getSVGFile(path) 91 | return SVGInnerHTML 92 | } 93 | 94 | // !important 该方法将会在浏览器中执行 95 | function mountSVG(opt, innerHTML) { 96 | if (typeof opt === 'string') opt = JSON.parse(opt) // opt需要为对象格式 97 | if (opt === null || typeof opt !== 'object') 98 | throw new TypeError('mountSVG() param:opt must be a object') 99 | 100 | const element = 101 | document.getElementById(opt.customEleId) || 102 | document.createElementNS('http://www.w3.org/2000/svg', 'svg') 103 | 104 | element.id = opt.customEleId 105 | element.style.display = 'none' 106 | element.innerHTML = innerHTML 107 | document.body.append(element) 108 | } 109 | 110 | export function createSVGSprites(option) { 111 | const opt = Object.assign(defaultOption, option) 112 | // 遍历目标文件夹以获取SVG图标的innerHTML字符串 113 | const svgHtml = preloadSVGToHTML( 114 | path.join(process.cwd(), opt.path), 115 | opt.symbolIdPrefix, 116 | opt.separator, 117 | ) 118 | 119 | return { 120 | // 必须的,将会显示在 warning 和 error 中 121 | name: pluginName, 122 | // 必须的,Rollup模块加载机制:解析模块时,返回模块ID来让rollup加载 123 | resolveId(id) { 124 | if (id === virtualModuleId) return resolvedVirtualModuleId 125 | }, 126 | // 加载对应模块时(带着模块ID),返回模块内容,感觉类似webpack loader 127 | load(id) { 128 | if (id === resolvedVirtualModuleId) 129 | return moduleStringify( 130 | { mountSVG: mountSVG, innerHTML: svgHtml }, 131 | `mountSVG(${JSON.stringify(opt)},innerHTML);`, 132 | ) 133 | }, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/plugin/mockServe.js: -------------------------------------------------------------------------------- 1 | // 基于vite-plugin-mock插件的再封装 2 | // 使用说明 https://github.com/anncwb/vite-plugin-mock/blob/main/README.zh_CN.md 3 | import { viteMockServe } from 'vite-plugin-mock' 4 | 5 | // 关于vite启动项目模式文档 https://cn.vitejs.dev/guide/env-and-mode.html#modes 6 | const isDev = (command) => command === 'serve' 7 | 8 | const isProd = (command) => command === 'build' 9 | 10 | const injectCode = ` 11 | import { setupProdMockServer } from '../mock/_mockProdServer.js' 12 | setupProdMockServer(); 13 | ` 14 | 15 | /** 16 | * @param mode 参考 https://cn.vitejs.dev/guide/env-and-mode.html#modes 17 | * @param option 可选,vite-plugin-mock的所需的配置项,用于覆盖默认的。细节见该文件头顶的链接 18 | */ 19 | export const mockServe = function (mode, option = null) { 20 | const _option = { 21 | // 默认开启’开发模式‘下的 mock 功能,若要关闭直接设置为 false 22 | localEnabled: isDev(mode), 23 | // 默认开启’生产模式‘下的 mock 功能,若要关闭直接设置为 false 24 | prodEnabled: isProd(mode), 25 | // 根据 prodEnabled 的值,动态控制生产环境中mock的开启,未开启的mock也将不会被打包 26 | injectCode, 27 | } 28 | 29 | return viteMockServe(Object.assign(_option, option)) 30 | } 31 | -------------------------------------------------------------------------------- /src/router/CONSTANT.js: -------------------------------------------------------------------------------- 1 | // 不提前导出会报“ Cannot access 'xxx' before initialization” 2 | export const LAYOUT = () => import('/src/layout/index.vue') 3 | 4 | /** 5 | * 快速简易创建单层路由,其被 Layout 组件包裹 6 | * @example 7 | * createLayoutWrapper({ 8 | * path: '/profile', 9 | * children: { 10 | * path: 'index', 11 | * name: 'Profile', 12 | * component: () => import('/src/views/demo/profile/index.vue'), 13 | * meta: { title: '个人中心' }, 14 | * }, 15 | * }), 16 | * 当访问/profile时,跳转到/profile/index,且该页面被Layout包裹,标题为'个人中心' 17 | * 18 | * @param raw RouteRecordRaw 类型对象,不过children只有一个时,可以直接写成对象形式不用数组包裹 19 | * @param defaultChild 该路由默认要重定向到的子路由 20 | * @return RouteLocationRaw 最外层以Layout组件包裹的路由RouteRecordRaw对象 21 | */ 22 | export function createLayoutWrapper(raw, defaultChild = 'index') { 23 | if (!raw.path) { 24 | throw TypeError('Param ‘raw’ need path property!') 25 | } 26 | const children = [].concat(raw.children) 27 | const redirect = children.length === 1 ? children[0].path : defaultChild 28 | return { 29 | ...raw, 30 | redirect: raw.path + '/' + redirect, 31 | component: LAYOUT, 32 | children, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/router/README.md: -------------------------------------------------------------------------------- 1 | ## 项目路由使用说明 2 | TODO 3 | -------------------------------------------------------------------------------- /src/router/helper.js: -------------------------------------------------------------------------------- 1 | import {h} from 'vue' 2 | import {useRouter} from 'vue-router' 3 | 4 | const redirectComponent = { 5 | name: 'Redirect', 6 | setup() { 7 | const {currentRoute, replace} = useRouter() 8 | const query = currentRoute.value.query 9 | const path = currentRoute.value.params.path 10 | replace({path, query}) 11 | return () => h('div') 12 | }, 13 | } 14 | 15 | export const redirectRoute = { 16 | path: '/redirect', 17 | name: 'Redirect', 18 | hidden: true, 19 | component: redirectComponent, 20 | meta: {title: '...',noCache: true}, 21 | beforeEnter: (to, from) => { 22 | to.query = from.query 23 | to.params.path = from.fullPath 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHashHistory, createWebHistory,} from 'vue-router' 2 | import {getToken} from '/src/utils/storage' 3 | import {constRoutes} from './modules/const' 4 | import {basicRoutes} from './modules/basic' 5 | import {useUserStore} from '/src/store/user' 6 | import {useLayoutStore} from '/src/store/layout' 7 | 8 | const base = import.meta.env.BASE_URL 9 | const mode = import.meta.env.VITE_ROUTER_HISTORY 10 | const historyMode = // 从.env文件中读取配置判断是否hash模式还是history模式 11 | mode === 'hash' ? createWebHashHistory(base) : createWebHistory(base) 12 | const pageTitle = import.meta.env.VITE_DEFAULT_TITLE 13 | 14 | // 定义一个公共路径集合,任何用户及匿名者都能访问的到 15 | export const PUBLIC_PATH = new Set() 16 | basicRoutes.forEach((item) => PUBLIC_PATH.add(item.path)) 17 | 18 | // [vue-router官方文档指路]:(https://next.router.vuejs.org/zh/guide/index.html) 19 | const router = createRouter({ 20 | history: historyMode, 21 | routes: constRoutes, // 写的有点臃肿了哈 22 | strict: true, // 禁止尾随斜杠 23 | }) 24 | 25 | /** 26 | * 前置路由守卫钩子 27 | * 官网文档:https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html 28 | * 主要参考:https://juejin.cn/post/6844903478880370701 29 | * 参数类型:RouteLocationNormalized对象:https://next.router.vuejs.org/zh/api/#routelocationnormalized 30 | * 31 | * vue3中使用 addRoute 动态添加路由。并应在动态新增后再进行跳转 32 | * 刷新页面后,动态添加的路由将会丢失,需要重新加载 33 | * https://blog.csdn.net/weixin_43835425/article/details/116708448 34 | */ 35 | router.beforeEach(async (to) => { 36 | // 根据是否有 token 判断用户是否登录 37 | const token = getToken() 38 | // 如果[未登录]且要访问[不在]公共路径集合里的路径时,跳转到登录页面并记录之前的页面用于重新访问 39 | if (!token && !PUBLIC_PATH.has(to.path)) 40 | return {path: '/login', query: {redirect: to.fullPath}} 41 | const userStore = useUserStore() 42 | // 如果已登录但因为刷新后导致保存在内存中的数据(登录信息,动态添加的路由等)丢失, 43 | // 需要再次发起请求重新获取用户信息,并动态添加路由 44 | if (token && !userStore['hasUserInfo']) { 45 | await userStore['getUserInfo']() 46 | // 要添加个catch处理错误 47 | return to 48 | } 49 | }) 50 | 51 | /** 52 | * 后置路由守卫钩子 53 | * 对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。 54 | */ 55 | router.afterEach((to) => { 56 | document.title = to.meta.title || pageTitle 57 | // 记录访问过的页面 58 | useLayoutStore()['accessRecord'](to) 59 | }) 60 | 61 | export default router 62 | -------------------------------------------------------------------------------- /src/router/modules/async.js: -------------------------------------------------------------------------------- 1 | import { LAYOUT } from '../CONSTANT' 2 | import nestedRouter from './nested' 3 | 4 | /** 5 | * 异步路由表,由前端控制。 结合用户的roles过滤后添加到router中 6 | * 7 | * 在路由中的meta.roles.length<1时,说明不需要权限,任何已登录用户都可访问 8 | * 一个父级路由只带有 1 个子路由 或者 9 | * 一个父级路由只带有 1 个子路且这个子路由在只带1个子路由,只会显示最后一个子路由 10 | * 11 | * 需要被面包屑和侧边菜单栏显示,则路由和子路由的meta.title是必须的 12 | * FIXME: 13 | * 由于异步路由是创建同步路由后异步添加的,当访问的是这里面的某个路由时,在浏览器刷新时,url不变,↙ 14 | * 但是还没有这还没被加进去,会出现 [Vue Router warn]: No match found for location with path "/test/test1" 15 | * https://blog.csdn.net/weixin_43835425/article/details/116708448 16 | */ 17 | export const asyncRoutes = [ 18 | { 19 | path: '/test', 20 | redirect: '/test/test1', 21 | component: LAYOUT, 22 | hidden: true, 23 | children: [ 24 | { 25 | path: 'test1', 26 | name: 'Test1', 27 | component: () => import('/src/views/test/test1.vue'), 28 | meta: { title: '测试-1' }, 29 | }, 30 | ], 31 | }, 32 | { 33 | path: '/permission', 34 | redirect: '/permission/page', 35 | component: LAYOUT, 36 | meta: { title: '权限页面', roles: ['admin', 'editor', 'test'] }, 37 | children: [ 38 | { 39 | path: 'page', 40 | component: () => import('/src/views/demo/permission/page.vue'), 41 | name: 'PagePermission', 42 | meta: { title: '权限-页面'}, 43 | }, 44 | { 45 | path: 'test', 46 | component: () => import('/src/views/demo/permission/test.vue'), 47 | name: 'TestPermission', 48 | meta: { title: '权限-测试', roles: ['test'] }, 49 | }, 50 | { 51 | path: 'admin', 52 | component: () => import('/src/views/demo/permission/admin.vue'), 53 | meta: { title: '权限-管理', roles: ['admin'] }, 54 | }, 55 | ], 56 | }, 57 | /* 导入其他模块的路由 */ 58 | nestedRouter, 59 | // 404页面需要放在最后,确保没有路由被匹配时能正确跳转到404.vue 60 | { 61 | path: '/:catchAll(.*)*', 62 | name: 'NotFound', 63 | hidden: true, 64 | component: () => import('/src/views/sys/error-page/404.vue'), 65 | }, 66 | // 外链路由,主要是生成menu用的 67 | { 68 | path: '/ex-link', 69 | children: [ 70 | { 71 | path: 'https://github.com/someGenki/vue-lite-admin', 72 | name: 'ex-link-github', 73 | meta: { title: 'Github' }, 74 | }, 75 | ], 76 | }, 77 | { 78 | path: '/about', 79 | redirect: '/about/index', 80 | component: LAYOUT, 81 | children: [ 82 | { 83 | path: 'index', 84 | name: 'about', 85 | component: () => import('/src/views/about/index.vue'), 86 | meta: { title: '关于', icon: 'el-icon-place' }, 87 | }, 88 | ], 89 | }, 90 | ] 91 | -------------------------------------------------------------------------------- /src/router/modules/basic.js: -------------------------------------------------------------------------------- 1 | import { LAYOUT } from '../CONSTANT' 2 | import Redirect from '/src/views/sys/redirect/index.vue' 3 | import Login from '/src/views/sys/login/index.vue' 4 | import {redirectRoute} from "../helper"; 5 | 6 | // 这里存放不需要登录,不需要权限都能访问到的路由 7 | export const basicRoutes = [ 8 | redirectRoute, 9 | { 10 | path: '/login', 11 | hidden: true, 12 | component: Login, 13 | meta: { noCache: true, title: 'Vue Lite Admin 登录页' }, 14 | }, 15 | { 16 | path: '/404', 17 | component: () => import('/src/views/sys/error-page/404.vue'), 18 | hidden: true, 19 | }, 20 | { 21 | path: '/401', 22 | component: () => import('/src/views/sys/error-page/401.vue'), 23 | hidden: true, 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /src/router/modules/const.js: -------------------------------------------------------------------------------- 1 | import { LAYOUT, createLayoutWrapper } from '../CONSTANT' 2 | import { basicRoutes } from './basic' 3 | 4 | /** 5 | * 通用路由表,不需要动态获取的默认路由 6 | * 所有被展示到sidebar的路由都要有唯一的name属性 7 | * 当页面的name和组件的name重复时,会引发栈溢出ERROR 8 | */ 9 | export const constRoutes = [ 10 | ...basicRoutes, 11 | { 12 | path: '/', 13 | redirect: '/dashboard', 14 | component: LAYOUT, 15 | children: [ 16 | { 17 | path: '/dashboard', 18 | name: 'Dashboard', 19 | component: () => import('/src/views/dashboard/index.vue'), 20 | meta: { title: '首页', icon: 'el-icon-house' }, 21 | }, 22 | ], 23 | }, 24 | createLayoutWrapper({ 25 | path: '/icons', 26 | children: { 27 | path: 'index', 28 | name: 'Icons', 29 | component: () => import('/src/views/demo/icons/index.vue'), 30 | meta: { title: '图标展示', icon: 'el-icon-shopping-cart-full' }, 31 | }, 32 | }), 33 | createLayoutWrapper({ 34 | path: '/profile', 35 | children: { 36 | path: 'index', 37 | name: 'Profile', 38 | component: () => import('/src/views/demo/profile/index.vue'), 39 | meta: { title: '个人中心' }, 40 | }, 41 | }), 42 | /** 示例功能 */ 43 | { 44 | path: '/example', 45 | component: LAYOUT, 46 | redirect: '/example/file-upload', 47 | meta: { title: '功能示例', icon: 'el-icon-copy-document' }, 48 | children: [ 49 | { 50 | path: 'file-upload', 51 | component: () => 52 | import('/src/views/demo/example/file-upload/index.vue'), 53 | name: 'FileUpload', 54 | meta: { title: '文件上传', icon: 'el-icon-upload-filled' }, 55 | }, 56 | { 57 | path: 'file-download', 58 | component: () => 59 | import('/src/views/demo/example/file-download/index.vue'), 60 | name: 'FileDownload', 61 | meta: { title: '文件下载', icon: 'el-icon-download' }, 62 | }, 63 | { 64 | path: 'emotion-demo', 65 | component: () => 66 | import('/src/views/demo/example/emotion-demo/index.vue'), 67 | name: 'EmotionDemo', 68 | meta: { title: '输入框表情', icon: 'el-icon-box' }, 69 | }, 70 | { 71 | path: 'compare-demo', 72 | component: () => 73 | import('/src/views/demo/example/compare-demo/index.vue'), 74 | name: 'CompareDemo', 75 | meta: { title: '图片对比', icon: 'el-icon-picture-filled' }, 76 | }, 77 | { 78 | path: 'text-editor', 79 | component: () => 80 | import('/src/views/demo/example/text-editor/index.vue'), 81 | name: 'TextEditor', 82 | meta: { title: '文本编辑器', icon: 'el-icon-edit' }, 83 | }, 84 | { 85 | path: 'image-cropper', 86 | component: () => import('/src/views/sys/error-page/building.vue'), 87 | name: 'ImageCropper', 88 | meta: { title: '图片裁剪', icon: 'el-icon-picture-rounded' }, 89 | }, 90 | { 91 | path: 'silk-ribbon', 92 | component: () => import('/src/views/sys/error-page/building.vue'), 93 | name: 'SilkRibbon', 94 | meta: { title: '缎带组件', icon: 'el-icon-collection-tag' }, 95 | }, 96 | ], 97 | }, 98 | /** 示例页面 */ 99 | { 100 | path: '/example-page', 101 | component: LAYOUT, 102 | redirect: '/example-page/404', 103 | meta: { title: '页面示例', icon: 'el-icon-document' }, 104 | children: [ 105 | { 106 | path: '404', 107 | component: () => import('/src/views/sys/error-page/404.vue'), 108 | name: '404', 109 | meta: { title: '404页面', icon: 'el-icon-close' }, 110 | }, 111 | { 112 | path: '401', 113 | component: () => import('/src/views/sys/error-page/401.vue'), 114 | name: '401', 115 | meta: { title: '401页面', icon: 'el-icon-close' }, 116 | }, 117 | { 118 | path: 'watermark-page', 119 | component: () => import('/src/views/demo/example-page/watermark-page.vue'), 120 | name: 'WatermarkPage', 121 | meta: { title: '页面水印', icon: 'el-icon-flag' }, 122 | }, 123 | { 124 | path: 'scroll-page', 125 | component: () => import('/src/views/demo/example-page/scroll-page.vue'), 126 | name: 'ScrollPage', 127 | meta: { title: '滚动页面', icon: 'el-icon-d-caret' }, 128 | }, 129 | { 130 | path: 'example-table', 131 | name: 'ExampleTable', 132 | component: () => import('/src/views/sys/error-page/building.vue'), 133 | meta: { title: '表格 Table', icon: 'el-icon-calendar' }, 134 | }, 135 | { 136 | path: 'example-echarts', 137 | name: 'ExampleEcharts', 138 | component: () => import('/src/views/sys/error-page/building.vue'), 139 | meta: { title: '图表 Echarts', icon: 'el-icon-pie-chart' }, 140 | }, 141 | { 142 | path: 'example-drag', 143 | name: 'ExampleDrag', 144 | component: () => import('/src/views/sys/error-page/building.vue'), 145 | meta: { title: '拖拽 Drag', icon: 'el-icon-pointer' }, 146 | }, 147 | { 148 | path: 'online-chat', 149 | name: 'OnlineChat', 150 | component: () => import('/src/views/sys/error-page/building.vue'), 151 | meta: { title: '即时通讯聊天', icon: 'el-icon-microphone' }, 152 | }, 153 | ], 154 | }, 155 | ] 156 | -------------------------------------------------------------------------------- /src/router/modules/nested.js: -------------------------------------------------------------------------------- 1 | import { LAYOUT } from '../CONSTANT.js' 2 | 3 | // 嵌套路由示例 from vue-element-admin (include views) 4 | export default { 5 | path: '/nested', 6 | component: LAYOUT, 7 | redirect: '/nested/menu1/menu1-1', 8 | name: 'Nested', 9 | meta: { title: '嵌套路由' }, 10 | children: [ 11 | { 12 | path: 'menu1', 13 | component: () => import('/src/views/demo/nested/menu1/index.vue'), 14 | name: 'Menu1', 15 | meta: { title: 'Menu 1' }, 16 | redirect: '/nested/menu1/menu1-1', 17 | children: [ 18 | { 19 | path: 'menu1-1', 20 | component: () => import('/src/views/demo/nested/menu1/menu1-1/index.vue'), 21 | name: 'Menu1-1', 22 | meta: { title: 'Menu 1-1' }, 23 | }, 24 | { 25 | path: 'menu1-2', 26 | component: () => import('/src/views/demo/nested/menu1/menu1-2/index.vue'), 27 | name: 'Menu1-2', 28 | redirect: '/nested/menu1/menu1-2/menu1-2-1', 29 | meta: { title: 'Menu 1-2' }, 30 | children: [ 31 | { 32 | path: 'menu1-2-1', 33 | component: () => 34 | import('/src/views/demo/nested/menu1/menu1-2/menu1-2-1/index.vue'), 35 | name: 'Menu1-2-1', 36 | meta: { title: 'Menu 1-2-1' }, 37 | }, 38 | { 39 | path: 'menu1-2-2', 40 | component: () => 41 | import('/src/views/demo/nested/menu1/menu1-2/menu1-2-2/index.vue'), 42 | name: 'Menu1-2-2', 43 | meta: { title: 'Menu 1-2-2' }, 44 | }, 45 | ], 46 | }, 47 | { 48 | path: 'menu1-3', 49 | component: () => import('/src/views/demo/nested/menu1/menu1-3/index.vue'), 50 | name: 'Menu1-3', 51 | meta: { title: 'Menu 1-3' }, 52 | }, 53 | ], 54 | }, 55 | { 56 | path: 'menu2', 57 | name: 'Menu2', 58 | component: () => import('/src/views/demo/nested/menu2/index.vue'), 59 | meta: { title: 'Menu 2' }, 60 | }, 61 | ], 62 | } 63 | -------------------------------------------------------------------------------- /src/store/example.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | /** 5 | * 如何使用pinia作为vue3的状态管理仓库呢? 6 | * docs: https://pinia.vuejs.org/ 7 | * 0. 前提须知:Pinia是模块化的,不像Vuex有根store!!!其次,直接从state解构会失去响应式 8 | * 1. 引入Pinia依赖、app.use(createPinia()); export const useStore = defineStore(...); 9 | * 2. setup() 中访问 state、getters、actions 可直接 const store = useStore(); store.xxx 10 | * 3. 批量修改state参考:https://pinia.vuejs.org/core-concepts/state.html#mutating-the-state 11 | * 4. 定义store有选项式(options)语法和宽松的setup语法 12 | */ 13 | export const useExampleStore = defineStore('example', { 14 | // 定义状态 15 | state: () => ({ 16 | example: 'This is just an example', 17 | }), 18 | // 可以认为是store的计算属性;鼓励使用箭头函数 19 | getters: { 20 | exampleEnhanced: (state) => state.example + '!', 21 | }, 22 | // 操作方法,不再有麻烦恶心的Mutation,Action之分,统一在这操作state 不用箭头函数,this指向当前store 23 | actions: { 24 | exampleEnhancer() { 25 | this.example += 'e' 26 | }, 27 | }, 28 | }) 29 | 30 | export const useExampleStore2 = defineStore('example2', () => { 31 | const count = ref(0) 32 | 33 | function increment() { 34 | count.value++ 35 | } 36 | 37 | return { count, increment } 38 | }) 39 | /** 40 | * 订阅state的改变 41 | * 1. store.$subscribe((mutation, state) => { ... }) 42 | * 参数的mutation具体可以 import { MutationType } from 'pinia' 查看 43 | * 他与watch不用的是vue patch执行完后才触发 44 | * 2. 回调函数的作用:当state发生改变时,将整个state存到localStorage中 45 | * 例:localStorage.setItem('cart', JSON.stringify(state)) 46 | * 3. 注意:回调函数中mutation.event在dev和prod环境下表现不同! 47 | */ 48 | 49 | /** 50 | * 批量修改state参考:https://pinia.vuejs.org/core-concepts/state.html#mutating-the-state 51 | * 订阅actions的触发:https://pinia.vuejs.org/core-concepts/actions.html#subscribing-to-actions 52 | * // 持久化插件 53 | * // https://github.com/prazdevs/pinia-plugin-persistedstate 54 | */ 55 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const store = createPinia() 4 | 5 | export default store 6 | -------------------------------------------------------------------------------- /src/store/layout.js: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia' 2 | import {getSetting} from '/src/utils/storage' 3 | // import variables from '/src/styles/vars.module.scss' 4 | 5 | const noRecordViewPath = ['/login','/redirect'] 6 | 7 | export const useLayoutStore = defineStore('layout', { 8 | state: () => ({ 9 | // 侧边菜单栏展开的默认宽度 10 | sUnfoldWidth: getSetting('sUnfoldWidth', 190), 11 | // 是否展开侧边菜单栏 12 | unfoldSidebar: getSetting('unfoldSidebar', true), 13 | // 是否只保持一个子菜单的展开 14 | menuAccordion: getSetting('menuAccordion', true), 15 | // 是否固定头部 16 | fixedHeader: getSetting('fixedHeader', false), 17 | // 是否显示标签栏 18 | showTabBar: getSetting('showTabBar', true), 19 | // 是否显示侧边菜单栏中的Logo 20 | showLogo: getSetting('showLogo', true), 21 | 22 | // 控制设置面板的显示隐藏 23 | showSettings: false, 24 | // 侧边菜单栏折叠后宽度 25 | sCollapseWidth: 64, 26 | // 是否为移动端(小屏) 27 | isMobile: document.body.clientWidth < 768, 28 | 29 | // 路由记录相关属性 30 | breadcrumbList: [], 31 | visitedViews: [], 32 | cachedViews: [], 33 | }), 34 | getters: { 35 | // 响应式:如果是移动端,则不显示折叠后的sidebar 36 | sidebarWidth: (state) => 37 | state.unfoldSidebar === true 38 | ? state.sUnfoldWidth + 'px' 39 | : (state.isMobile ? 0 : state.sCollapseWidth) + 'px', 40 | // 当固定头部时,main-container的上边距值 41 | mainPaddingTopOnFixed: (state) => { 42 | if (!state.fixedHeader) return '0' 43 | else if (state.fixedHeader && state.showTabBar) return 42 + 30 + 'px' 44 | else return 42 + 'px' 45 | }, 46 | // 内容区域的左边距,避免覆盖sidebar 47 | mainPaddingLeft(state) { 48 | return state.isMobile ? 0 : this.sidebarWidth 49 | }, 50 | // ==getters 结束分割线== 51 | }, 52 | actions: { 53 | // 侧边栏切换 54 | toggleSidebar(bool) { 55 | if (bool !== undefined) { 56 | this.unfoldSidebar = bool 57 | } else { 58 | this.unfoldSidebar = !this.unfoldSidebar 59 | } 60 | }, 61 | // 设置面板切换 62 | toggleSettings(bool) { 63 | if (bool !== undefined) { 64 | this.showSettings = bool 65 | } else { 66 | this.showSettings = !this.showSettings 67 | } 68 | }, 69 | // 注册于src/layout/index.vue 监听页面resize 70 | checkIsMobile() { 71 | this.isMobile = document.body.clientWidth < 768 72 | }, 73 | /** 74 | * 记录每次访问的页面 75 | * 当前功能: 76 | * 1. 获取匹配的路由来生成面包屑导航 77 | * 2. 更新当前激活的路由 78 | */ 79 | accessRecord(to) { 80 | const matched = to.matched.filter((item) => item.meta && item.meta.title) 81 | this.breadcrumbList.length = 0 82 | this.breadcrumbList.push(...matched) 83 | this.addVisitedView(to) 84 | this.cachedVisitedView(to) 85 | }, 86 | // 记录访问过的页面,用于生成tab-bar 87 | addVisitedView(view) { 88 | if ( 89 | view.meta.title && 90 | !this.visitedViews.some((v) => v.path === view.path) && 91 | !noRecordViewPath.some((v) => view.path.includes(v)) 92 | ) { 93 | this.visitedViews.push({ 94 | name: view.name, 95 | path: view.path, 96 | query: view.query, 97 | title: view.meta.title, 98 | fullPath: view.fullPath, 99 | timeStamp: Date.now(), 100 | }) 101 | } 102 | }, 103 | // 根据条件缓存访问过的页面 104 | cachedVisitedView(view) { 105 | // 未设置不缓存 且 还没被缓存过 才缓存起来 106 | if ( 107 | !view.meta.noCache && 108 | view.name && 109 | !this.cachedViews.includes(view.name) 110 | ) { 111 | this.cachedViews.push(view.name) 112 | } 113 | }, 114 | // 移除被缓存的页面 115 | removeCachedView(route) { 116 | const index = this.cachedViews.indexOf(route.name) 117 | if (index > -1) { 118 | this.cachedViews.splice(index, 1) 119 | } 120 | }, 121 | // ==actions 结束分割线== 122 | }, 123 | }) 124 | -------------------------------------------------------------------------------- /src/store/style.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { toRaw } from 'vue' 3 | 4 | export const useStyleStore = defineStore('style', { 5 | // TODO 增加缓存到localstorage中 6 | state: () => ({ 7 | 'primary-color': '#35A7FF', 8 | 'primary-color-tinge': '#75d2de', 9 | 'primary-text-color': '#000000a6', 10 | 'primary-text-color-tinge': '#00000070', 11 | }), 12 | getters: { 13 | // 侧边菜单栏颜色样式 14 | elMenuStyle: (state) => ({ 15 | text: '#dcdcdc', 16 | background: 'transparent', 17 | activeText: state['primary-color'], 18 | }), 19 | }, 20 | actions: { 21 | injectCssVarToRoot() { 22 | const styles = document.documentElement.style 23 | const vars = toRaw(this.$state) 24 | Object.keys(vars).forEach((item) => { 25 | styles.setProperty(`--${item}`, vars[item]) 26 | }) 27 | }, 28 | changePrimaryColor(val) { 29 | document.documentElement.style.setProperty('--primary-color', val) 30 | }, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /src/store/user.js: -------------------------------------------------------------------------------- 1 | import router from '/src/router' 2 | import { defineStore } from 'pinia' 3 | import { getToken, setToken } from '/src/utils/storage' 4 | import { login as _login, getInfo as _getInfo } from '/src/api/user' 5 | import { asyncRoutes } from '/src/router/modules/async' 6 | import { constRoutes } from '/src/router/modules/const' 7 | 8 | export const useUserStore = defineStore('user', { 9 | state: () => ({ 10 | token: getToken(), 11 | name: '', 12 | avatar: '', 13 | /** 14 | * 项目只记录用户的角色,比如 admin,test之类的。 15 | * 声明该组件/元素需要该角色才会出现。 16 | * 如果还有`operateCode`&`menuCode`再说 17 | */ 18 | roles: [], 19 | addRoutes: [], 20 | }), 21 | getters: { 22 | // 根据roles是否不为空判断是否有用户信息 23 | hasUserInfo: (state) => state.roles && state.roles.length > 0, 24 | hadLogin: (state) => !!state.token, 25 | }, 26 | actions: { 27 | async login({ username, password }) { 28 | const res = await _login({ username, password }) 29 | setToken(res.data.token) 30 | this.token = res.data.token 31 | this.roles.push(...res.data.roles) 32 | this.name = res.data.name || 'Yuan' 33 | this.addRoutes = this.generateRoutes() 34 | }, 35 | 36 | async getUserInfo() { 37 | const res = await _getInfo() 38 | this.roles.push(...res.data.roles) 39 | this.addRoutes = this.generateRoutes() 40 | }, 41 | 42 | generateRoutes() { 43 | const accessedRoutes = this.roles.includes('admin') 44 | ? asyncRoutes || [] // 是最高权限的admin则直接加入所有异步路由 45 | : filterAsyncRoutes(asyncRoutes, this.roles) 46 | // 将动态生成的可以访问路由加入 vue router 中 47 | accessedRoutes.forEach((route) => router.addRoute(route)) 48 | // 记录所有路由,它与router.getRoutes有所不同! 49 | return constRoutes.concat(accessedRoutes) 50 | }, 51 | 52 | changeRoles(roles) { 53 | this.roles.length = 0 54 | this.roles.push(...roles) 55 | this.addRoutes = this.generateRoutes() 56 | }, 57 | }, 58 | }) 59 | 60 | /** 61 | * 递归的根据已登录用户的roles来过滤异步路由表来生成专属的路由表 62 | * @author PanJiaChen 63 | * @param routes asyncRoutes 64 | * @param roles 用户所有的角色数组 65 | */ 66 | function filterAsyncRoutes(routes, roles) { 67 | const res = [] 68 | 69 | routes.forEach((route) => { 70 | const tmp = { ...route } // 防止对象被修改 71 | if (hasPermission(roles, tmp)) { 72 | if (tmp.children) { 73 | tmp.children = filterAsyncRoutes(tmp.children, roles) 74 | } 75 | res.push(tmp) 76 | } 77 | }) 78 | 79 | return res 80 | } 81 | 82 | // 比较当前roles和路由需要的role是否有交集 83 | function hasPermission(roles, route) { 84 | return route.meta && route.meta.roles 85 | ? roles.some((role) => route.meta.roles.includes(role)) 86 | : true // 当某个路由record没有设置roles属性则默认所有都可以访问,所以返回true 87 | } 88 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 溢出省略号 3 | * @param {Number} 行数 4 | */ 5 | @mixin ellipsis($rowCount: 1) { 6 | @if $rowCount <= 1 { 7 | overflow: hidden; 8 | text-overflow: ellipsis; 9 | white-space: nowrap; 10 | } @else { 11 | display: -webkit-box; 12 | min-width: 0; 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | -webkit-line-clamp: $rowCount; 16 | -webkit-box-orient: vertical; 17 | } 18 | } 19 | 20 | @mixin clearfix { 21 | &::after { 22 | display: table; 23 | clear: both; 24 | content: ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $namespace: 'yuan'; 2 | $element-separator: '__'; 3 | $modifier-separator: '--'; 4 | $state-prefix: 'is-'; 5 | 6 | /* 定义全局scss变量,用于css var不生效时的默认值,主要还是使用css的变量来动态修改颜色 */ 7 | $primary-color: #02bf6f; 8 | $primary-color-tinge: #75d2de; 9 | $primary-text-color: #000000a6; 10 | $primary-text-color-tinge: #00000070; 11 | 12 | /* 由于部分js中使用了该数值,若要更改,需与js用到的地方同步修改 */ 13 | $hover-background-color: #f6f6f6; 14 | $sidebar-logo-height: 42px; 15 | $navbar-height: 42px; 16 | $tabBar-height: 30px; 17 | 18 | $scrollbar-width: 8px; 19 | /* 响应式的断点 */ 20 | $xl-width: 1920px; // 大大屏幕尺寸 21 | $lg-width: 1200px; // 大屏幕桌面尺寸 22 | $sm-width: 768px; // 小屏幕平板尺寸 23 | 24 | 25 | /* 字体 */ 26 | $title-text-color: #1d2129; // 显示标题的颜色 27 | $content-text-color: #171819; // 正文/评论展示的颜色 28 | $name-text-color: #515767; // 用于展示的名字的颜色 29 | $deep-blue: #1d7dfa; // 深蓝色,用于字体hover时的颜色 30 | $mild-gray: #949494; // 中等灰,用于显示次要信息的颜色 31 | $deep-gray: #4a545f; // 深灰色,用于显示卡片里描述的颜色 32 | 33 | /* 边框 */ 34 | $divide-thin-border: 1px solid #e6e6e6; // 卡片之间分割 35 | $blue-btn-border: 1px solid #1e80ff; // 按钮的边框 36 | 37 | /* 阴影 */ 38 | $box-shadow-base: 0 2px 6px rgba(0, 0, 0, 0.15); // 浮层阴影 39 | $popover-shadow: 0 12px 12px 0 #6e78824c; // 气泡卡片的阴影 40 | $card-shadow: 12px 12px 20px rgba(0, 0, 0, 0.05); 41 | 42 | /* 背景 */ 43 | $hover-bg: #e3e4e5; 44 | $card-bg: #ffffff; 45 | $button-bg: #40a9ff; 46 | $disabled-bg: #bbbbbb; 47 | -------------------------------------------------------------------------------- /src/styles/common.scss: -------------------------------------------------------------------------------- 1 | @import './element-plus.scss'; 2 | @import './_variables.scss'; 3 | 4 | /* 全局样式定义 */ 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | p { 15 | color: var(--primary-text-color, $primary-text-color); 16 | } 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | .clearfix::after { 23 | display: table; 24 | clear: both; 25 | content: ''; 26 | } 27 | 28 | .mask-zIndex99 { 29 | position: fixed; 30 | top: 0; 31 | left: 0; 32 | z-index: 99; 33 | width: 100%; 34 | height: 100%; 35 | background-color: #0003; 36 | } 37 | 38 | .flex-justify-around { 39 | display: flex; 40 | justify-content: space-around; 41 | } 42 | 43 | .flex-justify-between { 44 | display: flex; 45 | justify-content: space-between; 46 | } 47 | 48 | .flex-justify-center { 49 | display: flex; 50 | justify-content: center; 51 | } 52 | 53 | .hover-shadow:hover { 54 | box-shadow: 0 4px 12px 0 #0000001a; 55 | } 56 | 57 | // css3 自定义滚动条 https://www.jianshu.com/p/c2addb233acd 58 | // 自定义滚动条介绍 https://www.cnblogs.com/ranyonsue/p/9487599.html 59 | ::-webkit-scrollbar { 60 | z-index: 0; 61 | width: $scrollbar-width; 62 | height: $scrollbar-width; 63 | } 64 | 65 | ::-webkit-scrollbar-thumb { 66 | background-color: skyblue; 67 | border-radius: 6px; 68 | } 69 | 70 | ::-webkit-scrollbar-track { 71 | //background-color: white; 72 | } 73 | -------------------------------------------------------------------------------- /src/styles/element-plus.scss: -------------------------------------------------------------------------------- 1 | @import './_variables.scss'; 2 | 3 | /* 覆盖element-plus的部分样式 */ 4 | .el-breadcrumb__item:last-child .el-breadcrumb__inner { 5 | color: #afafaf; 6 | } 7 | 8 | .el-breadcrumb__inner a, 9 | .el-breadcrumb__inner.is-link { 10 | font-weight: 500; 11 | } 12 | 13 | .el-dropdown-menu { 14 | user-select: none; 15 | } 16 | 17 | .el-input-number--mini { 18 | width: 100px; 19 | } 20 | 21 | .el-upload.el-upload--text { 22 | width: 100%; 23 | max-width: 360px; 24 | 25 | > .el-upload-dragger { 26 | width: 100%; 27 | } 28 | } 29 | 30 | .el-menu-item *{ 31 | vertical-align: middle !important; 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/vars.module.scss: -------------------------------------------------------------------------------- 1 | $sidebar-logo-height: 42px; 2 | $navbar-height: 42px; 3 | $tabBar-height: 30px; 4 | $sm-width: 768px; 5 | 6 | :export { 7 | smWidth: $sm-width; 8 | navbarHeight: $navbar-height; 9 | tabBarHeight: $tabBar-height; 10 | sidebarLogoHeight: $sidebar-logo-height; 11 | } -------------------------------------------------------------------------------- /src/utils/.playground.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/someGenki/vue-lite-admin/b130f6f49740ea76cd93aaede3860fb5b1a57f27/src/utils/.playground.js -------------------------------------------------------------------------------- /src/utils/compRegister.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局组件注册 3 | * @param app - vue app实例 4 | * @param {object|[]} components - 需要批量注册的组件,每个数组项/对象属性需包含name属性作为默认注册名(待改进,目前是只用来注册ElIcon) 5 | * @param {object} [opts] - 配置选项 6 | * @param {string} [opts.prefix] - 全局注册的前缀 7 | * @param {Map} [opts.replace] - 替换默认的组件名,用于避免冲突 当Value为false值时不注册该组件 8 | */ 9 | export function globalRegister(app, components, opts) { 10 | const compsArr = Array.isArray(components) 11 | ? components 12 | : Object.values(components) 13 | const { prefix, replace } = opts 14 | 15 | compsArr.forEach((component) => { 16 | let name = component.name 17 | if (replace && replace.has(name)) { 18 | if (!replace.get(name)) return 19 | name = replace.get(name) 20 | } 21 | if (prefix) { 22 | name = prefix + name 23 | } 24 | app.component(name, component) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/convert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 将中划线格式字符串驼峰化 3 | * @example first-name => firstName 4 | * @from vue-next/packages/shared/src/index.ts 5 | */ 6 | export function camelize(str) { 7 | return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : '')) 8 | } 9 | 10 | /** 11 | * 将驼峰化(Lower Camel Case)转换成Kebab Case 12 | * 大驼峰的化要自己处理下前面的 - 号 13 | * @example firstName => first-name 14 | */ 15 | export function KebabCase(str) { 16 | return str.replace(/[A-Z]/g, (_, c) => '-' + c.toLowerCase()) 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/refreshToken.ts: -------------------------------------------------------------------------------- 1 | // 请求前根据时间来刷新token的方法 [供参考用!] 2 | class RefreshToken { 3 | private refreshing: boolean 4 | private waitingQueue: any[] 5 | 6 | constructor() { 7 | this.refreshing = false 8 | this.waitingQueue = [] 9 | } 10 | 11 | // 用于排除不需要token的接口,避免死循环请求 12 | private isNeedTokenURL(url, arr = ['/login', '/refresh', '/getUserInfo']) { 13 | return !arr.some((val) => url.indexOf(val) > -1) 14 | } 15 | 16 | // 返回一个promise对象,用于阻塞调用者继续执行。这里面发送刷新请求,成功后清空请求队列 17 | private refreshTokenBeforeReq( 18 | doRefreshTokenApi: () => Promise 19 | ): Promise { 20 | // 创建一个未完成的promise,把改变状态的resolve方法交给请求token结束后执行 21 | const promise = new Promise((resolve) => { 22 | console.log('等待新token') 23 | // 等待队列放的是一个回调函数,来延迟resolve的执行,以此控制promise状态的改变 24 | this.waitingQueue.push(() => resolve(null)) 25 | }) 26 | if (!this.refreshing) { 27 | this.refreshing = true 28 | // 模拟请求刷新Token接口,当接口返回数据时执行then方法 29 | doRefreshTokenApi() 30 | .then(() => { 31 | console.log('刷新token成功,放行队列中的请求') 32 | this.refreshing = false 33 | this.waitingQueue.forEach((cb) => cb()) 34 | this.waitingQueue.length = 0 35 | }) 36 | .catch((error) => { 37 | console.error(error) 38 | }) 39 | } 40 | return promise 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { getToken } from './storage.js' 3 | import { ElMessage } from 'element-plus' 4 | 5 | const BASEURL = String(import.meta.env.VITE_BASE_URL) 6 | 7 | axios.defaults.timeout = 5000 // 响应超时时间 8 | axios.defaults.baseURL = BASEURL // 请求的根路径 9 | 10 | // 配置 请求 拦截器 11 | axios.interceptors.request.use((request) => { 12 | const token = getToken() 13 | if (token) request.headers.token = token 14 | return request 15 | }) 16 | 17 | // 配置 响应 拦截器 18 | axios.interceptors.response.use( 19 | // 响应200 20 | (response) => { 21 | // ... doing something 22 | return Promise.resolve(response.data) 23 | }, 24 | // 响应4xx 5xx 25 | (error) => { 26 | // ... doing something 27 | ElMessage({ message: error.response?.data?.msg || 'undefined' }) 28 | return Promise.reject(error.response) 29 | } 30 | ) 31 | 32 | // jsonp('url',{param1:data}).then(data => { console.log(data) }) 33 | export function jsonp(url, params, cbName = null) { 34 | // 函数调用计数,每次调用该函数时自增,用于避免回调函数重名 35 | const cbCount = (jsonp.cnt = (jsonp.cnt | 0) + 1) 36 | const callbackName = cbName || 'cb_' + cbCount 37 | 38 | // 拼接请求参数 先是判断是否已有查询参数,后添加回调函数名作为参数 39 | let querystring = 40 | (url.indexOf('?') > 0 ? '&' : '?') + `callback=${callbackName}` 41 | Object.entries(params || {}).forEach( 42 | ([k, v]) => (querystring += `&${k}=${v}`) 43 | ) 44 | 45 | // 创建script标签,添加src属性并添加到body中 46 | const scriptTag = document.createElement('script') 47 | scriptTag.src = url + querystring 48 | document.appendChild(scriptTag) 49 | 50 | return new Promise( 51 | (resolve) => 52 | (window[callbackName] = (data) => { 53 | resolve(data) 54 | scriptTag.remove() 55 | delete window[callbackName] 56 | }) 57 | ) 58 | } 59 | 60 | export default axios 61 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | //docs https://www.npmjs.com/package/js-cookie 3 | 4 | const KEY_PREFIX = 'YUAN-' 5 | const KEY_TOKEN = 'ADMIN-TOKEN' 6 | const invalids = [undefined, null, 'undefined', 'null'] 7 | 8 | export function getToken() { 9 | return Cookies.get(KEY_TOKEN) 10 | } 11 | 12 | export function setToken(token) { 13 | Cookies.set(KEY_TOKEN, token) 14 | } 15 | 16 | export function removeToken() { 17 | return Cookies.remove(KEY_TOKEN) 18 | } 19 | 20 | export function get(key) { 21 | return localStorage.getItem(KEY_PREFIX + key) 22 | } 23 | 24 | export function set(key, val) { 25 | localStorage.setItem(KEY_PREFIX + key, val) 26 | } 27 | 28 | export function remove(key) { 29 | localStorage.removeItem(KEY_PREFIX + key) 30 | } 31 | 32 | export function saveSetting(key, val) { 33 | if (invalids.includes(val)) { 34 | console.warn('dont use invalid value!') 35 | } 36 | localStorage.setItem(KEY_PREFIX + key, JSON.stringify(val)) 37 | } 38 | 39 | export function batchSaveSetting(keys, obj) { 40 | keys.forEach((key) => saveSetting(key, obj[key])) 41 | } 42 | 43 | export function getSetting(key, defVal = undefined) { 44 | let item = localStorage.getItem(KEY_PREFIX + key) 45 | if (invalids.includes(item)) { 46 | return defVal 47 | } else { 48 | return JSON.parse(item) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/util.js: -------------------------------------------------------------------------------- 1 | const isObject = (obj) => (obj !== null && typeof obj === 'object') 2 | 3 | // 延时执行版防抖 使用场景:按钮点击、输入框验证 4 | export function debounce(fn, delay = 1000) { 5 | let timer = null 6 | return function (...args) { 7 | if (timer) clearTimeout(timer) 8 | timer = setTimeout(() => { 9 | fn.apply(this, args) 10 | timer = null 11 | }, delay) 12 | } 13 | } 14 | // 定时器版立即执行节流 使用场景: input输入 页面滚动 窗口缩放 按键长按 联想搜索 滚动加载更多 15 | export function throttle(fn, interval = 1000) { 16 | let timer = null 17 | return function(...args) { 18 | if (timer === null) { 19 | fn(...args) 20 | // timer为null时,有空可执行。并设置timer,interval之后再置为null 21 | timer = setTimeout(() => timer = null, interval); 22 | } 23 | } 24 | } 25 | 26 | export function deepClone(target, map = new WeakMap()) { 27 | if (target === null || typeof target !== 'object') return target // 基本类型返回 28 | if ([Date, RegExp, Set, Map, Function].includes(target.constructor)) 29 | return (new target.constructor(target)) // 特殊类型克隆 30 | if (map.get(target)) return map.get(target) 31 | const newTarget = Array.isArray(target) ? [] : {} 32 | map.set(target, newTarget) 33 | for (let prop in target) { 34 | if (target.hasOwnProperty(prop)) 35 | newTarget[prop] = deepClone(target[prop], map) 36 | } 37 | return newTarget 38 | } 39 | 40 | export function deepEqual(o1, o2) { 41 | // 类型不全为Object则直接使用 === 比较 42 | if (!isObject(o1) || !isObject(o2)) return o1 === o2 43 | // 地址值一样则是同一个对象 44 | if (o1 === o2) return true 45 | // 获取对象的keys 46 | const keys1 = Object.keys(o1); 47 | const keys2 = Object.keys(o2); 48 | // keys长度不一致 提前返回 false 49 | if (keys1.length !== keys2.length) return false; 50 | // 递归比较2个 object 的key值 51 | return keys1.every((k1, idx) => deepEqual(o1[k1], o2[keys2[idx]])) 52 | } 53 | -------------------------------------------------------------------------------- /src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 46 | 47 | 58 | -------------------------------------------------------------------------------- /src/views/dashboard/components/Cards.js: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { ElRow, ElCol } from 'element-plus' 3 | import Github from './GithubState/index.vue' 4 | import WeChat from './WeChatWallet/index.vue' 5 | import Earning from './EarningPosition/index.vue' 6 | import Bilibili from './BilibiliState/index.vue' 7 | 8 | const cards = [Github, Bilibili, Earning, WeChat] 9 | 10 | const ElColProps = { 11 | xs: 24, 12 | sm: 12, 13 | lg: 6, 14 | style: { paddingBottom: '10px' }, 15 | } 16 | 17 | // 根据cards的数量生成N个被ElCol包裹的卡片组件 18 | const renderElColCards = cards.map((card) => { 19 | return h(ElCol, ElColProps, { default: () => h(card) }) 20 | }) 21 | 22 | /** 23 | * 函数式组件用法示例,但真实项目不常用... 24 | * docs:https://v3.cn.vuejs.org/guide/migration/functional-components.html 25 | */ 26 | export default function (props) { 27 | return h(ElRow, props, { default: () => renderElColCards }) 28 | } 29 | -------------------------------------------------------------------------------- /src/views/dashboard/components/EarningPosition/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 64 | 65 | 116 | -------------------------------------------------------------------------------- /src/views/dashboard/components/GithubState/githubCat.svg: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 | 18 | 22 | 23 | 24 | 28 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/views/dashboard/components/GithubState/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 33 | 34 | 89 | -------------------------------------------------------------------------------- /src/views/dashboard/components/WeChatWallet/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | 35 | 77 | -------------------------------------------------------------------------------- /src/views/dashboard/components/WeChatWallet/wechat-receiving.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/someGenki/vue-lite-admin/b130f6f49740ea76cd93aaede3860fb5b1a57f27/src/views/dashboard/components/WeChatWallet/wechat-receiving.jpg -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 70 | 71 | 76 | -------------------------------------------------------------------------------- /src/views/dashboard/options/navList.js: -------------------------------------------------------------------------------- 1 | import eleLogo from '/src/assets/element-logo.svg' 2 | import vueLogo from '/src/assets/vlogo.png' 3 | // 如需使用src/assets中图片可以使用es6的导入功能 👆 4 | 5 | const navList = [ 6 | { 7 | link: 'https://element-plus.gitee.io/#/zh-CN/', 8 | img: eleLogo, 9 | title: 'Element Plus官网', 10 | desc: 'Vue 3的桌面端组件库', 11 | }, 12 | { 13 | link: 'https://v3.cn.vuejs.org/', 14 | img: vueLogo, 15 | title: 'Vue.js3中文文档', 16 | desc: '渐进式 JavaScript 框架', 17 | }, 18 | { 19 | link: 'https://pinia.vuejs.org/', 20 | img: 'https://d33wubrfki0l68.cloudfront.net/ddd72aa8248a5c2f77429b9496e6e3e4da2a4e26/8afc0/logo.svg', 21 | title: 'Pinia官方文档', 22 | desc: 'Vuex的替代者,采用模块化设计', 23 | }, 24 | { 25 | link: 'https://vueuse.org/', 26 | img: 'https://d33wubrfki0l68.cloudfront.net/a5780e53fee68ddd1cd73a00484151d2d052cb4d/b7469/logo-vertical.png', 27 | desc: 'Collection of essential Vue Composition Utilities', 28 | }, 29 | { 30 | link: 'https://cn.vitejs.dev/', 31 | img: 'https://cn.vitejs.dev/logo.svg', 32 | title: 'Vite 官方中文文档', 33 | desc: '下一代前端开发与构建工具', 34 | }, 35 | { 36 | link: 'https://vvbin.cn/next/', 37 | img: 'https://vvbin.cn/next/assets/logo.63028018.png', 38 | title: 'VbenAdmin', 39 | desc: '我见过很优秀的免费的管理系统模板', 40 | }, 41 | ] 42 | 43 | export default navList 44 | -------------------------------------------------------------------------------- /src/views/demo/example-page/scroll-page.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | 27 | -------------------------------------------------------------------------------- /src/views/demo/example-page/watermark-page.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /src/views/demo/example/compare-demo/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/views/demo/example/emotion-demo/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/views/demo/example/file-download/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 55 | 56 | 77 | -------------------------------------------------------------------------------- /src/views/demo/example/file-upload/index.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 146 | 147 | 166 | -------------------------------------------------------------------------------- /src/views/demo/example/text-editor/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | 40 | 46 | -------------------------------------------------------------------------------- /src/views/demo/icons/index.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 110 | 111 | 176 | -------------------------------------------------------------------------------- /src/views/demo/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/views/demo/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/views/demo/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/views/demo/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/demo/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/demo/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/demo/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/demo/permission/admin.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/views/demo/permission/page.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 55 | 56 | 62 | -------------------------------------------------------------------------------- /src/views/demo/permission/test.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/views/demo/profile/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 111 | 112 | 202 | -------------------------------------------------------------------------------- /src/views/demo/profile/juejin-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/views/sys/error-page/401.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/views/sys/error-page/404.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/views/sys/error-page/building.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/views/sys/login/loginBackgronds.js: -------------------------------------------------------------------------------- 1 | // 花里胡哨的东西之登录页背景渐变色 2 | export default [ 3 | 'linear-gradient(60deg, #96deda 0%, #50c9c3 100%)', 4 | 'linear-gradient(60deg, #64b3f4 0%, #c2e59c 100%)', 5 | 'linear-gradient(120deg, #f6d365 0%, #fda085 100%)', 6 | 'linear-gradient(to top, #fbc2eb 0%, #a6c1ee 100%)', 7 | 'linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%)', 8 | 'linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)', 9 | 'linear-gradient(120deg, #fccb90 0%, #d57eeb 100%)', 10 | 'linear-gradient(120deg, #89f7fe 0%, #66a6ff 100%)', 11 | 'linear-gradient(to top, #cd9cf2 0%, #f6f3ff 100%)', 12 | 'linear-gradient(to top, #c471f5 0%, #fa71cd 100%)', 13 | 'linear-gradient(to top, #88d3ce 0%, #6e45e2 100%)', 14 | 'linear-gradient(-20deg, #6e45e2 0%, #88d3ce 100%)', 15 | 'linear-gradient(-20deg, #b721ff 0%, #21d4fd 100%)', 16 | 'linear-gradient(to top, #f77062 0%, #fe5196 100%)', 17 | 'linear-gradient(-20deg, #00cdac 0%, #8ddad5 100%)', 18 | 'linear-gradient(to top, #209cff 0%, #68e0cf 100%)', 19 | 'linear-gradient(to top, #9be15d 0%, #00e3ae 100%)', 20 | 'linear-gradient(to top, #cc208e 0%, #6713d2 100%)', 21 | 'linear-gradient(to top, #b3ffab 0%, #12fff7 100%)', 22 | 'linear-gradient(-225deg, #E3FDF5 0%, #FFE6FA 100%)', 23 | 'linear-gradient(-225deg, #7DE2FC 0%, #B9B6E5 100%)', 24 | 'linear-gradient(-225deg, #20E2D7 0%, #F9FEA5 100%)', 25 | 'linear-gradient(-225deg, #FFFEFF 0%, #D7FFFE 100%)', 26 | 'linear-gradient(to right, #4facfe 0%, #00f2fe 100%)', 27 | 'linear-gradient(to right, #43e97b 0%, #38f9d7 100%)', 28 | 'linear-gradient(to right, #74ebd5 0%, #9face6 100%)', 29 | 'linear-gradient(to right, #92fe9d 0%, #00c9ff 100%)', 30 | 'linear-gradient(to top, #d5dee7 0%, #ffafbd 0%, #c9ffbf 100%)', 31 | 'linear-gradient(-225deg, #DFFFCD 0%, #90F9C4 48%, #39F3BB 100%)', 32 | 'linear-gradient(-225deg, #5D9FFF 0%, #B8DCFF 48%, #6BBBFF 100%)', 33 | 'linear-gradient(-225deg, #9EFBD3 0%, #57E9F2 48%, #45D4FB 100%)', 34 | 'linear-gradient(-225deg, #D4FFEC 0%, #57F2CC 48%, #4596FB 100%)', 35 | 'linear-gradient(to top, #3f51b1 0%, #5a55ae 13%, #7b5fac 25%, #8f6aae 38%, #a86aa4 50%, #cc6b8e 62%, #f18271 75%, #f3a469 87%, #f7c978 100%)', 36 | ] 37 | -------------------------------------------------------------------------------- /src/views/sys/login/useLogin.js: -------------------------------------------------------------------------------- 1 | import {reactive, toRaw} from 'vue' 2 | import {useRouter} from 'vue-router' 3 | import {ElNotification} from 'element-plus' 4 | import {useUserStore} from '/src/store/user' 5 | 6 | const loginFormObj = { 7 | username: 'jojo2', 8 | password: 'jojo2', 9 | code: '4396', 10 | } 11 | 12 | const getParams = (fullPath) => { 13 | let index 14 | if (fullPath && (index = fullPath.indexOf('?')) > -1) { 15 | const usp = new URLSearchParams(fullPath.substring(index)) 16 | return Object.fromEntries(usp.entries()) 17 | } 18 | } 19 | 20 | 21 | const getRandomCode = () => { 22 | return Math.floor(Math.random() * 9000 + 1000) + '' 23 | } 24 | 25 | export function useLogin() { 26 | const user = useUserStore() 27 | const router = useRouter() 28 | const route = router.currentRoute.value 29 | const code = getRandomCode() 30 | loginFormObj.code = code 31 | const loginForm = reactive(loginFormObj) 32 | const loginRules = { 33 | username: [{required: true, message: '用户名不能为空'}], 34 | password: [{required: true, message: '密码不能为空'}], 35 | code: [ 36 | { 37 | validator: (rule, value, cb) => { 38 | if (!value) { 39 | cb(new Error('请输入验证码')) 40 | } else if (value !== code) { 41 | cb(new Error('验证码不一致')) 42 | } else { 43 | cb() 44 | } 45 | }, 46 | trigger: 'blur', 47 | }, 48 | ], 49 | } 50 | // 表单验证通过时,调用store中的登录方法 51 | const loginPassed = () => { 52 | return user['login'](toRaw(loginForm)) 53 | .then(() => new Promise((res) => setTimeout(() => res(), 120))) 54 | .then( 55 | (data) => { 56 | router.push({ 57 | path: route.query.redirect || '/', 58 | query: getParams(route.query.redirect) || {}, 59 | }) 60 | ElNotification({ 61 | type: 'success', 62 | duration: 1500, 63 | message: '登录成功!', 64 | }) 65 | }, 66 | (reason) => { 67 | console.log(reason) 68 | ElNotification({ 69 | type: 'error', 70 | offset: 80, 71 | message: '登录失败 ' + reason, 72 | }) 73 | } 74 | ) 75 | } 76 | 77 | return {loginForm, loginRules, code, loginPassed} 78 | } 79 | -------------------------------------------------------------------------------- /src/views/sys/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /src/views/test/test1.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | // 与ESlint类似的CSS检测工具, 用于样式规范检查与修复 [官网] https://stylelint.io/ 2 | // vue项目中使用新的单文件组件样式的特性(状态驱动的动态 CSS | v-bind(var) )时,值需要用引号包裹起来,避免被stylelint所格式化 3 | module.exports = { 4 | root: true, 5 | extends: [ 6 | 'stylelint-config-recommended-scss', 7 | 'stylelint-config-recommended-vue', 8 | 'stylelint-config-prettier', 9 | // 对css进行排序 → https://github.com/stormwarning/stylelint-config-recess-order/blob/main/index.js 10 | 'stylelint-config-recess-order', 11 | ], 12 | rules: { 13 | 'no-empty-source': null, 14 | 'property-no-unknown':null, 15 | 'at-rule-no-unknown': null, 16 | 'no-descending-specificity': null, 17 | 'selector-pseudo-class-no-unknown':null, 18 | 'scss/at-import-partial-extension': null, 19 | 'scss/at-import-no-partial-leading-underscore': null, 20 | }, 21 | ignoreFiles: [ 22 | '**/*.js', 23 | '**/*.jsx', 24 | '**/*.ts', 25 | '**/*.tsx', 26 | 'dist/**', 27 | 'public/**', 28 | 'index.html', 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import dayjs from 'dayjs' 3 | import pkg from './package.json' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | import {mockServe} from './src/plugin/mockServe' 7 | import {createSVGSprites} from './src/plugin/createSVGSprites' 8 | 9 | const {dependencies, devDependencies, name, version} = pkg 10 | 11 | const __APP_INFO__ = { 12 | pkg: {dependencies, devDependencies, name, version}, 13 | lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), 14 | } 15 | 16 | const libNameReg = /\/node_modules\/([^/]+)\// 17 | 18 | const manualChunks = (id) => { 19 | if (libNameReg.test(id.toString())) { 20 | const libName = RegExp.$1 21 | switch (libName) { 22 | case '@vue': 23 | case 'echarts': 24 | case '@popperjs': 25 | case 'element-plus': 26 | case '@element-plus': 27 | return libName 28 | default: 29 | return 'vendor' 30 | } 31 | } 32 | } 33 | 34 | // 官方文档 https://cn.vitejs.dev/config/ dio 35 | export default ({command}) => { 36 | return { 37 | base: '/admin/', // 可被命令行参数 --base=/xxx/ 覆盖 38 | 39 | server: { 40 | port: 8008, 41 | open: true, 42 | // 关于本地反向代理解决跨域 戳文档:https://cn.vitejs.dev/config/#server-proxy 43 | }, 44 | 45 | build: { 46 | reportCompressedSize: false, // 禁用 压缩大小报告,以提高大型项目的构建性能。 47 | // https://www.zhihu.com/question/518443897/answer/2397938046 48 | rollupOptions: {manualChunks}, 49 | cssCodeSplit: false, 50 | }, 51 | 52 | plugins: [ 53 | vue(), 54 | vueJsx(), // 文档 https://github.com/vuejs/babel-plugin-jsx/blob/dev/packages/babel-plugin-jsx/README-zh_CN.md 55 | mockServe(command), 56 | createSVGSprites(), 57 | ], 58 | 59 | resolve: { 60 | alias: { 61 | // 别名 `@` 指向 `src` 目录 PS:IDEA编辑器还是不能识别 62 | '@': path.resolve(__dirname, 'src'), 63 | assets: '/src/assets', 64 | comp: '/src/components', 65 | }, 66 | }, 67 | 68 | css: { 69 | preprocessorOptions: { 70 | scss: { 71 | additionalData: `@import "/src/styles/_variables";\n`, 72 | }, 73 | }, 74 | 75 | postcss: { 76 | plugins: [ 77 | { 78 | postcssPlugin: 'internal:charset-removal', 79 | AtRule: { 80 | // 去除 warning: "@charset" must be the first rule in the file 81 | charset: (atRule) => atRule.name === 'charset' && atRule.remove(), 82 | }, 83 | }, 84 | ], 85 | }, 86 | }, 87 | 88 | // 定义全局常量替换方式,其中每项在开发环境下会被定义在全局,而在构建时被静态替换 89 | define: { 90 | __APP_INFO__: JSON.stringify(__APP_INFO__), 91 | }, 92 | } 93 | } 94 | --------------------------------------------------------------------------------