├── .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 |
2 | 个人中心
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/views/menu/menu1/menu1-2/index.vue:
--------------------------------------------------------------------------------
1 |
2 | menu1-2
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/views/menu/menu1/menu1-1/menu1-1-1/index.vue:
--------------------------------------------------------------------------------
1 |
2 | menu1-1-1
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/views/menu/menu1/menu1-1/menu1-1-2/index.vue:
--------------------------------------------------------------------------------
1 |
2 | menu1-1-2
3 |
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 |
2 | menu2
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/views/power/role/index.vue:
--------------------------------------------------------------------------------
1 |
2 | 角色管理(admin权限可见)
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/views/power/route/index.vue:
--------------------------------------------------------------------------------
1 |
2 | 路由管理(admin权限可见)
3 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | {{ props.meta.title }}
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 | 字典管理(admin、editor权限可见)
4 |
5 |
6 | 新增(admin、editor可见)
7 |
8 |
9 | 删除(admin可见)
10 |
11 |
12 | 详情(所有用户可见)
13 |
14 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ item.meta.title }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
23 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
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 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
2 |
3 |
4 |
![404]()
5 |
6 | 页面不小心走丢了~,您可以
7 |
8 |
9 |
10 |
11 | 返回首页
12 |
13 |
14 |
15 |
16 |
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 |
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 文件中的 `` 解析器
52 | parser: 'vue-eslint-parser',
53 | // 解析器选项
54 | parserOptions: {
55 | // .vue 文件中的 `
78 |
79 |
134 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { type ConfigEnv, loadEnv } from 'vite'
3 | import vue from '@vitejs/plugin-vue'
4 | import { createHtmlPlugin } from 'vite-plugin-html'
5 | import AutoImport from 'unplugin-auto-import/vite'
6 | import Components from 'unplugin-vue-components/vite'
7 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
8 | import Icons from 'unplugin-icons/vite'
9 | import IconsResolver from 'unplugin-icons/resolver'
10 |
11 | const pathResolve = (dir = ''): string => resolve(__dirname, '.', dir)
12 |
13 | export default ({ command, mode }: ConfigEnv) => {
14 | const dev = mode === 'development'
15 | const { VITE_APP_TITLE, VITE_BASE_URL, VITE_PROXY_SERVER } = loadEnv(mode, process.cwd())
16 |
17 | return {
18 | base: './',
19 | resolve: {
20 | alias: {
21 | '@': pathResolve('src')
22 | },
23 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.scss']
24 | },
25 | // 开发环境下根据`.env`配置决定是否开启代理
26 | server:
27 | dev && VITE_PROXY_SERVER === 'true'
28 | ? {
29 | port: 80,
30 | proxy: {
31 | '/api': {
32 | target: VITE_BASE_URL,
33 | rewrite: (path: string) => path.replace(/^\/api/, ''),
34 | changeOrigin: true
35 | }
36 | },
37 | host: '0.0.0.0',
38 | cors: true
39 | }
40 | : {},
41 | plugins: [
42 | vue(),
43 | // html 插件
44 | createHtmlPlugin({
45 | // 在这里写 entry 后,你将不需要在`index.html`内添加 script 标签,原有标签需要删除
46 | entry: 'src/main.ts',
47 | // 需要注入`index.html`模版的数据
48 | inject: {
49 | data: {
50 | title: VITE_APP_TITLE,
51 | logo: 'src/assets/images/common/logo.svg'
52 | }
53 | }
54 | }),
55 | // 自动导入 vue 、element-plus 方法
56 | AutoImport({
57 | imports: ['vue'],
58 | resolvers: [ElementPlusResolver()],
59 | dts: pathResolve('types/auto-imports.d.ts')
60 | }),
61 | // 自动导入 element-plus、icon 组件
62 | Components({
63 | resolvers: [
64 | ElementPlusResolver(),
65 | IconsResolver({
66 | // iconify 图标库,ep 代表 element-plus
67 | enabledCollections: ['ep'],
68 | // 图标前缀,默认 I
69 | prefix: 'Icon'
70 | })
71 | ],
72 | dts: pathResolve('types/components.d.ts')
73 | }),
74 | // 自动导入图标配置
75 | Icons({
76 | autoInstall: true
77 | })
78 | ],
79 | css: {
80 | modules: {
81 | scopeBehaviour: 'local'
82 | },
83 | preprocessorOptions: {
84 | scss: {
85 | // 导入全局 scss 使用 @use 替代 @import,因为 sass 团队说他们最终会删除 @import 语法
86 | additionalData: `
87 | @use "@/styles/element-theme.scss" as *;
88 | @use "@/styles/common.scss" as *;
89 | `,
90 | javascriptEnabled: true,
91 | charset: false
92 | }
93 | }
94 | },
95 | build: {
96 | // 默认 'modules',指支持原生 es 模块的浏览器,最低 es2015
97 | target: 'es2015',
98 | // 禁用 map 文件
99 | sourcemap: false,
100 | // 禁用 brotli 压缩大小报告
101 | brotliSize: false,
102 | // 消除打包大小超过500kb警告
103 | chunkSizeWarningLimit: 2000,
104 | // 防止 vite 将 rgba() 颜色转化为 #RGBA
105 | cssTarget: 'chrome61',
106 | // css 打包分割
107 | cssCodeSplit: true,
108 | // 小于此阈值(4kb)的导入或引用资源将内联为 base64 编码
109 | assetsInlineLimit: 4096,
110 | rollupOptions: {
111 | output: {
112 | // 页面分块
113 | manualChunks: {
114 | // ...
115 | }
116 | }
117 | }
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createRouter,
3 | createWebHashHistory,
4 | createWebHistory,
5 | type RouteRecordRaw
6 | } from 'vue-router'
7 |
8 | const { VITE_HISTORY_MODE } = import.meta.env
9 | const Layout = () => import('@/layout/index.vue')
10 |
11 | /** 常驻路由 */
12 | export const routes: RouteRecordRaw[] = [
13 | {
14 | path: '/',
15 | component: Layout,
16 | children: [
17 | {
18 | path: '/',
19 | name: 'home',
20 | component: () => import('@/views/home/index.vue'),
21 | meta: {
22 | title: '首页',
23 | menu: true
24 | }
25 | },
26 | {
27 | path: 'center',
28 | name: 'center',
29 | component: () => import('@/views/center/index.vue'),
30 | meta: {
31 | title: '个人中心'
32 | }
33 | }
34 | ]
35 | },
36 | {
37 | path: '/login',
38 | name: 'login',
39 | component: () => import('@/views/login/index.vue'),
40 | meta: {
41 | title: '登录'
42 | }
43 | },
44 | {
45 | path: '/:pathMatch(.*)*',
46 | name: '404',
47 | component: () => import('@/views/error/404.vue'),
48 | meta: {
49 | title: '404'
50 | }
51 | }
52 | ]
53 |
54 | /** 动态路由 */
55 | export const asyncRoutes: RouteRecordRaw[] = [
56 | {
57 | path: '/menu',
58 | name: 'menu',
59 | component: Layout,
60 | redirect: {
61 | name: 'menu2'
62 | },
63 | meta: {
64 | title: 'menu',
65 | subtitle: '多层级菜单',
66 | menu: true
67 | },
68 | children: [
69 | {
70 | path: 'menu1',
71 | name: 'menu1',
72 | meta: {
73 | title: 'menu1',
74 | icon: 'coffee-cup'
75 | },
76 | children: [
77 | {
78 | path: 'menu1-1',
79 | name: 'menu1-1',
80 | meta: {
81 | title: 'menu1-1',
82 | icon: 'milk-tea'
83 | },
84 | children: [
85 | {
86 | path: 'menu1-1-1',
87 | name: 'menu1-1-1',
88 | component: () => import('@/views/menu/menu1/menu1-1/menu1-1-1/index.vue'),
89 | meta: {
90 | title: 'menu1-1-1'
91 | }
92 | },
93 | {
94 | path: 'menu1-1-2',
95 | name: 'menu1-1-2',
96 | component: () => import('@/views/menu/menu1/menu1-1/menu1-1-2/index.vue'),
97 | meta: {
98 | title: 'menu1-1-2'
99 | }
100 | }
101 | ]
102 | },
103 | {
104 | path: 'menu1-2',
105 | name: 'menu1-2',
106 | component: () => import('@/views/menu/menu1/menu1-2/index.vue'),
107 | meta: {
108 | title: 'menu1-2',
109 | icon: 'coffee'
110 | }
111 | }
112 | ]
113 | },
114 | {
115 | path: 'menu2',
116 | name: 'menu2',
117 | component: () => import('@/views/menu/menu2/index.vue'),
118 | meta: {
119 | title: 'menu2',
120 | icon: 'pear'
121 | }
122 | }
123 | ]
124 | },
125 | {
126 | path: '/power',
127 | name: 'power',
128 | component: Layout,
129 | redirect: {
130 | name: 'dict'
131 | },
132 | meta: {
133 | title: '权限',
134 | subtitle: '权限管理',
135 | roles: ['admin', 'editor'],
136 | menu: true
137 | },
138 | children: [
139 | {
140 | path: 'dict',
141 | name: 'dict',
142 | component: () => import('@/views/power/dict/index.vue'),
143 | meta: {
144 | title: '字典管理',
145 | icon: 'collection',
146 | roles: ['admin', 'editor']
147 | }
148 | },
149 | {
150 | path: 'role',
151 | name: 'role',
152 | component: () => import('@/views/power/role/index.vue'),
153 | meta: {
154 | title: '角色管理',
155 | icon: 'user',
156 | roles: ['admin']
157 | }
158 | },
159 | {
160 | path: 'route',
161 | name: 'route',
162 | component: () => import('@/views/power/route/index.vue'),
163 | meta: {
164 | title: '路由管理',
165 | icon: 'location',
166 | roles: ['admin']
167 | }
168 | }
169 | ]
170 | }
171 | ]
172 |
173 | /** 创建 router 实例 */
174 | const router = createRouter({
175 | history: VITE_HISTORY_MODE === 'hash' ? createWebHashHistory() : createWebHistory(),
176 | routes,
177 | scrollBehavior(to, from, savedPosition) {
178 | // 保存返回页面滚动位置
179 | // if (savedPosition) {
180 | // return savedPosition
181 | // } else {
182 | // return { top: 0 }
183 | // }
184 |
185 | // 每次回到顶部
186 | return { top: 0 }
187 | }
188 | })
189 |
190 | export default router
191 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![logo]()
5 |
{{ VITE_APP_DESCRIBE }}
6 |
7 |
8 |
59 |
60 |
61 |
62 |
127 |
128 |
215 |
--------------------------------------------------------------------------------
/src/layout/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
16 |
17 |
42 |
43 |
44 |
45 |
116 |
117 |
227 |
--------------------------------------------------------------------------------
/src/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{ userAccount }}
8 |
9 |
10 | {{ item.title }}
11 | 去查看
18 |
19 |
20 |
21 |
22 |
23 |
24 |
数据概览
25 |
26 | 时间段:
27 |
35 |
36 |
37 |
38 |
44 | {{ item.title }}
45 | {{ item.total }}
46 | 环比上月{{ item.percent }}
55 |
56 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
130 |
131 |
256 |
--------------------------------------------------------------------------------
/src/assets/images/error/404.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/error/403.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------