├── .env.production ├── .env.staging ├── src ├── assets │ └── images │ │ ├── home │ │ └── work.png │ │ ├── login │ │ ├── build.png │ │ └── map.png │ │ ├── user │ │ └── avatar.png │ │ ├── common │ │ ├── logo.svg │ │ └── logo-text.svg │ │ └── error │ │ ├── 404.svg │ │ └── 403.svg ├── store │ ├── index.ts │ └── modules │ │ ├── app.ts │ │ └── user.ts ├── views │ ├── center │ │ └── index.vue │ ├── menu │ │ ├── menu1 │ │ │ ├── menu1-2 │ │ │ │ └── index.vue │ │ │ └── menu1-1 │ │ │ │ ├── menu1-1-1 │ │ │ │ └── index.vue │ │ │ │ └── menu1-1-2 │ │ │ │ └── index.vue │ │ └── menu2 │ │ │ └── index.vue │ ├── power │ │ ├── role │ │ │ └── index.vue │ │ ├── route │ │ │ └── index.vue │ │ └── dict │ │ │ └── index.vue │ ├── error │ │ └── 404.vue │ ├── login │ │ └── index.vue │ └── home │ │ └── index.vue ├── apis │ ├── system.ts │ └── user.ts ├── layout │ ├── components │ │ ├── Main.vue │ │ ├── Aside │ │ │ ├── MenuItem.vue │ │ │ ├── SubMenu.vue │ │ │ └── index.vue │ │ ├── Footer.vue │ │ └── Header.vue │ └── index.vue ├── styles │ ├── variables.scss │ ├── element-theme.scss │ ├── index.scss │ ├── reset.scss │ ├── transition.scss │ └── common.scss ├── directives │ ├── index.ts │ └── controller │ │ └── permission.ts ├── App.vue ├── main.ts ├── utils │ ├── cache.ts │ ├── index.ts │ └── request.ts └── router │ ├── permission.ts │ └── index.ts ├── .env.development ├── tsconfig.node.json ├── .env ├── .vscode ├── settings.json └── extensions.json ├── types ├── index.d.ts ├── vue-router.d.ts └── vite-env.d.ts ├── prettier.config.js ├── .gitignore ├── index.html ├── tsconfig.json ├── mock ├── index.ts └── controller │ ├── system.ts │ └── user.ts ├── README.md ├── package.json ├── .eslintrc.js ├── README_EN.md ├── .stylelintrc.js └── vite.config.ts /.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境接口基础路径 2 | VITE_BASE_URL = http://192.168.1.13:8082 -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | # 预发布环境接口基础路径 2 | VITE_BASE_URL = http://192.168.1.13:8081 -------------------------------------------------------------------------------- /src/assets/images/home/work.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abuzhengyi/vue3-admin/HEAD/src/assets/images/home/work.png -------------------------------------------------------------------------------- /src/assets/images/login/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abuzhengyi/vue3-admin/HEAD/src/assets/images/login/build.png -------------------------------------------------------------------------------- /src/assets/images/login/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abuzhengyi/vue3-admin/HEAD/src/assets/images/login/map.png -------------------------------------------------------------------------------- /src/assets/images/user/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abuzhengyi/vue3-admin/HEAD/src/assets/images/user/avatar.png -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const store = createPinia() 4 | 5 | export default store 6 | -------------------------------------------------------------------------------- /src/views/center/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/menu/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/menu/menu1/menu1-1/menu1-1-1/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/menu/menu1/menu1-1/menu1-1-2/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 开发环境接口基础路径 2 | VITE_BASE_URL = http://192.168.1.13:8080 3 | # 开发环境下是否开启代理,true/false 4 | VITE_PROXY_SERVER = true 5 | # 开发环境下是否开启mock,true/false 6 | VITE_MOCK_SERVER = true -------------------------------------------------------------------------------- /src/views/menu/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/power/role/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/power/route/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE = 'V3后台管理' 2 | # 应用描述 3 | VITE_APP_DESCRIBE = 'Vue3 后台管理系统基础解决方案' 4 | # 应用版权 5 | VITE_APP_COPYRIGHT = '© 版权所有 2023 github.com/abuzhengyi' 6 | # cache 持久化存储的 key 前缀 7 | VITE_CACHE_PREFIX = 'v3-admin' 8 | # 路由模式,hash/history 9 | VITE_HISTORY_MODE = 'hash' -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | /** VSCode 插件配置 */ 2 | { 3 | // 开启所有自动修复,包含 ESLint、StyleLint 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": true 6 | }, 7 | // Prettier 8 | "editor.formatOnSave": true, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | /** 使用 vscode 打开会自动提示是否安装以下插件 */ 2 | { 3 | "recommendations": [ 4 | "vue.volar", 5 | "vue.vscode-typescript-vue-plugin", 6 | "dbaeumer.vscode-eslint", 7 | "stylelint.vscode-stylelint", 8 | "esbenp.prettier-vscode", 9 | "antfu.unocss" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare module '*.svg' 4 | declare module '*.png' 5 | declare module '*.jpg' 6 | declare module '*.jpeg' 7 | declare module '*.gif' 8 | declare module '*.json' 9 | declare module '*.js' 10 | 11 | declare global { 12 | type FormInstance = import('element-plus/es').FormInstance 13 | } 14 | -------------------------------------------------------------------------------- /types/vue-router.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router' 2 | 3 | declare module 'vue-router' { 4 | interface RouteMeta { 5 | // 标题 6 | title?: string 7 | // 副标题 8 | subtitle?: string 9 | // 图标 10 | icon?: string 11 | // 是否为一级菜单 12 | menu?: boolean 13 | // 用户权限 14 | roles?: string[] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/apis/system.ts: -------------------------------------------------------------------------------- 1 | import { get, post } from '@/utils/request' 2 | 3 | /** 获取系统通知 */ 4 | export const getNoticeList = () => post('/notice/list') 5 | 6 | /** 获取统计数据 */ 7 | export interface StatisticsData { 8 | date: string[] 9 | } 10 | 11 | export const getStatisticsList = (data: StatisticsData) => post('/statistics/list', data) 12 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 超过字数换行 3 | printWidth: 100, 4 | // 一个 tab 缩进的空格 5 | tabWidth: 2, 6 | // 采用单引号 7 | singleQuote: true, 8 | // 末尾不加分号 9 | semi: false, 10 | // 对象或者数组的最后一项后面不要加逗号 11 | trailingComma: 'none', 12 | // 大括号中间空格 13 | bracketSpacing: true, 14 | // 箭头函数的参数无论有几个,都要括号包裹 15 | arrowParens: 'always' 16 | } 17 | -------------------------------------------------------------------------------- /src/layout/components/Main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | /** 全局 css 变量,在 js 中获取这些变量请使用 utils 中的 getCssVar 方法 */ 2 | :root { 3 | // aside 宽度 4 | --v3-aside-width: 200px; 5 | // aside 折叠后的宽度 6 | --v3-aside-width-collapse: 64px; 7 | // header 高度 8 | --v3-header-height: 64px; 9 | // 主体内容的最小宽度,视口宽度小于此阈值时 aside 将折叠 10 | --v3-main-width-min: 700px; 11 | // footer 高度 12 | --v3-footer-height: 80px; 13 | } 14 | -------------------------------------------------------------------------------- /.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 | dist 12 | dist-ssr 13 | *.local 14 | *.mjs 15 | .eslintcache 16 | types/auto-imports.d.ts 17 | types/components.d.ts 18 | 19 | # Editor directories and files 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%- title %> 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import store from '@/store' 3 | import { defineStore } from 'pinia' 4 | 5 | export const useAppStore = defineStore('app', () => { 6 | const collapse = ref(false) 7 | 8 | // 设置 aside 折叠 9 | const setCollapse = (val: boolean) => { 10 | collapse.value = val 11 | } 12 | 13 | return { collapse, setCollapse } 14 | }) 15 | 16 | export const useAppStoreHook = () => useAppStore(store) 17 | -------------------------------------------------------------------------------- /src/layout/components/Aside/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/apis/user.ts: -------------------------------------------------------------------------------- 1 | import { get, post } from '@/utils/request' 2 | import md5 from 'md5' 3 | 4 | /** 登录 */ 5 | export interface LoginData { 6 | account: string 7 | password: string 8 | } 9 | 10 | export const loginApi = ({ account, password }: LoginData) => { 11 | password &&= md5(password) 12 | return post('/login', { 13 | account, 14 | password 15 | }) 16 | } 17 | 18 | /** 退出登录 */ 19 | export const logoutApi = () => post('/logout') 20 | 21 | /** 获取用户信息 */ 22 | export const getUserInfoApi = () => post('/user/info') 23 | -------------------------------------------------------------------------------- /src/layout/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import type { App, Plugin, Directive } from 'vue' 2 | 3 | /** 自动注册自定义指令 */ 4 | export default { 5 | async install(app: App) { 6 | // 导入所有自定义指令模块 7 | const modules: Record = import.meta.glob('./controller/*.ts', { 8 | eager: true, 9 | import: 'default' 10 | }) 11 | for (const [path, directive] of Object.entries(modules)) { 12 | // 根据文件路径获取 name 13 | const name = path.match(/\/((\w)*)\.ts/)?.[1] 14 | // 注册自定义指令 15 | name && app.directive(name, directive) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/element-theme.scss: -------------------------------------------------------------------------------- 1 | /** Element Plus 主题自定义 */ 2 | @forward 'element-plus/theme-chalk/src/common/var.scss' with ( 3 | $colors: ( 4 | ( 5 | 'white': #ffffff, 6 | 'black': #000000, 7 | 'primary': ( 8 | 'base': #409eff 9 | ), 10 | 'success': ( 11 | 'base': #67c23a 12 | ), 13 | 'warning': ( 14 | 'base': #e6a23c 15 | ), 16 | 'danger': ( 17 | 'base': #f56c6c 18 | ), 19 | 'error': ( 20 | 'base': #f56c6c 21 | ), 22 | 'info': ( 23 | 'base': #909399 24 | ) 25 | ) 26 | ) 27 | ); 28 | -------------------------------------------------------------------------------- /types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | interface ImportMetaEnv { 11 | readonly VITE_APP_TITLE: string 12 | readonly VITE_APP_DESCRIBE: string 13 | readonly VITE_APP_COPYRIGHT: string 14 | readonly VITE_HISTORY_MODE: string 15 | readonly VITE_BASE_URL: string 16 | readonly VITE_PROXY_SERVER: string 17 | } 18 | 19 | interface ImportMeta { 20 | readonly env: ImportMetaEnv 21 | } 22 | -------------------------------------------------------------------------------- /src/views/power/dict/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": ["src/*"] 18 | } 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "types/*.d.ts", "vite.config.ts"], 21 | "exclude": ["node_modules"], 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /src/layout/components/Aside/SubMenu.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /src/directives/controller/permission.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from 'vue' 2 | import { useUserStoreHook } from '@/store/modules/user' 3 | 4 | /** 权限指令 */ 5 | export default { 6 | mounted(el, binding) { 7 | const { value } = binding 8 | const roles = useUserStoreHook().roles 9 | if (value && value instanceof Array && value.length > 0) { 10 | const permissionRoles = value 11 | const hasPermission = roles.some((role) => { 12 | return permissionRoles.includes(role) 13 | }) 14 | if (!hasPermission) { 15 | el.style.display = 'none' 16 | } 17 | } else { 18 | throw new Error(`Need roles! Like v-permission="['admin','editor']"`) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | /** pinia */ 5 | import store from './store' 6 | 7 | /** router */ 8 | import router from './router' 9 | import './router/permission' 10 | 11 | /** mockjs */ 12 | import mockXHR from '../mock' 13 | mockXHR() 14 | 15 | /** directives */ 16 | import directives from '@/directives' 17 | 18 | /** styles */ 19 | import './styles' 20 | 21 | const app = createApp(App) 22 | 23 | /** icons */ 24 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 25 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 26 | app.component(`IconEp${key}`, component) 27 | } 28 | 29 | app.use(store).use(router).use(directives).mount('#app') 30 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './reset.scss' as *; 2 | @use './variables.scss' as *; 3 | @use './transition.scss' as *; 4 | 5 | body { 6 | font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 7 | '微软雅黑', Arial, sans-serif; 8 | font-size: var(--el-font-size-medium); 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | color: var(--el-text-color-primary); 12 | background-color: var(--el-bg-color-page); 13 | 14 | @at-root &, 15 | html, 16 | #app { 17 | height: 100%; 18 | } 19 | } 20 | 21 | a { 22 | color: var(--el-color-primary); 23 | } 24 | 25 | .container { 26 | height: 800px; 27 | min-width: var(--v3-main-width-min); 28 | padding: 20px; 29 | margin: 20px; 30 | background-color: var(--el-bg-color); 31 | border-radius: var(--el-border-radius-small); 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/images/common/logo.svg: -------------------------------------------------------------------------------- 1 | element plus-logo-small 副本 -------------------------------------------------------------------------------- /mock/index.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import type { AxiosResponse, Method } from 'axios' 3 | 4 | export interface MockData { 5 | url: string 6 | method?: Method 7 | response: AxiosResponse 8 | } 9 | 10 | /** 注册 mock 拦截 */ 11 | export default async () => { 12 | const { DEV, VITE_PROXY_SERVER, VITE_BASE_URL } = import.meta.env, 13 | proxy = DEV && VITE_PROXY_SERVER === 'true', 14 | baseURL = proxy ? '/api' : VITE_BASE_URL 15 | 16 | // 开发环境并且开启代理情况下执行,resolve(false) 未完成注册 17 | if (!proxy) return false 18 | // 导入所有 mock 模块 19 | const modules = Promise>>>( 20 | import.meta.glob('./controller/*.ts') 21 | ) 22 | for (const path in modules) { 23 | const mocks = await modules[path]() 24 | // 注册所有 mock 25 | for (const key in mocks) { 26 | const mockData = mocks[key] 27 | Mock.mock(`${baseURL}${mockData.url}`, mockData.method || 'get', mockData.response) 28 | } 29 | } 30 | // resolve(true) 注册完成 31 | return true 32 | } 33 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | -------------------------------------------------------------------------------- /src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | /** 样式重置 */ 2 | html, 3 | body, 4 | div, 5 | span, 6 | applet, 7 | object, 8 | iframe, 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6, 15 | p, 16 | blockquote, 17 | pre, 18 | a, 19 | abbr, 20 | acronym, 21 | address, 22 | big, 23 | cite, 24 | code, 25 | del, 26 | dfn, 27 | em, 28 | font, 29 | img, 30 | ins, 31 | kbd, 32 | q, 33 | s, 34 | samp, 35 | small, 36 | strike, 37 | strong, 38 | sub, 39 | sup, 40 | tt, 41 | var, 42 | b, 43 | u, 44 | i, 45 | center, 46 | image, 47 | dl, 48 | dt, 49 | dd, 50 | ol, 51 | ul, 52 | li, 53 | fieldset, 54 | form, 55 | label, 56 | legend, 57 | table, 58 | caption, 59 | tbody, 60 | tfoot, 61 | thead, 62 | tr, 63 | th, 64 | td, 65 | *::after, 66 | *::before { 67 | padding: 0; 68 | margin: 0; 69 | border: 0; 70 | outline: 0; 71 | box-sizing: border-box; 72 | } 73 | 74 | ol, 75 | ul { 76 | list-style: none; 77 | } 78 | 79 | a { 80 | text-decoration: none; 81 | } 82 | 83 | img { 84 | vertical-align: top; 85 | } 86 | 87 | span, 88 | a { 89 | display: inline-block; 90 | } 91 | 92 | input::-webkit-outer-spin-button, 93 | input::-webkit-inner-spin-button { 94 | appearance: none; 95 | } 96 | -------------------------------------------------------------------------------- /mock/controller/system.ts: -------------------------------------------------------------------------------- 1 | export const noticeList = { 2 | url: '/notice/list', 3 | method: 'post', 4 | response: { 5 | code: 200, 6 | message: 'success', 7 | data: [ 8 | { title: 'Vue3后台管理系统上线啦!' }, 9 | { title: '后台管理系统基础解决方案。' }, 10 | { title: '后续更多功能,敬请关注。', routeName: 'home' } 11 | ] 12 | } 13 | } 14 | 15 | export const statisticsList = { 16 | url: '/statistics/list', 17 | method: 'post', 18 | response: { 19 | code: 200, 20 | message: 'success', 21 | data: [ 22 | { 23 | title: '用户充值', 24 | total: 265221.6, 25 | percent: '+40%', 26 | color: '#ffa900', 27 | actived: true, 28 | data: [20, 60, 70, 800, 600, 966, 633] 29 | }, 30 | { 31 | title: '用户活跃', 32 | total: 36552.0, 33 | percent: '+8%', 34 | color: '#2a55e5', 35 | actived: true, 36 | data: [90, 652, 90, 96, 62, 800, 60] 37 | }, 38 | { 39 | title: '用户分析', 40 | total: 633525.6, 41 | percent: '-24%', 42 | color: '#6abf40', 43 | actived: true, 44 | data: [852, 658, 20, 63, 85, 652, 63] 45 | } 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mock/controller/user.ts: -------------------------------------------------------------------------------- 1 | import cache from '../../src/utils/cache' 2 | 3 | export const login = { 4 | url: '/login', 5 | method: 'post', 6 | response: ({ body }) => { 7 | const { account, password } = JSON.parse(body) 8 | return { 9 | code: 200, 10 | message: 'success', 11 | data: { 12 | token: `${account + password}` 13 | } 14 | } 15 | } 16 | } 17 | 18 | export const logout = { 19 | url: '/logout', 20 | method: 'post', 21 | response: { 22 | code: 200, 23 | message: 'success' 24 | } 25 | } 26 | 27 | export const userInfo = { 28 | url: '/user/info', 29 | method: 'post', 30 | response: () => { 31 | const token = cache.get('token') 32 | let data = {} 33 | if (/^admin.*/.test(token)) { 34 | data = { 35 | name: 'admin', 36 | account: 'admin', 37 | avatar: '', 38 | roles: ['admin', 'editor'] 39 | } 40 | } else if (/^editor.*/.test(token)) { 41 | data = { 42 | name: 'editor', 43 | account: 'editor', 44 | avatar: '', 45 | roles: ['editor'] 46 | } 47 | } else { 48 | data = {} 49 | } 50 | return { 51 | code: 200, 52 | message: 'success', 53 | data 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | 29 | 60 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | /** fade */ 2 | .fade-enter-active, 3 | .fade-leave-active { 4 | transition: opacity 0.3s; 5 | } 6 | 7 | .fade-enter-from, 8 | .fade-leave-active { 9 | opacity: 0; 10 | } 11 | 12 | /** fade-transform */ 13 | .fade-transform-leave-active, 14 | .fade-transform-enter-active { 15 | transition: all 0.3s; 16 | } 17 | 18 | .fade-transform-enter-from { 19 | opacity: 0; 20 | transform: translateX(-30px); 21 | } 22 | 23 | .fade-transform-leave-to { 24 | opacity: 0; 25 | transform: translateX(30px); 26 | } 27 | 28 | /** popover-transform */ 29 | .popover-transform-enter-active, 30 | .popover-transform-leave-active { 31 | transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; 32 | } 33 | 34 | .popover-transform-enter-to, 35 | .popover-transform-leave-from { 36 | opacity: 1; 37 | transform: translateY(0); 38 | } 39 | 40 | .popover-transform-enter-from, 41 | .popover-transform-leave-to { 42 | opacity: 0; 43 | transform: translateY(5px); 44 | } 45 | 46 | /** panel-transform */ 47 | .panel-transform-enter-active, 48 | .panel-transform-leave-active { 49 | transition: all 0.3s; 50 | } 51 | 52 | .panel-transform-enter-from, 53 | .panel-transform-leave-to { 54 | opacity: 0; 55 | transform: translateX(20px); 56 | } 57 | 58 | /** list-transform */ 59 | .list-transform-enter-active, 60 | .list-transform-leave-active { 61 | transition: all 1s; 62 | } 63 | 64 | .list-transform-enter-from, 65 | .list-transform-leave-to { 66 | opacity: 0; 67 | transform: translateY(-30px); 68 | } 69 | 70 | .list-transform-leave-active { 71 | position: absolute !important; 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | type CacheContent = string | number | boolean | [] | object 2 | 3 | interface ThisContent { 4 | cache: Storage 5 | set: (name: string, content: CacheContent) => void 6 | get: (name: string) => CacheContent 7 | remove: (name: string) => void 8 | removeAll: () => void 9 | } 10 | 11 | /** 持久化存储 */ 12 | const Cache: unknown = function (this: ThisContent, storage = localStorage) { 13 | // 获取存储的 key 14 | const { VITE_CACHE_PREFIX } = import.meta.env 15 | const getKey = (name: string) => `${VITE_CACHE_PREFIX}-${name}`.toUpperCase() 16 | 17 | // 这里默认使用 localStorage 做存储,可以传入 sessionStorage 18 | this.cache = storage 19 | 20 | // 存储storage 21 | this.set = (name, content) => { 22 | if (!name) return 23 | if (typeof content !== 'string') { 24 | content = JSON.stringify(content) 25 | } 26 | this.cache.setItem(getKey(name), content) 27 | } 28 | 29 | // 获取storage 30 | this.get = (name) => { 31 | if (!name) return 32 | const content = this.cache.getItem(getKey(name)) || '' 33 | // 判断布尔、数字、数组、对象类型 34 | if (/^(true|false|[0-9]{1,}|\[(.*)\]|\{(.*)\})$/.test(content)) { 35 | return JSON.parse(content) 36 | } else { 37 | return content 38 | } 39 | } 40 | 41 | // 删除单个storage 42 | this.remove = (name) => { 43 | if (!name) return 44 | this.cache.removeItem(getKey(name)) 45 | } 46 | 47 | // 删除所有storage 48 | this.removeAll = () => { 49 | const storage = this.cache 50 | for (const item in storage) { 51 | this.cache.removeItem(item) 52 | } 53 | } 54 | } 55 | 56 | const cacheInstance = new ( ThisContent>Cache)() 57 | 58 | export default cacheInstance 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

