├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── .stylelintignore ├── .stylelintrc.cjs ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── src ├── App.vue ├── api │ ├── index.ts │ ├── interface │ │ ├── index.ts │ │ └── system.ts │ └── modules │ │ ├── login.ts │ │ └── system.ts ├── assets │ └── images │ │ ├── 403.svg │ │ ├── 404.svg │ │ ├── 500.svg │ │ ├── login-left.svg │ │ ├── login_bg.svg │ │ ├── logo.png │ │ └── logo.svg ├── components │ ├── ECharts │ │ ├── config │ │ │ └── index.ts │ │ └── index.vue │ ├── ErrorMessage │ │ ├── 403.vue │ │ ├── 404.vue │ │ ├── 500.vue │ │ └── index.scss │ └── SwitchDark │ │ └── index.vue ├── config │ └── index.ts ├── hooks │ └── useTheme.ts ├── layout │ ├── components │ │ ├── Header │ │ │ ├── ToolBarLeft.vue │ │ │ ├── ToolBarRight.vue │ │ │ └── components │ │ │ │ ├── Avatar.vue │ │ │ │ ├── BreadCrumb.vue │ │ │ │ ├── CollapseIcon.vue │ │ │ │ ├── Fullscreen.vue │ │ │ │ └── ThemeSetting.vue │ │ ├── Main │ │ │ ├── index.scss │ │ │ └── index.vue │ │ ├── Menu │ │ │ └── SubMenu.vue │ │ ├── Tabs │ │ │ ├── components │ │ │ │ └── MoreButton.vue │ │ │ ├── index.scss │ │ │ └── index.vue │ │ └── ThemeDrawer │ │ │ ├── index.scss │ │ │ └── index.vue │ ├── index.scss │ └── index.vue ├── main.ts ├── router │ ├── dynamicRouter.ts │ └── index.ts ├── store │ ├── helper │ │ └── persist.ts │ ├── index.ts │ ├── interface │ │ └── index.ts │ └── modules │ │ ├── auth.ts │ │ ├── global.ts │ │ ├── keepAlive.ts │ │ ├── tabs.ts │ │ └── user.ts ├── styles │ ├── common.scss │ ├── element-dark.scss │ ├── iconfont.scss │ ├── reset.scss │ └── var.scss ├── typings │ ├── auto-imports.d.ts │ └── components.d.ts ├── utils │ ├── color.ts │ ├── index.ts │ ├── mittBus.ts │ └── nprogress.ts ├── views │ ├── about │ │ ├── index.scss │ │ └── index.vue │ ├── dashboard │ │ ├── components │ │ │ ├── carChart.vue │ │ │ └── vistorChart.vue │ │ ├── index.scss │ │ └── index.vue │ ├── echarts │ │ ├── columnChart │ │ │ ├── index.scss │ │ │ └── index.vue │ │ └── waterChart │ │ │ ├── index.scss │ │ │ └── index.vue │ ├── login │ │ ├── components │ │ │ └── LoginForm.vue │ │ ├── index.scss │ │ └── index.vue │ ├── menu │ │ ├── menu1 │ │ │ ├── index.scss │ │ │ └── index.vue │ │ ├── menu2 │ │ │ ├── menu21 │ │ │ │ ├── index.scss │ │ │ │ └── index.vue │ │ │ ├── menu22 │ │ │ │ ├── menu221 │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.vue │ │ │ │ └── menu222 │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.vue │ │ │ └── menu23 │ │ │ │ ├── index.scss │ │ │ │ └── index.vue │ │ └── menu3 │ │ │ ├── index.scss │ │ │ └── index.vue │ └── system │ │ ├── accountManage │ │ ├── components │ │ │ └── userDialog.vue │ │ ├── index.scss │ │ └── index.vue │ │ ├── departmentManage │ │ ├── components │ │ │ └── departmentDialog.vue │ │ ├── index.scss │ │ └── index.vue │ │ ├── menuManage │ │ ├── components │ │ │ └── menuDialog.vue │ │ ├── index.scss │ │ └── index.vue │ │ └── roleManage │ │ ├── components │ │ └── roleDialog.vue │ │ ├── index.scss │ │ └── index.vue └── vite-env.d.ts ├── stats.html ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] # 表示所有文件适用 6 | charset = utf-8 # 设置文件字符集为 utf-8 7 | indent_style = space # 缩进风格(tab | space) 8 | indent_size = 2 # 缩进大小 9 | end_of_line = lf # 控制换行类型(lf | cr | crlf) 10 | trim_trailing_whitespace = true # 去除行首的任意空白字符 11 | insert_final_newline = true # 始终在文件末尾插入一个新行 12 | 13 | [*.md] # 表示仅 md 文件适用以下规则 14 | max_line_length = off 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # title 2 | VITE_GLOB_APP_TITLE = vue3-admin-client 3 | 4 | # 本地运行端口号 5 | VITE_PORT = 3000 6 | 7 | # 打包后是否生成包分析文件 8 | VITE_REPORT = false 9 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | VITE_USER_NODE_ENV = development 3 | 4 | # 开发环境接口地址 5 | VITE_API_URL = /api 6 | 7 | # 开发环境跨域代理 8 | VITE_PROXY = http://localhost:3300 9 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | VITE_USER_NODE_ENV = production 3 | 4 | # 开发环境接口地址 5 | VITE_API_URL = /api 6 | 7 | # 打包时是否删除 console 8 | VITE_DROP_CONSOLE = true 9 | 10 | 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | node_modules 3 | *.md 4 | *.woff 5 | *.ttf 6 | .vscode 7 | .idea 8 | dist 9 | /public 10 | /docs 11 | .husky 12 | .local 13 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // @see: http://eslint.cn 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | browser: true, 7 | node: true, 8 | es6: true 9 | }, 10 | // 指定如何解析语法 11 | parser: 'vue-eslint-parser', 12 | // 优先级低于 parse 的语法解析配置 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | ecmaVersion: 2020, 16 | sourceType: 'module', 17 | jsxPragma: 'React', 18 | ecmaFeatures: { 19 | jsx: true 20 | } 21 | }, 22 | // 继承某些已有的规则 23 | extends: [ 24 | 'plugin:vue/vue3-recommended', 25 | 'plugin:@typescript-eslint/recommended', 26 | 'plugin:prettier/recommended' 27 | ], 28 | /** 29 | * "off" 或 0 ==> 关闭规则 30 | * "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行) 31 | * "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错) 32 | */ 33 | rules: { 34 | // eslint (http://eslint.cn/docs/rules) 35 | 'no-var': 'error', // 要求使用 let 或 const 而不是 var 36 | 'no-multiple-empty-lines': ['error', { max: 1 }], // 不允许多个空行 37 | 'prefer-const': 'off', // 使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const 38 | 'no-use-before-define': 'off', // 禁止在 函数/类/变量 定义之前使用它们 39 | 40 | // typeScript (https://typescript-eslint.io/rules) 41 | '@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量 42 | '@typescript-eslint/no-empty-function': 'error', // 禁止空函数 43 | '@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore 44 | '@typescript-eslint/ban-ts-comment': 'error', // 禁止 @ts- 使用注释或要求在指令后进行描述 45 | '@typescript-eslint/no-inferrable-types': 'off', // 可以轻松推断的显式类型可能会增加不必要的冗长 46 | '@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间 47 | '@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型 48 | '@typescript-eslint/ban-types': 'off', // 禁止使用特定类型 49 | '@typescript-eslint/no-var-requires': 'off', // 允许使用 require() 函数导入模块 50 | '@typescript-eslint/no-non-null-assertion': 'off', // 不允许使用后缀运算符的非空断言(!) 51 | 52 | // vue (https://eslint.vuejs.org/rules) 53 | 'vue/script-setup-uses-vars': 'error', // 防止 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-admin-client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "author": { 7 | "name": "Yuimng", 8 | "email": "944307891@qq.com", 9 | "url": "https://github.com/Yuimng" 10 | }, 11 | "license": "MIT", 12 | "homepage": "https://github.com/Yuimng/vue3-admin-client", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/Yuimng/vue3-admin-client.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/Yuimng/vue3-admin-client/issues" 19 | }, 20 | "scripts": { 21 | "dev": "vite", 22 | "build": "vue-tsc && vite build", 23 | "preview": "vite preview", 24 | "lint": "eslint --ext .js,.ts,.vue ./src", 25 | "lint:fix": "eslint --fix --ext .js,.ts,.vue ./src", 26 | "lint:prettier": "prettier --write \"src/**/*.{js,ts,tsx,css,less,scss,vue,html}\"", 27 | "lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix" 28 | }, 29 | "dependencies": { 30 | "@vueuse/core": "^10.9.0", 31 | "axios": "^1.6.8", 32 | "echarts": "^5.5.0", 33 | "echarts-liquidfill": "^3.1.0", 34 | "element-plus": "^2.7.1", 35 | "mitt": "^3.0.1", 36 | "nprogress": "^0.2.0", 37 | "pinia": "^2.1.7", 38 | "pinia-plugin-persistedstate": "^3.2.1", 39 | "screenfull": "^6.0.2", 40 | "vue": "^3.4.21", 41 | "vue-router": "4" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "^20.12.7", 45 | "@types/nprogress": "^0.2.3", 46 | "@typescript-eslint/eslint-plugin": "^7.7.0", 47 | "@typescript-eslint/parser": "^7.7.0", 48 | "@vitejs/plugin-vue": "^5.0.4", 49 | "eslint": "^8.57.0", 50 | "eslint-config-prettier": "^9.1.0", 51 | "eslint-plugin-prettier": "^5.1.3", 52 | "eslint-plugin-vue": "^9.25.0", 53 | "prettier": "^3.2.5", 54 | "rollup-plugin-visualizer": "^5.12.0", 55 | "sass": "^1.75.0", 56 | "stylelint": "^16.6.1", 57 | "stylelint-config-html": "^1.1.0", 58 | "stylelint-config-recess-order": "^5.0.1", 59 | "stylelint-config-recommended-scss": "^14.0.0", 60 | "stylelint-config-recommended-vue": "^1.5.0", 61 | "stylelint-config-standard": "^36.0.0", 62 | "stylelint-config-standard-scss": "^13.1.0", 63 | "typescript": "^5.4.5", 64 | "typescript-eslint": "^7.7.0", 65 | "unplugin-auto-import": "^0.17.5", 66 | "unplugin-vue-components": "^0.26.0", 67 | "vite": "^5.2.0", 68 | "vue-tsc": "^2.0.6" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosInstance, 3 | AxiosRequestConfig, 4 | InternalAxiosRequestConfig, 5 | AxiosResponse 6 | } from 'axios' 7 | import { ResultData } from './interface' 8 | import { useUserStore } from '@/store/modules/user' 9 | 10 | const config = { 11 | // 默认地址请求地址,可在 .env.** 文件中修改 12 | baseURL: import.meta.env.VITE_API_URL as string, 13 | // 设置超时时间 14 | timeout: 30000 15 | } 16 | 17 | class Http { 18 | private static axiosInstance: AxiosInstance 19 | constructor(config: AxiosRequestConfig) { 20 | Http.axiosInstance = axios.create(config) 21 | this.interceptorsRequest() 22 | this.interceptorsResponse() 23 | } 24 | 25 | //请求拦截器 26 | private interceptorsRequest() { 27 | Http.axiosInstance.interceptors.request.use( 28 | (config: InternalAxiosRequestConfig) => { 29 | // 为请求头对象,添加 Token 验证的 Authorization 字段 30 | const userStore = useUserStore() 31 | config.headers.Authorization = userStore.token 32 | return config 33 | }, 34 | (error: string) => { 35 | return Promise.reject(error) 36 | } 37 | ) 38 | } 39 | //响应拦截器 40 | private interceptorsResponse() { 41 | Http.axiosInstance.interceptors.response.use( 42 | async (response: AxiosResponse) => { 43 | const { data } = response 44 | return data 45 | }, 46 | (error: string) => { 47 | return Promise.reject(error) 48 | } 49 | ) 50 | } 51 | get(url: string, params?: object, _object = {}): Promise> { 52 | return Http.axiosInstance.get(url, { params, ..._object }) 53 | } 54 | post(url: string, params?: object | string, _object = {}): Promise> { 55 | return Http.axiosInstance.post(url, params, _object) 56 | } 57 | put(url: string, params?: object, _object = {}): Promise> { 58 | return Http.axiosInstance.put(url, params, _object) 59 | } 60 | delete(url: string, params?: any, _object = {}): Promise> { 61 | return Http.axiosInstance.delete(url, { params, ..._object }) 62 | } 63 | } 64 | 65 | export default new Http(config) 66 | -------------------------------------------------------------------------------- /src/api/interface/index.ts: -------------------------------------------------------------------------------- 1 | // 请求响应参数(不包含data) 2 | export interface Result { 3 | code: number 4 | msg: string 5 | } 6 | 7 | // 请求响应参数(包含data) 8 | export interface ResultData extends Result { 9 | data: T 10 | } 11 | 12 | // 登录模块 13 | export namespace Login { 14 | export interface ReqLoginForm { 15 | username: string 16 | password: string 17 | expires7d: boolean 18 | } 19 | export interface ResLogin { 20 | id: number 21 | username: string 22 | token: string 23 | expires: number 24 | } 25 | export interface Userinfo { 26 | id: number 27 | name: string 28 | username: string | null 29 | email: string | null 30 | phone: string | null 31 | avatar: string | null 32 | remark: string | null 33 | roleId: number 34 | role: string 35 | roleName: string 36 | isSuper: number 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/api/interface/system.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number 3 | name: string 4 | username: string | null 5 | deptId: number 6 | email: string | null 7 | phone: string | null 8 | avatar: string | null 9 | remark: string | null 10 | roleId: number 11 | role: string 12 | roleName: string 13 | } 14 | 15 | export interface Role { 16 | id: number 17 | role: string 18 | roleName: string 19 | isSuper: number 20 | remark: string 21 | createdAt: string 22 | } 23 | 24 | export interface Menu { 25 | id: number 26 | name: string 27 | path: string 28 | component: string | (() => Promise) 29 | redirect?: string 30 | parentId: number 31 | sort: number 32 | meta: MetaProps 33 | createdAt: string 34 | children?: Menu[] 35 | } 36 | 37 | export interface MetaProps { 38 | icon: string 39 | title: string 40 | isLink: boolean 41 | isEnable: boolean 42 | isAffix: boolean 43 | isKeepAlive: boolean 44 | } 45 | 46 | export interface ResultTable { 47 | /** 总条目数 */ 48 | count: number 49 | /** 列表数据 */ 50 | rows: T[] 51 | } 52 | 53 | export interface Department { 54 | id: number 55 | parentId: number 56 | name: string 57 | sort: number 58 | isEnable: number 59 | createdAt: string 60 | children?: Department[] 61 | } 62 | -------------------------------------------------------------------------------- /src/api/modules/login.ts: -------------------------------------------------------------------------------- 1 | import http from '@/api' 2 | import { Login } from '@/api/interface' 3 | import { Menu } from '@/api/interface/system' 4 | 5 | /** 6 | * @description: 登录模块接口列表 7 | */ 8 | 9 | // 用户登录 10 | export const loginApi = (params: Login.ReqLoginForm) => { 11 | return http.post('/login', params) 12 | } 13 | 14 | // 获取用户信息 15 | export const getUserInfoApi = (userId: number) => { 16 | return http.get(`/user/${userId}`) 17 | } 18 | 19 | // 获取菜单列表 20 | export const getAuthMenuListApi = () => { 21 | return http.post(`/menu/list/`) 22 | } 23 | 24 | // 用户退出登录 25 | // export const logoutApi = () => { 26 | // return http.post('/logout') 27 | // } 28 | -------------------------------------------------------------------------------- /src/api/modules/system.ts: -------------------------------------------------------------------------------- 1 | import http from '@/api' 2 | import { Department, Menu, ResultTable, Role, User } from '@/api/interface/system' 3 | export const getUserList = (data: object) => { 4 | return http.post>('/user/list', { ...data }) 5 | } 6 | 7 | export const deleteUser = (id: number) => { 8 | return http.post('/user/delete', { id }) 9 | } 10 | 11 | export const addUser = (data: object) => { 12 | return http.post('/user/add', { ...data }) 13 | } 14 | 15 | export const editUser = (data: object) => { 16 | return http.post('/user/update', { ...data }) 17 | } 18 | 19 | export const getRolesAll = () => { 20 | return http.post('/role/listAll') 21 | } 22 | 23 | export const getRoleList = (data: object) => { 24 | return http.post>('/role/list', { ...data }) 25 | } 26 | 27 | export const deleteRole = (id: number) => { 28 | return http.post('/role/delete', { id }) 29 | } 30 | 31 | export const addRole = (data: object) => { 32 | return http.post('/role/add', { ...data }) 33 | } 34 | 35 | export const editRole = (data: object) => { 36 | return http.post('/role/update', { ...data }) 37 | } 38 | 39 | export const getRoleMenus = (roleId: number) => { 40 | return http.post('/role/useMenus', { roleId }) 41 | } 42 | 43 | export const getMenusAll = () => { 44 | return http.post('/menu/listAll') 45 | } 46 | 47 | export const getMenuList = (data: object) => { 48 | return http.post('/menu/list', { ...data }) 49 | } 50 | 51 | export const deleteMenu = (id: number) => { 52 | return http.post('/menu/delete', { id }) 53 | } 54 | 55 | export const addMenu = (data: object) => { 56 | return http.post('/menu/add', { ...data }) 57 | } 58 | 59 | export const editMenu = (data: object) => { 60 | return http.post('/menu/update', { ...data }) 61 | } 62 | 63 | export const getDepartmentsAll = () => { 64 | return http.post('/department/listAll') 65 | } 66 | 67 | export const deleteDept = (id: number) => { 68 | return http.post('/department/delete', { id }) 69 | } 70 | 71 | export const addDept = (data: object) => { 72 | return http.post('/department/add', { ...data }) 73 | } 74 | 75 | export const editDept = (data: object) => { 76 | return http.post('/department/update', { ...data }) 77 | } 78 | -------------------------------------------------------------------------------- /src/assets/images/403.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 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 | 403 34 | 35 | -------------------------------------------------------------------------------- /src/assets/images/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 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 | 404 34 | 35 | -------------------------------------------------------------------------------- /src/assets/images/login_bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/ECharts/config/index.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/core' 2 | import { BarChart, LineChart, PieChart } from 'echarts/charts' 3 | import { 4 | TitleComponent, 5 | TooltipComponent, 6 | GridComponent, 7 | DatasetComponent, 8 | TransformComponent, 9 | LegendComponent, 10 | ToolboxComponent, 11 | DataZoomComponent 12 | } from 'echarts/components' 13 | import { LabelLayout, UniversalTransition } from 'echarts/features' 14 | import { CanvasRenderer } from 'echarts/renderers' 15 | import type { BarSeriesOption, LineSeriesOption, PieSeriesOption } from 'echarts/charts' 16 | import type { 17 | TitleComponentOption, 18 | TooltipComponentOption, 19 | GridComponentOption, 20 | DatasetComponentOption 21 | } from 'echarts/components' 22 | import type { ComposeOption } from 'echarts/core' 23 | import 'echarts-liquidfill' 24 | 25 | export type ECOption = ComposeOption< 26 | | BarSeriesOption 27 | | LineSeriesOption 28 | | PieSeriesOption 29 | | TitleComponentOption 30 | | TooltipComponentOption 31 | | GridComponentOption 32 | | DatasetComponentOption 33 | > 34 | 35 | echarts.use([ 36 | TitleComponent, 37 | TooltipComponent, 38 | GridComponent, 39 | DatasetComponent, 40 | TransformComponent, 41 | LegendComponent, 42 | ToolboxComponent, 43 | DataZoomComponent, 44 | BarChart, 45 | LineChart, 46 | PieChart, 47 | LabelLayout, 48 | UniversalTransition, 49 | CanvasRenderer 50 | ]) 51 | 52 | export default echarts 53 | -------------------------------------------------------------------------------- /src/components/ECharts/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 114 | -------------------------------------------------------------------------------- /src/components/ErrorMessage/403.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/components/ErrorMessage/404.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/components/ErrorMessage/500.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/components/ErrorMessage/index.scss: -------------------------------------------------------------------------------- 1 | .not-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | height: 100%; 7 | 8 | .not-img { 9 | margin-right: 120px; 10 | } 11 | 12 | .not-detail { 13 | display: flex; 14 | flex-direction: column; 15 | 16 | h2, 17 | h4 { 18 | padding: 0; 19 | margin: 0; 20 | } 21 | 22 | h2 { 23 | font-size: 60px; 24 | color: var(--el-text-color-primary); 25 | } 26 | 27 | h4 { 28 | margin: 30px 0 20px; 29 | font-size: 19px; 30 | font-weight: normal; 31 | color: var(--el-text-color-regular); 32 | } 33 | 34 | .el-button { 35 | width: 100px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/SwitchDark/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | // 全局默认配置项 2 | 3 | // 首页地址(默认) 4 | export const HOME_URL: string = '/dashboard' 5 | 6 | // 登录页地址(默认) 7 | export const LOGIN_URL: string = '/login' 8 | 9 | // 默认主题颜色 10 | export const DEFAULT_PRIMARY: string = '#2254F4' 11 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_PRIMARY } from '@/config' 2 | import { useGlobalStore } from '@/store/modules/global' 3 | import { getDarkColor, getLightColor } from '@/utils/color' 4 | import { storeToRefs } from 'pinia' 5 | 6 | export const useTheme = () => { 7 | const globalStore = useGlobalStore() 8 | const { isDark, primary } = storeToRefs(globalStore) 9 | 10 | // 切换暗黑模式 ==> 同时修改主题颜色、侧边栏、头部颜色 11 | const switchDark = () => { 12 | const html = document.documentElement as HTMLElement 13 | if (isDark.value) html.setAttribute('class', 'dark') 14 | else html.setAttribute('class', '') 15 | changePrimary(primary.value) 16 | } 17 | 18 | // 修改主题颜色 19 | const changePrimary = (val: string | null) => { 20 | if (!val) { 21 | val = DEFAULT_PRIMARY 22 | } 23 | document.documentElement.style.setProperty('--el-color-primary', val) 24 | document.documentElement.style.setProperty( 25 | '--el-color-primary-dark-2', 26 | isDark.value ? `${getLightColor(val, 0.2)}` : `${getDarkColor(val, 0.3)}` 27 | ) 28 | for (let i = 1; i <= 9; i++) { 29 | const primaryColor = isDark.value 30 | ? `${getDarkColor(val, i / 10)}` 31 | : `${getLightColor(val, i / 10)}` 32 | document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, primaryColor) 33 | } 34 | 35 | globalStore.setPrimaryState(val) 36 | } 37 | 38 | const initTheme = () => { 39 | switchDark() 40 | changePrimary(primary.value) 41 | } 42 | 43 | return { 44 | switchDark, 45 | changePrimary, 46 | initTheme 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/layout/components/Header/ToolBarLeft.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /src/layout/components/Header/ToolBarRight.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /src/layout/components/Header/components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 48 | 49 | 63 | -------------------------------------------------------------------------------- /src/layout/components/Header/components/BreadCrumb.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/layout/components/Header/components/CollapseIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /src/layout/components/Header/components/Fullscreen.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | 31 | 37 | -------------------------------------------------------------------------------- /src/layout/components/Header/components/ThemeSetting.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /src/layout/components/Main/index.scss: -------------------------------------------------------------------------------- 1 | .el-main { 2 | box-sizing: border-box; 3 | padding: 10px; 4 | overflow-x: hidden; // 消除横移动画出现x轴滚动条 5 | background-color: var(--el-bg-color-page); 6 | } 7 | -------------------------------------------------------------------------------- /src/layout/components/Main/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 62 | 63 | 66 | -------------------------------------------------------------------------------- /src/layout/components/Menu/SubMenu.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | 33 | 46 | -------------------------------------------------------------------------------- /src/layout/components/Tabs/components/MoreButton.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 47 | 48 | 56 | -------------------------------------------------------------------------------- /src/layout/components/Tabs/index.scss: -------------------------------------------------------------------------------- 1 | .tabs-box { 2 | background-color: var(--el-bg-color); 3 | 4 | .tabs-menu { 5 | position: relative; 6 | width: 100%; 7 | 8 | :deep(.el-tabs) { 9 | .el-tabs__header { 10 | box-sizing: border-box; 11 | height: 40px; 12 | padding: 0 10px; 13 | margin: 0; 14 | 15 | .el-tabs__nav-wrap { 16 | position: absolute; 17 | width: calc(100% - 70px); 18 | 19 | .el-tabs__nav { 20 | box-sizing: border-box; 21 | border: none; 22 | 23 | .el-tabs__item { 24 | border-bottom: 1px solid transparent; 25 | border-left: none; 26 | } 27 | 28 | .el-tabs__item.is-active { 29 | color: var(--el-color-primary) !important; 30 | border-bottom: 3px solid var(--el-color-primary); 31 | } 32 | 33 | .el-tabs__item:hover { 34 | color: unset; 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | .more-btn { 42 | position: absolute; 43 | top: 0; 44 | right: 0; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/layout/components/Tabs/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 83 | 84 | 87 | -------------------------------------------------------------------------------- /src/layout/components/ThemeDrawer/index.scss: -------------------------------------------------------------------------------- 1 | .theme-item { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | padding: 0 5px; 6 | margin: 14px 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/layout/components/ThemeDrawer/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | 47 | 50 | -------------------------------------------------------------------------------- /src/layout/index.scss: -------------------------------------------------------------------------------- 1 | .el-container { 2 | width: 100%; 3 | height: 100%; 4 | 5 | :deep(.el-aside) { 6 | width: auto; 7 | border-right: 1px solid var(--el-border-color-light); 8 | 9 | .aside { 10 | display: flex; 11 | flex-direction: column; 12 | height: 100%; 13 | transition: width 0.3s ease; 14 | 15 | .el-scrollbar { 16 | height: calc(100% - 55px); 17 | 18 | .el-menu { 19 | width: 100%; 20 | overflow-x: hidden; 21 | border-right: none; 22 | } 23 | } 24 | 25 | .logo { 26 | box-sizing: border-box; 27 | display: flex; 28 | align-items: center; 29 | justify-content: left; 30 | height: 55px; 31 | padding-left: 20px; 32 | 33 | .logo-img { 34 | position: relative; 35 | left: -20000px; /* 移动到窗口外很远的地方,也可以用transform: translateY(-20000px); */ 36 | width: 28px; 37 | height: 28px; 38 | margin-right: 6px; 39 | filter: drop-shadow(var(--el-color-primary) 20000px 0); 40 | } 41 | 42 | .logo-text { 43 | font-size: 22px; 44 | font-weight: 600; 45 | white-space: nowrap; 46 | } 47 | } 48 | } 49 | } 50 | 51 | .el-header { 52 | box-sizing: border-box; 53 | display: flex; 54 | align-items: center; 55 | justify-content: space-between; 56 | height: 55px; 57 | padding: 0 15px; 58 | border-bottom: 1px solid var(--el-border-color-light); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 57 | 58 | 61 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import App from './App.vue' 4 | 5 | // reset style sheet 6 | import '@/styles/reset.scss' 7 | // CSS common style sheet 8 | import '@/styles/common.scss' 9 | // iconfont 10 | import '@/styles/iconfont.scss' 11 | // el-message el-message-box css 12 | import 'element-plus/es/components/message-box/style/css' 13 | import 'element-plus/es/components/message/style/css' 14 | // el-tree css 15 | import 'element-plus/es/components/tree/style/css' 16 | // element dark css 17 | import 'element-plus/theme-chalk/dark/css-vars.css' 18 | // custom element dark css 19 | import '@/styles/element-dark.scss' 20 | 21 | import router from '@/router' 22 | 23 | import pinia from '@/store' 24 | 25 | createApp(App).use(router).use(pinia).mount('#app') 26 | -------------------------------------------------------------------------------- /src/router/dynamicRouter.ts: -------------------------------------------------------------------------------- 1 | import router from '@/router/index' 2 | import { useAuthStore } from '@/store/modules/auth' 3 | import { RouteRecordRaw } from 'vue-router' 4 | 5 | // 引入 views 文件夹下所有 vue 文件 6 | const modules = import.meta.glob('@/views/**/*.vue') 7 | 8 | export const initDynamicRouter = async () => { 9 | const authStore = useAuthStore() 10 | try { 11 | // 添加动态路由 flatMenuListGet 递归将菜单全部平铺 12 | authStore.flatMenuListGet.forEach((item) => { 13 | if (item.children && item.children.length > 0) { 14 | item.redirect = item.children[0].path 15 | } 16 | item.children && delete item.children 17 | if (item.component && typeof item.component == 'string') { 18 | item.component = modules['/src/views' + item.component + '.vue'] 19 | } 20 | // 添加路由 21 | router.addRoute('layout', item as unknown as RouteRecordRaw) 22 | }) 23 | } catch (error) { 24 | return Promise.reject(error) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { HOME_URL, LOGIN_URL } from '@/config' 2 | import { useAuthStore } from '@/store/modules/auth' 3 | import { useUserStore } from '@/store/modules/user' 4 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' 5 | import { initDynamicRouter } from './dynamicRouter' 6 | import NProgress from '@/utils/nprogress' 7 | 8 | const routes: RouteRecordRaw[] = [ 9 | { 10 | path: '/', 11 | redirect: HOME_URL 12 | }, 13 | { 14 | path: '/login', 15 | name: 'login', 16 | component: () => import('@/views/login/index.vue') 17 | }, 18 | { 19 | path: '/layout', 20 | redirect: HOME_URL, // 重定向主页 21 | name: 'layout', 22 | component: () => import('@/layout/index.vue'), 23 | children: [ 24 | // -----非全屏页面动态引入----- 25 | ] 26 | }, 27 | 28 | // -----全屏页面引入----- 29 | 30 | // 错误页面 31 | { 32 | path: '/403', 33 | name: '403', 34 | component: () => import('@/components/ErrorMessage/403.vue'), 35 | meta: { 36 | title: '403页面' 37 | } 38 | }, 39 | { 40 | path: '/404', 41 | name: '404', 42 | component: () => import('@/components/ErrorMessage/404.vue'), 43 | meta: { 44 | title: '404页面' 45 | } 46 | }, 47 | { 48 | path: '/500', 49 | name: '500', 50 | component: () => import('@/components/ErrorMessage/500.vue'), 51 | meta: { 52 | title: '500页面' 53 | } 54 | }, 55 | { 56 | path: '/:pathMatch(.*)*', 57 | component: () => import('@/components/ErrorMessage/404.vue') 58 | } 59 | ] 60 | 61 | const router = createRouter({ 62 | history: createWebHistory(), 63 | routes 64 | }) 65 | 66 | /** 67 | * @description 路由拦截 beforeEach 68 | * */ 69 | router.beforeEach(async (to, from, next) => { 70 | const userStore = useUserStore() 71 | const authStore = useAuthStore() 72 | // nprogress 启动 73 | NProgress.start() 74 | 75 | // 判断是访问登陆页,有 Token 就在当前页面,没有 Token 重置路由到登陆页 76 | if (to.path.toLocaleLowerCase() === LOGIN_URL) { 77 | if (userStore.token && isTokenValid(userStore.expires)) return next(from.fullPath) // 保持原页面 78 | resetRouter() // 清除已加载动态路由 79 | return next() 80 | } 81 | 82 | // 判断是否有 Token,没有或者token过期 重定向到 login 页面 83 | if (!userStore.token || !isTokenValid(userStore.expires)) 84 | return next({ path: LOGIN_URL, replace: true }) 85 | 86 | // 如果没有菜单列表,就重新请求菜单列表并添加动态路由 87 | if (!authStore.authMenuListGet.length) { 88 | const flag = await authStore.getAuthMenuList() 89 | if (flag) { 90 | // 动态加载路由 91 | await initDynamicRouter() 92 | return next({ ...to, replace: true }) 93 | } else { 94 | return next({ path: LOGIN_URL, replace: true }) 95 | } 96 | } 97 | 98 | next() 99 | }) 100 | 101 | /** 102 | * @description 重置路由(全部清除) 103 | * */ 104 | export const resetRouter = () => { 105 | const authStore = useAuthStore() 106 | authStore.flatMenuListGet.forEach((route) => { 107 | const { name } = route 108 | if (name && router.hasRoute(name)) router.removeRoute(name) 109 | }) 110 | } 111 | 112 | /** 113 | * token 是否有效 114 | * @param time token 过期时间 115 | * @returns boolean true:有效 / false:无效 116 | */ 117 | export const isTokenValid = (time: number) => { 118 | return time * 1000 > Date.now() 119 | } 120 | 121 | /** 122 | * @description 路由跳转错误 123 | * */ 124 | router.onError((error) => { 125 | NProgress.done() 126 | console.warn('路由错误', error.message) 127 | }) 128 | 129 | /** 130 | * @description 路由跳转结束 131 | * */ 132 | router.afterEach(() => { 133 | NProgress.done() 134 | }) 135 | 136 | export default router 137 | -------------------------------------------------------------------------------- /src/store/helper/persist.ts: -------------------------------------------------------------------------------- 1 | import { PersistedStateOptions } from 'pinia-plugin-persistedstate' 2 | 3 | /** 4 | * @description pinia 持久化参数配置 5 | * @param {String} key 存储到持久化的 name 6 | * @param {Array} paths 需要持久化的 state name 7 | * @return persist 8 | * */ 9 | const piniaPersistConfig = (key: string, paths?: string[]) => { 10 | const persist: PersistedStateOptions = { 11 | key, 12 | storage: localStorage, 13 | // storage: sessionStorage, 14 | paths 15 | } 16 | return persist 17 | } 18 | 19 | export default piniaPersistConfig 20 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 3 | 4 | const pinia = createPinia() 5 | // pinia persist 6 | pinia.use(piniaPluginPersistedstate) 7 | 8 | export default pinia 9 | -------------------------------------------------------------------------------- /src/store/interface/index.ts: -------------------------------------------------------------------------------- 1 | import { Menu } from '@/api/interface/system' 2 | 3 | /* GlobalState */ 4 | export interface GlobalState { 5 | isCollapse: boolean 6 | isDark: boolean 7 | breadcrumb: boolean 8 | primary: string 9 | } 10 | 11 | /* UserState */ 12 | export interface UserState { 13 | token: string 14 | expires: number 15 | userInfo: { 16 | id: number 17 | name: string 18 | username: string | null 19 | email: string | null 20 | phone: string | null 21 | avatar: string | null 22 | remark: string | null 23 | roleId: number 24 | role: string 25 | roleName: string 26 | isSuper: number 27 | } 28 | } 29 | 30 | /* AuthState */ 31 | export interface AuthState { 32 | routeName: string 33 | authButtonList: { 34 | [key: string]: string[] 35 | } 36 | authMenuList: Menu[] 37 | } 38 | 39 | /* tabsMenuProps */ 40 | export interface TabsMenuProps { 41 | icon: string 42 | title: string 43 | path: string 44 | name: string 45 | close: boolean 46 | isKeepAlive: boolean 47 | } 48 | 49 | /* TabsState */ 50 | export interface TabsState { 51 | tabsMenuList: TabsMenuProps[] 52 | } 53 | 54 | /* keepAliveState */ 55 | export interface KeepAliveState { 56 | keepAliveNames: string[] 57 | } 58 | -------------------------------------------------------------------------------- /src/store/modules/auth.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { AuthState } from '@/store/interface' 3 | import { getAuthMenuListApi } from '@/api/modules/login' 4 | import { getAllBreadcrumbList, getFlatMenuList, getShowMenuList } from '@/utils' 5 | 6 | // import { useUserStore } from '@/store/modules/user' 7 | // import router from '@/router' 8 | // import { LOGIN_URL } from '@/config' 9 | import { ElMessage } from 'element-plus' 10 | 11 | export const useAuthStore = defineStore({ 12 | id: 'ym-auth', 13 | state: (): AuthState => ({ 14 | // 按钮权限列表 15 | authButtonList: {}, 16 | // 菜单权限列表 17 | authMenuList: [], 18 | // 当前页面的 router name,用来做按钮权限筛选 19 | routeName: '' 20 | }), 21 | getters: { 22 | // 菜单权限列表 ==> 这里的菜单没有经过任何处理 23 | authMenuListGet: (state) => state.authMenuList, 24 | // 菜单权限列表 ==> 扁平化之后的一维数组菜单,主要用来添加动态路由 25 | flatMenuListGet: (state) => getFlatMenuList(state.authMenuList), 26 | // 菜单权限列表 ==> 左侧菜单栏渲染,需要剔除 isEnable === 0 27 | showMenuListGet: (state) => getShowMenuList(state.authMenuList), 28 | // 递归处理后的所有面包屑导航列表 29 | breadcrumbListGet: (state) => getAllBreadcrumbList(state.authMenuList) 30 | }, 31 | actions: { 32 | async getAuthMenuList() { 33 | try { 34 | const res = await getAuthMenuListApi() 35 | if (res.code === 200) { 36 | if (res.data.length === 0) { 37 | // 菜单列表为空,则跳转到登录页 38 | ElMessage.error('角色菜单列表为空,联系管理员') 39 | return false 40 | } else { 41 | // 成功获取菜单列表 42 | this.authMenuList = res.data 43 | return true 44 | } 45 | } else { 46 | ElMessage.error(res.msg) 47 | // 获取菜单失败 则跳转到登录页 48 | return false 49 | } 50 | } catch (error) { 51 | console.log(error) 52 | return false 53 | } 54 | } 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /src/store/modules/global.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { GlobalState } from '@/store/interface' 3 | import piniaPersistConfig from '@/store/helper/persist' 4 | import { DEFAULT_PRIMARY } from '@/config' 5 | 6 | export const useGlobalStore = defineStore({ 7 | id: 'ym-global', 8 | state: (): GlobalState => ({ 9 | isCollapse: false, 10 | isDark: false, 11 | breadcrumb: true, 12 | primary: DEFAULT_PRIMARY 13 | }), 14 | getters: {}, 15 | actions: { 16 | setCollapseState(state: boolean) { 17 | this.isCollapse = state 18 | }, 19 | setBreadcrumbState(state: boolean) { 20 | this.breadcrumb = state 21 | }, 22 | setPrimaryState(color: string) { 23 | this.primary = color 24 | } 25 | }, 26 | persist: piniaPersistConfig('ym-global') 27 | }) 28 | -------------------------------------------------------------------------------- /src/store/modules/keepAlive.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { KeepAliveState } from '@/store/interface' 3 | 4 | export const useKeepAliveStore = defineStore({ 5 | id: 'ym-keepAlive', 6 | state: (): KeepAliveState => ({ 7 | keepAliveNames: [] 8 | }), 9 | getters: {}, 10 | actions: { 11 | async addKeepAlive(name: string) { 12 | if (this.keepAliveNames.includes(name)) return 13 | this.keepAliveNames.push(name) 14 | }, 15 | async removeKeepAlive(name: string) { 16 | this.keepAliveNames = this.keepAliveNames.filter((item) => item !== name) 17 | }, 18 | async setKeepAliveNames(names: string[] = []) { 19 | this.keepAliveNames = names 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/store/modules/tabs.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { TabsMenuProps, TabsState } from '@/store/interface' 3 | import piniaPersistConfig from '@/store/helper/persist' 4 | import router from '@/router' 5 | import { useKeepAliveStore } from '@/store/modules/keepAlive' 6 | 7 | const keepAliveStore = useKeepAliveStore() 8 | 9 | export const useTabsStore = defineStore({ 10 | id: 'ym-tabs', 11 | state: (): TabsState => ({ 12 | tabsMenuList: [] 13 | }), 14 | getters: {}, 15 | actions: { 16 | // 添加tab 17 | async addTab(tabItem: TabsMenuProps) { 18 | if (this.tabsMenuList.every((item) => item.path !== tabItem.path)) { 19 | this.tabsMenuList.push(tabItem) 20 | } 21 | // keepalive 22 | if (!keepAliveStore.keepAliveNames.includes(tabItem.path) && tabItem.isKeepAlive) { 23 | keepAliveStore.addKeepAlive(tabItem.path) 24 | } 25 | }, 26 | // 移除tab 27 | async removeTab(tabPath: string, isCurrent: boolean = true) { 28 | if (isCurrent) { 29 | this.tabsMenuList.forEach((item, index) => { 30 | if (item.path !== tabPath) return 31 | const nextTab = this.tabsMenuList[index + 1] || this.tabsMenuList[index - 1] 32 | if (!nextTab) return 33 | router.push(nextTab.path) 34 | }) 35 | } 36 | 37 | // keepalive 38 | const tabItem = this.tabsMenuList.find((item) => item.path === tabPath) 39 | tabItem?.isKeepAlive && keepAliveStore.removeKeepAlive(tabItem.path) 40 | 41 | this.tabsMenuList = this.tabsMenuList.filter((item) => item.path !== tabPath) 42 | }, 43 | async closeTabsOnSide(tabPath: string, type: 'left' | 'right') { 44 | const currentIndex = this.tabsMenuList.findIndex((item) => item.path === tabPath) 45 | if (currentIndex !== -1) { 46 | const range = 47 | type === 'left' ? [0, currentIndex] : [currentIndex + 1, this.tabsMenuList.length] 48 | this.tabsMenuList = this.tabsMenuList.filter((item, index) => { 49 | return index < range[0] || index >= range[1] || !item.close 50 | }) 51 | } 52 | // keepAlive 53 | const keepAliveList = this.tabsMenuList.filter((item) => item.isKeepAlive) 54 | keepAliveStore.setKeepAliveNames(keepAliveList.map((item) => item.path)) 55 | }, 56 | // 关闭多个tab 57 | async closeMultipleTab(tabPath?: string) { 58 | this.tabsMenuList = this.tabsMenuList.filter((item) => { 59 | return item.path === tabPath || !item.close 60 | }) 61 | // keepAlive 62 | const keepAliveList = this.tabsMenuList.filter((item) => item.isKeepAlive) 63 | keepAliveStore.setKeepAliveNames(keepAliveList.map((item) => item.path)) 64 | }, 65 | 66 | async setTabs(tabsMenuList: TabsMenuProps[]) { 67 | this.tabsMenuList = tabsMenuList 68 | } 69 | }, 70 | persist: piniaPersistConfig('ym-tabs') 71 | }) 72 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { UserState } from '@/store/interface' 3 | import piniaPersistConfig from '@/store/helper/persist' 4 | import { getUserInfoApi } from '@/api/modules/login' 5 | 6 | export const useUserStore = defineStore({ 7 | id: 'ym-user', 8 | state: (): UserState => ({ 9 | token: '', 10 | expires: 0, 11 | userInfo: { 12 | id: 0, 13 | name: '', 14 | username: null, 15 | email: null, 16 | phone: null, 17 | avatar: null, 18 | remark: null, 19 | roleId: 0, 20 | role: '', 21 | roleName: '', 22 | isSuper: 0 23 | } 24 | }), 25 | getters: {}, 26 | actions: { 27 | // Set Token 28 | setTokenWithExpires(token: string, expires: number) { 29 | this.token = token 30 | this.expires = expires 31 | }, 32 | // Set setUserInfo 33 | async getUserInfo(userId: number) { 34 | if (userId) { 35 | const res = await getUserInfoApi(userId) 36 | if (res.code === 200) { 37 | this.userInfo = res.data 38 | } 39 | } 40 | } 41 | }, 42 | persist: piniaPersistConfig('ym-user') 43 | }) 44 | -------------------------------------------------------------------------------- /src/styles/common.scss: -------------------------------------------------------------------------------- 1 | /* custom card */ 2 | .card { 3 | box-sizing: border-box; 4 | padding: 18px; 5 | overflow-x: hidden; 6 | background-color: var(--el-bg-color); 7 | border: 1px solid var(--el-border-color-light); 8 | border-radius: 6px; 9 | box-shadow: 0 0 12px rgb(0 0 0 / 5%); 10 | } 11 | 12 | /* flex */ 13 | .flx-center { 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | } 18 | 19 | /* clearfix */ 20 | .clearfix::after { 21 | display: block; 22 | height: 0; 23 | overflow: hidden; 24 | clear: both; 25 | content: ''; 26 | } 27 | 28 | /* 文字单行省略号 */ 29 | .sle { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | white-space: nowrap; 33 | } 34 | 35 | /* 文字多行省略号 */ 36 | .mle { 37 | display: -webkit-box; 38 | overflow: hidden; 39 | -webkit-box-orient: vertical; 40 | -webkit-line-clamp: 2; 41 | line-clamp: 2; 42 | } 43 | 44 | /* 文字多了自动換行 */ 45 | .break-word { 46 | word-break: break-all; 47 | word-wrap: break-word; 48 | } 49 | 50 | /* fade-transform */ 51 | .fade-transform-leave-active, 52 | .fade-transform-enter-active { 53 | transition: all 0.2s; 54 | } 55 | 56 | .fade-transform-enter-from { 57 | opacity: 0; 58 | transition: all 0.2s; 59 | transform: translateX(-30px); 60 | } 61 | 62 | .fade-transform-leave-to { 63 | display: none; // 解决标签全部关闭时出现动画闪烁问题 64 | opacity: 0; 65 | transition: all 0.2s; 66 | transform: translateX(30px); 67 | } 68 | 69 | /* 滚动条 */ 70 | ::-webkit-scrollbar { 71 | width: 6px; 72 | height: 6px; 73 | } 74 | 75 | ::-webkit-scrollbar-thumb { 76 | background-color: var(--el-border-color-darker); 77 | border-radius: 20px; 78 | } 79 | 80 | /* nprogress */ 81 | #nprogress .bar { 82 | background: var(--el-color-primary) !important; 83 | } 84 | 85 | #nprogress .spinner-icon { 86 | border-top-color: var(--el-color-primary) !important; 87 | border-left-color: var(--el-color-primary) !important; 88 | } 89 | 90 | #nprogress .peg { 91 | box-shadow: 92 | 0 0 10px var(--el-color-primary), 93 | 0 0 5px var(--el-color-primary) !important; 94 | } 95 | 96 | /* 外边距、内边距全局样式 */ 97 | @for $i from 0 through 100 { 98 | .mt#{$i} { 99 | margin-top: #{$i}px !important; 100 | } 101 | .mr#{$i} { 102 | margin-right: #{$i}px !important; 103 | } 104 | .mb#{$i} { 105 | margin-bottom: #{$i}px !important; 106 | } 107 | .ml#{$i} { 108 | margin-left: #{$i}px !important; 109 | } 110 | .pt#{$i} { 111 | padding-top: #{$i}px !important; 112 | } 113 | .pr#{$i} { 114 | padding-right: #{$i}px !important; 115 | } 116 | .pb#{$i} { 117 | padding-bottom: #{$i}px !important; 118 | } 119 | .pl#{$i} { 120 | padding-left: #{$i}px !important; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/styles/element-dark.scss: -------------------------------------------------------------------------------- 1 | /* 自定义 element 暗黑模式 */ 2 | html.dark { 3 | /* wangEditor */ 4 | --w-e-toolbar-color: #eeeeee; 5 | --w-e-toolbar-bg-color: #141414; 6 | --w-e-textarea-bg-color: #141414; 7 | --w-e-textarea-color: #eeeeee; 8 | --w-e-toolbar-active-bg-color: #464646; 9 | --w-e-toolbar-border-color: var(--el-border-color-darker); 10 | 11 | .w-e-bar-item button:hover, 12 | .w-e-menu-tooltip-v5::before { 13 | color: #eeeeee; 14 | } 15 | 16 | /* login */ 17 | .login-container { 18 | background-color: #191919 !important; 19 | 20 | .login-box { 21 | background-color: rgb(0 0 0 / 80%) !important; 22 | 23 | .login-form { 24 | box-shadow: rgb(255 255 255 / 12%) 0 2px 10px 2px !important; 25 | 26 | .logo-text { 27 | color: var(--el-text-color-primary) !important; 28 | } 29 | } 30 | } 31 | } 32 | 33 | /* 下拉和菜单 */ 34 | .el-dropdown-menu__item:hover, .el-sub-menu__title:hover, .el-menu-item:hover { 35 | color: var(--el-color-primary); 36 | background-color: #000000; 37 | } 38 | 39 | .el-menu-item.is-active { 40 | color: #ffffff; 41 | background-color: var(--el-color-primary); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/iconfont.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | /* stylelint-disable-next-line font-family-name-quotes */ 3 | font-family: 'iconfont'; /* Project id 4523094 */ 4 | src: url('//at.alicdn.com/t/c/font_4523094_txalceq91m.woff2?t=1729656418798') format('woff2'), 5 | url('//at.alicdn.com/t/c/font_4523094_txalceq91m.woff?t=1729656418798') format('woff'), 6 | url('//at.alicdn.com/t/c/font_4523094_txalceq91m.ttf?t=1729656418798') format('truetype'); 7 | } 8 | 9 | .iconfont { 10 | font-family: iconfont !important; 11 | font-size: 16px; 12 | font-style: normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .toolbar-icon.iconfont { 18 | font-size: 22px; 19 | } 20 | 21 | .icon-user::before { 22 | content: '\e680'; 23 | } 24 | 25 | .icon-lock::before { 26 | content: '\e66c'; 27 | } 28 | 29 | .icon-more-outline::before { 30 | content: '\e66e'; 31 | } 32 | 33 | .icon-yingyong::before { 34 | content: '\e6ae'; 35 | } 36 | 37 | .icon-zhankai::before { 38 | content: '\e6b2'; 39 | } 40 | 41 | .icon-shouqi::before { 42 | content: '\e6b3'; 43 | } 44 | 45 | .icon-fangda::before { 46 | content: '\e6bb'; 47 | } 48 | 49 | .icon-suoxiao::before { 50 | content: '\e6bc'; 51 | } 52 | 53 | .icon-lingdang::before { 54 | content: '\e6a2'; 55 | } 56 | 57 | .icon-taiyang::before { 58 | content: '\e6b9'; 59 | } 60 | 61 | .icon-yueliang::before { 62 | content: '\e6ba'; 63 | } 64 | 65 | .icon-yifu::before { 66 | content: '\e6bf'; 67 | } 68 | 69 | .icon-tuichu::before { 70 | content: '\e6ad'; 71 | } 72 | 73 | .icon-quanjia::before { 74 | content: '\e6be'; 75 | } 76 | 77 | .icon-shanchu::before { 78 | content: '\e6a3'; 79 | } 80 | 81 | .icon-xiugai::before { 82 | content: '\e6af'; 83 | } 84 | 85 | .icon-sousuo::before { 86 | content: '\e6a6'; 87 | } 88 | 89 | .icon-shangchuan::before { 90 | content: '\e6a7'; 91 | } 92 | 93 | .icon-xiazai::before { 94 | content: '\e6aa'; 95 | } 96 | 97 | .icon-zhuye::before { 98 | content: '\e6b0'; 99 | } 100 | 101 | .icon-caidan::before { 102 | content: '\e6b1'; 103 | } 104 | 105 | .icon-tongji::before { 106 | content: '\e6a9'; 107 | } 108 | 109 | .icon-shuju::before { 110 | content: '\e6bd'; 111 | } 112 | 113 | .icon-shezhi::before { 114 | content: '\e6a5'; 115 | } 116 | 117 | .icon-paihang::before { 118 | content: '\e6c0'; 119 | } 120 | 121 | .icon-suo::before { 122 | content: '\e6b5'; 123 | } 124 | 125 | .icon-tishi::before { 126 | content: '\e6b7'; 127 | } 128 | 129 | .icon-attachment::before { 130 | content: '\e7e1'; 131 | } 132 | 133 | .icon-close-circle::before { 134 | content: '\e77d'; 135 | } 136 | 137 | .icon-minus-circle::before { 138 | content: '\e780'; 139 | } 140 | 141 | .icon-fold-closed::before { 142 | content: '\e6c1'; 143 | } 144 | 145 | .icon-d-arrow-left::before { 146 | content: '\e668'; 147 | } 148 | 149 | .icon-d-arrow-right::before { 150 | content: '\e669'; 151 | } 152 | 153 | .icon-down::before { 154 | content: '\e6b8'; 155 | } 156 | 157 | .icon-sync::before { 158 | content: '\e786'; 159 | } 160 | 161 | .icon-fangke::before { 162 | content: '\e7e2'; 163 | } 164 | 165 | .icon-fangche::before { 166 | content: '\e656'; 167 | } 168 | -------------------------------------------------------------------------------- /src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | /* Reset style sheet */ 2 | html, 3 | body, 4 | #app { 5 | width: 100%; 6 | height: 100%; 7 | padding: 0; 8 | margin: 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/var.scss: -------------------------------------------------------------------------------- 1 | /* global css variable */ 2 | $primary-color: var(--el-color-primary); 3 | -------------------------------------------------------------------------------- /src/typings/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/typings/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | 403: typeof import('./../components/ErrorMessage/403.vue')['default'] 11 | 404: typeof import('./../components/ErrorMessage/404.vue')['default'] 12 | 500: typeof import('./../components/ErrorMessage/500.vue')['default'] 13 | ECharts: typeof import('./../components/ECharts/index.vue')['default'] 14 | ElAside: typeof import('element-plus/es')['ElAside'] 15 | ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] 16 | ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] 17 | ElButton: typeof import('element-plus/es')['ElButton'] 18 | ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] 19 | ElCol: typeof import('element-plus/es')['ElCol'] 20 | ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] 21 | ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] 22 | ElContainer: typeof import('element-plus/es')['ElContainer'] 23 | ElDialog: typeof import('element-plus/es')['ElDialog'] 24 | ElDrawer: typeof import('element-plus/es')['ElDrawer'] 25 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 26 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] 27 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 28 | ElForm: typeof import('element-plus/es')['ElForm'] 29 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 30 | ElHeader: typeof import('element-plus/es')['ElHeader'] 31 | ElInput: typeof import('element-plus/es')['ElInput'] 32 | ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] 33 | ElMain: typeof import('element-plus/es')['ElMain'] 34 | ElMenu: typeof import('element-plus/es')['ElMenu'] 35 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] 36 | ElOption: typeof import('element-plus/es')['ElOption'] 37 | ElPagination: typeof import('element-plus/es')['ElPagination'] 38 | ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'] 39 | ElRadio: typeof import('element-plus/es')['ElRadio'] 40 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 41 | ElRow: typeof import('element-plus/es')['ElRow'] 42 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 43 | ElSelect: typeof import('element-plus/es')['ElSelect'] 44 | ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] 45 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 46 | ElTable: typeof import('element-plus/es')['ElTable'] 47 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 48 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 49 | ElTabs: typeof import('element-plus/es')['ElTabs'] 50 | ElTag: typeof import('element-plus/es')['ElTag'] 51 | ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] 52 | RouterLink: typeof import('vue-router')['RouterLink'] 53 | RouterView: typeof import('vue-router')['RouterView'] 54 | SwitchDark: typeof import('./../components/SwitchDark/index.vue')['default'] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | 3 | /** 4 | * @description hex颜色转rgb颜色 5 | * @param {String} str 颜色值字符串 6 | * @returns {String} 返回处理后的颜色值 7 | */ 8 | export function hexToRgb(str: any) { 9 | let hexs: any = '' 10 | let reg = /^\#?[0-9A-Fa-f]{6}$/ 11 | if (!reg.test(str)) return ElMessage.warning('输入错误的hex') 12 | str = str.replace('#', '') 13 | hexs = str.match(/../g) 14 | for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16) 15 | return hexs 16 | } 17 | 18 | /** 19 | * @description rgb颜色转Hex颜色 20 | * @param {*} r 代表红色 21 | * @param {*} g 代表绿色 22 | * @param {*} b 代表蓝色 23 | * @returns {String} 返回处理后的颜色值 24 | */ 25 | export function rgbToHex(r: any, g: any, b: any) { 26 | let reg = /^\d{1,3}$/ 27 | if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return ElMessage.warning('输入错误的rgb颜色值') 28 | let hexs = [r.toString(16), g.toString(16), b.toString(16)] 29 | for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}` 30 | return `#${hexs.join('')}` 31 | } 32 | 33 | /** 34 | * @description 加深颜色值 35 | * @param {String} color 颜色值字符串 36 | * @param {Number} level 加深的程度,限0-1之间 37 | * @returns {String} 返回处理后的颜色值 38 | */ 39 | export function getDarkColor(color: string, level: number) { 40 | let reg = /^\#?[0-9A-Fa-f]{6}$/ 41 | if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值') 42 | let rgb = hexToRgb(color) 43 | for (let i = 0; i < 3; i++) rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level)) 44 | return rgbToHex(rgb[0], rgb[1], rgb[2]) 45 | } 46 | 47 | /** 48 | * @description 变浅颜色值 49 | * @param {String} color 颜色值字符串 50 | * @param {Number} level 加深的程度,限0-1之间 51 | * @returns {String} 返回处理后的颜色值 52 | */ 53 | export function getLightColor(color: string, level: number) { 54 | let reg = /^\#?[0-9A-Fa-f]{6}$/ 55 | if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值') 56 | let rgb = hexToRgb(color) 57 | for (let i = 0; i < 3; i++) rgb[i] = Math.round(255 * level + rgb[i] * (1 - level)) 58 | return rgbToHex(rgb[0], rgb[1], rgb[2]) 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Department, Menu } from '@/api/interface/system' 2 | 3 | /** 4 | * @description 使用递归扁平化菜单,方便添加动态路由 5 | * @param {Array} menuList 菜单列表 6 | * @returns {Array} 7 | */ 8 | export function getFlatMenuList(menuList: Menu[]): Menu[] { 9 | let newMenuList: Menu[] = JSON.parse(JSON.stringify(menuList)) 10 | return newMenuList.flatMap((item) => [ 11 | item, 12 | ...(item.children ? getFlatMenuList(item.children) : []) 13 | ]) 14 | } 15 | 16 | /** 17 | * @description 使用递归过滤出需要渲染在左侧菜单的列表 (需剔除 isEnable == false 的菜单) 18 | * @param {Array} menuList 菜单列表 19 | * @returns {Array} 20 | * */ 21 | export function getShowMenuList(menuList: Menu[]) { 22 | let newMenuList: Menu[] = JSON.parse(JSON.stringify(menuList)) 23 | return newMenuList.filter((item) => { 24 | item.children?.length && (item.children = getShowMenuList(item.children)) 25 | return item.meta.isEnable 26 | }) 27 | } 28 | 29 | /** 30 | * @description 使用递归找出所有面包屑存储到 pinia/vuex 中 31 | * @param {Array} menuList 菜单列表 32 | * @param {Array} parent 父级菜单 33 | * @param {Object} result 处理后的结果 34 | * @returns {Object} 35 | */ 36 | export function getAllBreadcrumbList( 37 | menuList: Menu[], 38 | parent = [], 39 | result: { [key: string]: any } = {} 40 | ) { 41 | for (const item of menuList) { 42 | result[item.path] = [...parent, item] // 本路径:父级对象+自己 父级递归 43 | if (item.children) getAllBreadcrumbList(item.children, result[item.path], result) 44 | } 45 | return result 46 | } 47 | 48 | export interface MenuOption { 49 | label: string 50 | value: number 51 | children: MenuOption[] 52 | } 53 | 54 | export function getTreeMenuOptions(menuList: Menu[]): MenuOption[] { 55 | return menuList.map((item) => { 56 | return { 57 | label: item.meta.title, 58 | value: item.id, 59 | children: item.children ? getTreeMenuOptions(item.children) : [] 60 | } 61 | }) 62 | } 63 | 64 | export interface MenuTree { 65 | id: number 66 | label: string 67 | children?: MenuTree[] 68 | } 69 | 70 | export function getTreeMenu(menuList: Menu[]): MenuTree[] { 71 | return menuList.map((item) => { 72 | const treeItem: MenuTree = { 73 | id: item.id, 74 | label: item.meta.title 75 | } 76 | 77 | if (item.children && item.children.length > 0) { 78 | treeItem.children = getTreeMenu(item.children) 79 | } 80 | 81 | return treeItem 82 | }) 83 | } 84 | 85 | export function findNodeById(tree: Department[], id: number): Department | undefined { 86 | for (const node of tree) { 87 | if (node.id === id) { 88 | return node 89 | } 90 | if (node.children && node.children.length > 0) { 91 | const foundNode = findNodeById(node.children, id) 92 | if (foundNode) { 93 | return foundNode 94 | } 95 | } 96 | } 97 | return undefined 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/mittBus.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | const mittBus = mitt() 4 | 5 | export default mittBus 6 | -------------------------------------------------------------------------------- /src/utils/nprogress.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | import 'nprogress/nprogress.css' 3 | 4 | NProgress.configure({ 5 | easing: 'ease', // 动画方式 6 | speed: 500, // 递增进度条的速度 7 | showSpinner: true, // 是否显示加载ico 8 | trickleSpeed: 200, // 自动递增间隔 9 | minimum: 0.3 // 初始化时的最小百分比 10 | }) 11 | 12 | export default NProgress 13 | -------------------------------------------------------------------------------- /src/views/about/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/views/about/index.scss -------------------------------------------------------------------------------- /src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/views/dashboard/components/carChart.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 53 | 59 | -------------------------------------------------------------------------------- /src/views/dashboard/components/vistorChart.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 118 | 124 | -------------------------------------------------------------------------------- /src/views/dashboard/index.scss: -------------------------------------------------------------------------------- 1 | .gather-card { 2 | box-sizing: border-box; 3 | display: flex; 4 | align-items: center; 5 | justify-content: space-between; 6 | width: 100%; 7 | padding: 24px; 8 | border-radius: 8px; 9 | 10 | .card-info { 11 | .card-title { 12 | font-size: 14px; 13 | } 14 | 15 | .card-text { 16 | margin: 0; 17 | margin-top: 10px; 18 | font-size: 32px; 19 | } 20 | } 21 | 22 | .card-icon { 23 | i { 24 | font-size: 40px; 25 | color: rgb(30 32 37 / 18%); 26 | } 27 | } 28 | } 29 | 30 | .bg-1 { 31 | background: linear-gradient(rgb(43 90 237 / 80%) 0%, rgb(43 90 237) 100%); 32 | } 33 | 34 | .bg-2 { 35 | background: linear-gradient(rgb(251 164 10 / 80%) 0%, rgb(251 164 10) 100%); 36 | } 37 | 38 | .bg-3 { 39 | background: linear-gradient(rgb(43 174 133 / 80%) 0%, rgb(43 174 133) 100%); 40 | } 41 | 42 | .bg-4 { 43 | background: linear-gradient(rgb(241 144 140 / 80%) 0%, rgb(241 144 140) 100%); 44 | } 45 | 46 | .bg-5 { 47 | background: linear-gradient(rgb(0 201 245 / 80%) 0%, rgb(0 201 245) 100%); 48 | } 49 | 50 | .bg-6 { 51 | background: linear-gradient(rgb(36 134 185 / 80%) 0%, rgb(36 134 185) 100%); 52 | } 53 | 54 | .chart-card { 55 | box-sizing: border-box; 56 | display: flex; 57 | flex-direction: column; 58 | justify-content: space-between; 59 | width: 100%; 60 | height: 360px; 61 | padding: 24px; 62 | background-color: var(--el-bg-color-overlay); 63 | border-radius: 8px; 64 | 65 | .chart-title { 66 | font-size: 20px; 67 | font-weight: 600; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 92 | 93 | 96 | -------------------------------------------------------------------------------- /src/views/echarts/columnChart/index.scss: -------------------------------------------------------------------------------- 1 | .chart-box { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/echarts/columnChart/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /src/views/echarts/waterChart/index.scss: -------------------------------------------------------------------------------- 1 | .chart-box { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/echarts/waterChart/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/views/login/components/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/views/login/index.scss: -------------------------------------------------------------------------------- 1 | .login-container { 2 | height: 100%; 3 | background: #eeeeee; 4 | background-image: url('@/assets/images/login_bg.svg'); 5 | background-size: 100% 100%; 6 | background-size: cover; 7 | 8 | .login-box { 9 | position: relative; 10 | box-sizing: border-box; 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-around; 14 | width: 96.5%; 15 | height: 94%; 16 | padding: 0 50px; 17 | background-color: rgb(255 255 255 / 80%); 18 | border-radius: 10px; 19 | 20 | .switch-dark { 21 | position: absolute; 22 | top: 5%; 23 | right: 4%; 24 | } 25 | 26 | .login-left { 27 | width: 800px; 28 | margin-right: 20px; 29 | 30 | .login-left-img { 31 | width: 90%; 32 | height: 100%; 33 | } 34 | } 35 | 36 | .login-form { 37 | width: 420px; 38 | padding: 50px 40px 45px; 39 | border-radius: 10px; 40 | box-shadow: rgb(0 0 0 / 10%) 0 2px 10px 2px; 41 | 42 | .login-logo { 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | margin-bottom: 32px; 47 | 48 | .login-icon { 49 | position: relative; 50 | left: -20000px; /* 移动到窗口外很远的地方,也可以用transform: translateY(-20000px); */ 51 | width: 60px; 52 | height: 60px; 53 | filter: drop-shadow(var(--el-color-primary) 20000px 0); 54 | } 55 | 56 | .logo-text { 57 | padding: 0 0 0 25px; 58 | margin: 0; 59 | font-size: 28px; 60 | font-weight: bold; 61 | color: #34495e; 62 | white-space: nowrap; 63 | } 64 | } 65 | 66 | .login-btns { 67 | display: flex; 68 | justify-content: space-between; 69 | 70 | .login-btn { 71 | width: 48%; 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | @media screen and (width <= 1250px) { 79 | .login-left { 80 | display: none; 81 | } 82 | } 83 | 84 | @media screen and (width <= 600px) { 85 | .login-form { 86 | width: 97% !important; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 39 | 40 | 43 | -------------------------------------------------------------------------------- /src/views/menu/menu1/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/views/menu/menu1/index.scss -------------------------------------------------------------------------------- /src/views/menu/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/views/menu/menu2/menu21/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/views/menu/menu2/menu21/index.scss -------------------------------------------------------------------------------- /src/views/menu/menu2/menu21/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/views/menu/menu2/menu22/menu221/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/views/menu/menu2/menu22/menu221/index.scss -------------------------------------------------------------------------------- /src/views/menu/menu2/menu22/menu221/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/views/menu/menu2/menu22/menu222/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/views/menu/menu2/menu22/menu222/index.scss -------------------------------------------------------------------------------- /src/views/menu/menu2/menu22/menu222/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/views/menu/menu2/menu23/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/views/menu/menu2/menu23/index.scss -------------------------------------------------------------------------------- /src/views/menu/menu2/menu23/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/views/menu/menu3/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/views/menu/menu3/index.scss -------------------------------------------------------------------------------- /src/views/menu/menu3/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/views/system/accountManage/components/userDialog.vue: -------------------------------------------------------------------------------- 1 | 181 | 182 | 351 | 352 | 353 | -------------------------------------------------------------------------------- /src/views/system/accountManage/index.scss: -------------------------------------------------------------------------------- 1 | .search-container { 2 | padding-bottom: 0; 3 | } 4 | 5 | .search-form .el-input { 6 | --el-input-width: 220px; 7 | } 8 | 9 | .search-form .el-select { 10 | --el-select-width: 220px; 11 | } 12 | 13 | .search-form .el-form-item { 14 | margin-right: 24px; 15 | } 16 | 17 | .btn-icon { 18 | font-size: 14px; 19 | } 20 | 21 | .account-manage { 22 | display: flex; 23 | height: 100%; 24 | 25 | .left-card { 26 | width: 200px; 27 | } 28 | 29 | .right-card { 30 | display: flex; 31 | flex: 1; 32 | flex-direction: column; 33 | height: 100%; 34 | 35 | .table-container { 36 | display: flex; 37 | flex: 1; 38 | flex-direction: column; 39 | height: 100%; 40 | 41 | .table-content { 42 | flex: 1; 43 | } 44 | 45 | .table-pagination { 46 | justify-content: right; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/views/system/accountManage/index.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 197 | 198 | 201 | -------------------------------------------------------------------------------- /src/views/system/departmentManage/components/departmentDialog.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /src/views/system/departmentManage/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuimng/vue3-admin-client/5f2bae9812343a15b31087a8c9cb7acc82ab252a/src/views/system/departmentManage/index.scss -------------------------------------------------------------------------------- /src/views/system/departmentManage/index.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 85 | 86 | 91 | -------------------------------------------------------------------------------- /src/views/system/menuManage/components/menuDialog.vue: -------------------------------------------------------------------------------- 1 | 198 | 199 | 354 | 355 | 356 | -------------------------------------------------------------------------------- /src/views/system/menuManage/index.scss: -------------------------------------------------------------------------------- 1 | .search-container { 2 | padding-bottom: 0; 3 | } 4 | 5 | .search-form .el-input { 6 | --el-input-width: 220px; 7 | } 8 | 9 | .search-form .el-select { 10 | --el-select-width: 220px; 11 | } 12 | 13 | .search-form .el-form-item { 14 | margin-right: 24px; 15 | } 16 | 17 | .btn-icon { 18 | font-size: 14px; 19 | } 20 | 21 | .menu-manage { 22 | display: flex; 23 | flex-direction: column; 24 | height: 100%; 25 | 26 | .table-container { 27 | display: flex; 28 | flex: 1; 29 | flex-direction: column; 30 | height: 100%; 31 | 32 | .table-content { 33 | flex: 1; 34 | } 35 | 36 | .table-pagination { 37 | justify-content: right; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/views/system/menuManage/index.vue: -------------------------------------------------------------------------------- 1 | 91 | 148 | 149 | 152 | -------------------------------------------------------------------------------- /src/views/system/roleManage/components/roleDialog.vue: -------------------------------------------------------------------------------- 1 | 149 | 150 | 342 | 343 | 363 | -------------------------------------------------------------------------------- /src/views/system/roleManage/index.scss: -------------------------------------------------------------------------------- 1 | .search-container { 2 | padding-bottom: 0; 3 | } 4 | 5 | .search-form .el-input { 6 | --el-input-width: 220px; 7 | } 8 | 9 | .search-form .el-select { 10 | --el-select-width: 220px; 11 | } 12 | 13 | .search-form .el-form-item { 14 | margin-right: 24px; 15 | } 16 | 17 | .btn-icon { 18 | font-size: 14px; 19 | } 20 | 21 | .role-manage { 22 | display: flex; 23 | flex-direction: column; 24 | height: 100%; 25 | 26 | .table-container { 27 | display: flex; 28 | flex: 1; 29 | flex-direction: column; 30 | height: 100%; 31 | 32 | .table-content { 33 | flex: 1; 34 | } 35 | 36 | .table-pagination { 37 | justify-content: right; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/views/system/roleManage/index.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 157 | 158 | 161 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "baseUrl": "./", 17 | "paths": { 18 | "@": ["src"], 19 | "@/*": ["src/*"] 20 | }, 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true 27 | }, 28 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigEnv, UserConfig, defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | import Components from 'unplugin-vue-components/vite' 6 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 7 | import { visualizer } from 'rollup-plugin-visualizer' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig(({ mode }: ConfigEnv): UserConfig => { 11 | const viteEnv = loadEnv(mode, process.cwd()) 12 | return { 13 | base: '/', 14 | resolve: { 15 | alias: { 16 | '@': resolve(__dirname, './src') 17 | } 18 | }, 19 | server: { 20 | host: '0.0.0.0', 21 | port: viteEnv.VITE_PORT as unknown as number, 22 | proxy: { 23 | '/api': { 24 | target: viteEnv.VITE_PROXY, 25 | changeOrigin: true, 26 | rewrite: (path) => path.replace(/^\/api/, '') 27 | } 28 | } 29 | }, 30 | plugins: [ 31 | vue(), 32 | AutoImport({ 33 | resolvers: [ElementPlusResolver()], 34 | dts: 'src/typings/auto-imports.d.ts' // 指定类型声明文件的路径 35 | }), 36 | Components({ 37 | resolvers: [ElementPlusResolver()], 38 | dts: 'src/typings/components.d.ts' // 指定类型声明文件的路径 39 | }), 40 | visualizer({ open: false }) 41 | ], 42 | esbuild: { 43 | pure: viteEnv.VITE_DROP_CONSOLE ? ['console.log', 'debugger'] : [] 44 | }, 45 | build: { 46 | outDir: 'dist', 47 | minify: 'esbuild', 48 | sourcemap: false, 49 | // 禁用 gzip 压缩大小报告,可略微减少打包时间 50 | reportCompressedSize: true, 51 | // 规定触发警告的 chunk 大小 52 | chunkSizeWarningLimit: 2000, 53 | rollupOptions: { 54 | output: { 55 | // Static resource classification and packaging 56 | chunkFileNames: 'assets/js/[name]-[hash].js', 57 | entryFileNames: 'assets/js/[name]-[hash].js', 58 | assetFileNames: 'assets/[ext]/[name]-[hash].[ext]' 59 | } 60 | } 61 | } 62 | } 63 | }) 64 | --------------------------------------------------------------------------------