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

6 |
7 |
8 |
9 |
文件名: {{file.name}}
10 |
文件大小: {{file.size}}
11 |
文件类型: {{file.type}}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
38 |
--------------------------------------------------------------------------------
/src/views/vue-use/elements/useIntersectionObserver.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
请上下滚动鼠标
5 |
是否开始加载下一屏的数据:{{targetIsVisible ? '是' : '否'}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
39 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import vue from "@vitejs/plugin-vue";
3 | // import path from 'path-browserify'
4 | import path from "path";
5 |
6 | import { viteMockServe } from "vite-plugin-mock";
7 | import vitePluginGitInfo from "./src/plugins/vite-plugin-git-info.js";
8 |
9 | // svg-icon插件
10 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
11 | export default defineConfig({
12 | base: "./", // 打包路径s
13 | plugins: [
14 | vue(),
15 | createSvgIconsPlugin({
16 | // 指定要缓存的图标文件夹
17 | iconDirs: [path.resolve("./src/icons/svg")],
18 | // 执行icon name的格式
19 | symbolId: "icon-[name]"
20 | }),
21 | viteMockServe({
22 | enable: false,
23 | logger: true,
24 | mockPath: "./src/mock/",
25 | supportTs: false
26 |
27 | }),
28 | vitePluginGitInfo()
29 |
30 | ],
31 | resolve: {
32 | alias: {
33 | "@": path.resolve("./src")
34 | },
35 | extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"]
36 | },
37 | server: {
38 | cors: true, // 允许跨域
39 | host: "0.0.0.0",
40 | open: true, // 服务启动时是否自动打开浏览器
41 | port: 9999, // 服务端口号
42 | proxy: {
43 | "/api": {
44 | changeOrigin: true,
45 | rewrite: (path) => path.replace(/^\/api/, ""),
46 | target: "http://127.0.0.1:9999/"
47 | // target: "http://localhost:8080/"
48 |
49 | }
50 | }
51 |
52 | }
53 |
54 | });
--------------------------------------------------------------------------------