V3 Admin

3 | 中文 | English 4 |
5 | 6 | ## ⚡ 简介 7 | 8 | 一个基于 Vue3、 Element Plus、TypeScript、Vite、Pinia 等主流技术的后台管理系统基础解决方案. 9 | 10 | ## ✨ 特性 11 | 12 | - **Vue3**:采用 Vue3 最新的组合式 API(更好的逻辑复用和更灵活的代码组织) 13 | - **TypeScript**:JavaScript 语言的超集(避免运行时错误) 14 | - **Vite**:更快的前端自动化构建工具(真正的按需编译和模块热更新) 15 | - **Vue Router**:Vue 路由 16 | - **Pinia**: Vue3 的状态存储库 17 | - **Axios**:HTTP 请求(已封装) 18 | - **Mock**:拦截 HTTP 请求,返回模拟的响应数据 19 | - **Element Plus**:Element UI 的 Vue3 版本 20 | - **ECharts**:数据可视化图表库 21 | - **SCSS**:使用与 Element Plus 一致的 CSS 预处理语言 22 | - **ESlint + Stylelint + Prettier**:代码校验与风格统一 23 | - **自动按需导入**:无需手动引入 El 组件、El 图标、El 方法、Vue3 API 24 | - **注释**:尽可能详细的注释 25 | 26 | ## 🌱 功能 27 | 28 | - **用户管理**:登录、登出演示 29 | - **权限管理**:路由守卫、动态路由、权限指令 30 | - **错误页面**: 403、404 31 | - **多环境**:开发环境(development)、预发布环境(staging)、生产环境(production) 32 | 33 | ## 🚀 开始 34 | 35 | ### 版本 36 | 37 | > Node 版本需要 16+,npm 版本需要 8+,当你的包管理器发出警告时,请注意升级你的 Node 版本. 38 | 39 | ```bash 40 | # 查看 Node 版本 41 | node -v 42 | 43 | # 查看 npm 版本 44 | npm -v 45 | ``` 46 | 47 | ### 启动 48 | 49 | ```bash 50 | # 克隆项目 51 | git clone https://github.com/abuzhengyi/vue3-admin.git 52 | 53 | # 进入项目目录 54 | cd vue3-admin 55 | 56 | # 安装依赖(国内用户推荐使用淘宝镜像安装) 57 | npm i --registry=https://registry.npm.taobao.org 58 | 59 | # 启动服务 60 | npm run dev 61 | ``` 62 | 63 | ### 打包 64 | 65 | ```bash 66 | # 打包预发布环境 67 | npm run build:staging 68 | 69 | # 打包生产环境 70 | npm run build:production 71 | ``` 72 | 73 | ### 预览 74 | 75 | ```bash 76 | # 预览预发布环境 77 | npm run preview:staging 78 | 79 | # 预览生产环境 80 | npm run preview:production 81 | ``` 82 | 83 | ### 代码格式 84 | 85 | ```bash 86 | # 手动修复 87 | npm run lint 88 | 89 | # 自动修复 90 | 使用 VSCode 打开项目会提示安装 ESLint、StyleLint、Prettier 三个插件,安装完成后 ctrl + s 保存即可自动格式化修复. 91 | ``` 92 | 93 | ## 💕 Star 94 | 95 | 小项目开源不易,如果您觉得还不错的话,动动您的小手给作者点个 Star 吧,您的支持将是作者最大的动力! 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-admin", 3 | "private": true, 4 | "version": "0.0.0", 5 | "author": { 6 | "name": "abuzhengyi", 7 | "email": "1553384775@qq.com", 8 | "url": "https://github.com/abuzhengyi" 9 | }, 10 | "scripts": { 11 | "dev": "vite", 12 | "build:staging": "vue-tsc --noEmit && vite build --mode staging", 13 | "build:production": "vue-tsc --noEmit && vite build", 14 | "preview:staging": "npm run build:staging && vite preview", 15 | "preview:production": "npm run build:production && vite preview", 16 | "lint:eslint": "eslint src --ext .vue,.js,.ts,.jsx,.tsx --fix", 17 | "lint:stylelint": "stylelint **/*.{css,less,scss,vue} --fix", 18 | "lint:prettier": "prettier --write **/*.{js,ts,json,tsx,css,less,scss,vue}", 19 | "lint": "npm run lint:eslint && npm run lint:stylelint && npm run lint:prettier" 20 | }, 21 | "dependencies": { 22 | "@element-plus/icons-vue": "^2.0.10", 23 | "axios": "^1.2.1", 24 | "element-plus": "^2.2.26", 25 | "md5": "^2.3.0", 26 | "mockjs": "^1.1.0", 27 | "nprogress": "^0.2.0", 28 | "pinia": "^2.0.28", 29 | "pinia-plugin-persistedstate": "^3.0.1", 30 | "vue": "^3.2.45", 31 | "vue-router": "^4.1.6" 32 | }, 33 | "devDependencies": { 34 | "@iconify-json/ep": "^1.1.8", 35 | "@types/axios": "^0.14.0", 36 | "@types/md5": "^2.3.2", 37 | "@types/mockjs": "^1.0.7", 38 | "@types/node": "^18.11.14", 39 | "@types/nprogress": "^0.2.0", 40 | "@typescript-eslint/eslint-plugin": "^5.47.1", 41 | "@typescript-eslint/parser": "^5.47.1", 42 | "@vitejs/plugin-vue": "^4.0.0", 43 | "eslint": "^8.30.0", 44 | "eslint-plugin-prettier": "^4.2.1", 45 | "eslint-plugin-vue": "^9.8.0", 46 | "prettier": "2.8.1", 47 | "sass": "^1.56.2", 48 | "stylelint": "^15.2.0", 49 | "stylelint-config-recommended-vue": "^1.4.0", 50 | "stylelint-config-standard-scss": "^7.0.1", 51 | "stylelint-order": "^6.0.2", 52 | "typescript": "^4.9.3", 53 | "unplugin-auto-import": "^0.12.1", 54 | "unplugin-icons": "^0.14.15", 55 | "unplugin-vue-components": "^0.22.12", 56 | "vite": "^4.0.0", 57 | "vite-plugin-html": "^3.2.0", 58 | "vue-tsc": "^1.0.11" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/styles/common.scss: -------------------------------------------------------------------------------- 1 | /** position */ 2 | @each $name in absolute, relative, fixed { 3 | .#{$name} { 4 | position: $name; 5 | } 6 | } 7 | 8 | /** display */ 9 | @each $name in inline-flex, flex, none, inline, inline-block, block { 10 | .#{$name} { 11 | display: $name; 12 | } 13 | } 14 | 15 | /** flex */ 16 | .flex-1 { 17 | flex: 1 0 0; 18 | } 19 | 20 | /** padding、margin */ 21 | @each $value in 5, 10, 20, 30, 40, 50, 100 { 22 | @each $padding, $margin in (p, m), (pt, mt), (pr, mr), (pb, mb), (pl, ml) { 23 | .#{$padding}-#{$value} { 24 | @if $padding == p { 25 | padding: #{$value}px; 26 | } @else if $padding == pt { 27 | padding-top: #{$value}px; 28 | } @else if $padding == pr { 29 | padding-right: #{$value}px; 30 | } @else if $padding == pb { 31 | padding-bottom: #{$value}px; 32 | } @else if $padding == pl { 33 | padding-left: #{$value}px; 34 | } 35 | } 36 | .#{$margin}-#{$value} { 37 | @if $margin == m { 38 | margin: #{$value}px; 39 | } @else if $margin == mt { 40 | margin-top: #{$value}px; 41 | } @else if $margin == mr { 42 | margin-right: #{$value}px; 43 | } @else if $margin == mb { 44 | margin-bottom: #{$value}px; 45 | } @else if $margin == ml { 46 | margin-left: #{$value}px; 47 | } 48 | } 49 | } 50 | } 51 | 52 | /** justify-content、align-items */ 53 | @each $direction, $first, $last in (h, flex, start), (h, flex, end), (h, space, around), 54 | (h, space, between), (h, space, evenly), (h, false, center), (v, flex, start), (v, flex, end), 55 | (v, false, center) 56 | { 57 | .flex-#{$direction}#{$last} { 58 | @if $direction == h { 59 | @if $first { 60 | justify-content: #{$first}-#{$last}; 61 | } @else { 62 | justify-content: #{$last}; 63 | } 64 | } @else { 65 | @if $first { 66 | align-items: #{$first}-#{$last}; 67 | } @else { 68 | align-items: #{$last}; 69 | } 70 | } 71 | } 72 | } 73 | 74 | /** hover */ 75 | .hover { 76 | cursor: pointer; 77 | 78 | &:hover { 79 | color: var(--el-color-primary); 80 | } 81 | } 82 | 83 | /** height */ 84 | .h-100 { 85 | height: 100%; 86 | } 87 | 88 | .h-0 { 89 | height: 0; 90 | } 91 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 环境变量 */ 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true 7 | }, 8 | 9 | /** 全局变量,避免 no-undef 规则发出警告 */ 10 | globals: { 11 | ref: 'readonly', 12 | reactive: 'readonly', 13 | computed: 'readonly', 14 | readonly: 'readonly', 15 | watchEffect: 'readonly', 16 | watchPostEffect: 'readonly', 17 | watchSyncEffect: 'readonly', 18 | watch: 'readonly', 19 | isRef: 'readonly', 20 | unref: 'readonly', 21 | toRef: 'readonly', 22 | toRefs: 'readonly', 23 | isProxy: 'readonly', 24 | isReactive: 'readonly', 25 | isReadonly: 'readonly', 26 | onMounted: 'readonly', 27 | onUpdated: 'readonly', 28 | onUnmounted: 'readonly', 29 | onBeforeMount: 'readonly', 30 | onBeforeUpdate: 'readonly', 31 | onBeforeUnmount: 'readonly', 32 | onErrorCaptured: 'readonly', 33 | onRenderTracked: 'readonly', 34 | onRenderTriggered: 'readonly', 35 | onActivated: 'readonly', 36 | onDeactivated: 'readonly', 37 | onServerPrefetch: 'readonly', 38 | defineProps: 'readonly', 39 | defineEmits: 'readonly', 40 | defineExpose: 'readonly', 41 | FormInstance: 'readonly' 42 | }, 43 | 44 | /** 插件配置,提供处理器,插件名称可以省略 eslint-plugin- 前缀 */ 45 | // 处理器从文件中提取 JS 代码,然后让 ESLint 检测 46 | plugins: ['vue', '@typescript-eslint'], 47 | // 为特定类型的文件指定处理器 48 | overrides: [], 49 | 50 | /** 解析器配置,默认解析器为 espree */ 51 | // .vue 文件中的 `