update
This commit is contained in:
parent
668cfa8095
commit
e570a7e509
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
<title>Vue3 Element Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -3,6 +3,7 @@ export default [
|
||||
url: "/api/login",
|
||||
method: "post",
|
||||
timeout: 1000,
|
||||
statusCode: 200,
|
||||
response: {
|
||||
code: 200,
|
||||
message: "登录成功",
|
||||
|
||||
38
mock/menu.js
Normal file
38
mock/menu.js
Normal file
@ -0,0 +1,38 @@
|
||||
export default [
|
||||
{
|
||||
url: "/api/menus",
|
||||
method: "get",
|
||||
timeout: 100,
|
||||
response: {
|
||||
code: 200,
|
||||
message: "获取菜单成功",
|
||||
data: [
|
||||
{
|
||||
url: '/test',
|
||||
title: '测试页面',
|
||||
icon: 'el-icon-location',
|
||||
children: [
|
||||
{
|
||||
url: '/test',
|
||||
title: '列表',
|
||||
},
|
||||
{
|
||||
url: '/test/auth',
|
||||
title: '权限页面',
|
||||
},
|
||||
{
|
||||
url: '/test/nest',
|
||||
title: '二级菜单',
|
||||
children: [
|
||||
{
|
||||
url: '/test/nest',
|
||||
title: '子菜单',
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
1731
package-lock.json
generated
1731
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -28,10 +28,11 @@
|
||||
"@vue/compiler-sfc": "^3.0.5",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"element-plus": "^1.0.2-beta.35",
|
||||
"less": "^4.1.1",
|
||||
"mockjs": "^1.1.0",
|
||||
"sass": "^1.32.8",
|
||||
"vite": "^2.1.0",
|
||||
"vite-plugin-mock": "^2.3.0"
|
||||
"vite-plugin-mock": "^2.3.0",
|
||||
"vite-plugin-svg-icons": "^0.4.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
<style lang="less">
|
||||
<style lang="scss">
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
|
||||
10
src/api/menu.js
Normal file
10
src/api/menu.js
Normal file
@ -0,0 +1,10 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
|
||||
// 获取菜单
|
||||
export const GetMenus = () => {
|
||||
return request({
|
||||
url: "/api/menus",
|
||||
method: "get"
|
||||
});
|
||||
};
|
||||
1
src/assets/error-icons/403.svg
Normal file
1
src/assets/error-icons/403.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/error-icons/404.svg
Normal file
1
src/assets/error-icons/404.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.9 KiB |
1
src/assets/error-icons/500.svg
Normal file
1
src/assets/error-icons/500.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
7
src/assets/style/element-variables.scss
Normal file
7
src/assets/style/element-variables.scss
Normal file
@ -0,0 +1,7 @@
|
||||
/* 改变主题色变量 */
|
||||
$--color-primary: $mainColor;
|
||||
|
||||
/* 改变 icon 字体路径变量,必需 */
|
||||
$--font-path: "element-plus/lib/theme-chalk/fonts";
|
||||
|
||||
@import "element-plus/packages/theme-chalk/src/index";
|
||||
17
src/assets/style/global-variables.scss
Normal file
17
src/assets/style/global-variables.scss
Normal file
@ -0,0 +1,17 @@
|
||||
// 该文件中的变量是全局变量,在css文件和vue组件中可以直接使用
|
||||
|
||||
$mainColor: #409eff; // 网站主题色
|
||||
|
||||
// 菜单配置
|
||||
$menuBg: #304156; // 菜单背景颜色
|
||||
$menuTextColor: #fff; // 菜单文字颜色
|
||||
$menuActiveTextColor: $mainColor; // 已选中菜单文字颜色
|
||||
$menuActiveBg: none; // 已选中菜单背景颜色
|
||||
$menuHover: #263445; // 鼠标经过菜单时的背景颜色
|
||||
$subMenuBg: #1f2d3d; // 子菜单背景颜色
|
||||
$subMenuHover: #001528; // 鼠标经过子菜单时的背景颜色
|
||||
$collapseMenuActiveBg: #1f2d3d; // 菜单宽度折叠后,已选中菜单的背景颜色
|
||||
$collapseMenuActiveColor: $menuTextColor; // 菜单宽度折叠后,已选中菜单的文字颜色
|
||||
$collapseMenuActiveBorderBg: $mainColor; // 菜单宽度折叠后,已选中菜单的边框颜色
|
||||
$collapseMenuActiveBorderWidth: 2px; // 菜单宽度折叠后,已选中菜单的边框宽度
|
||||
$arrowColor: #909399; // 展开/收起箭头颜色
|
||||
1
src/assets/svg/home.svg
Normal file
1
src/assets/svg/home.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M510.72 41.856l435.2 361.024a32 32 0 0 1-36.16 52.48l-4.736-3.2-41.024-34.048V832c0 48.832-32.64 90.752-76.864 95.552l-8.448 0.448H245.312c-45.696 0-80.96-39.04-84.928-86.912L160 832V407.808l-55.04 44.608a32 32 0 0 1-40.96-0.64l-4.032-4.16a32 32 0 0 1 0.64-40.96l4.096-4.032L510.784 41.856z m-0.512 82.688L223.232 356.672a31.808 31.808 0 0 1 0.256 1.152l0.512 5.76V832c0 16.768 8.64 28.992 17.92 31.552l3.392 0.448h533.376c9.216 0 18.88-10.432 20.928-25.92L800 832V364.992L510.208 124.544z" /></svg>
|
||||
|
After Width: | Height: | Size: 766 B |
@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<p>
|
||||
<a href="https://vitejs.dev/guide/features.html" target="_blank">Vite Documentation</a> |
|
||||
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Documentation</a>
|
||||
</p>
|
||||
|
||||
<button @click="state.count++">count is: {{ state.count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test hot module replacement.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, reactive } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String
|
||||
})
|
||||
|
||||
const state = reactive({ count: 0 })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
40
src/components/SvgIcon/index.vue
Normal file
40
src/components/SvgIcon/index.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<svg
|
||||
class="icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, computed } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SvgIcon",
|
||||
props: {
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "icon",
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
|
||||
return { symbolId };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
1
src/globalComponents.js
Normal file
1
src/globalComponents.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as SvgIcon } from "@/components/SvgIcon/index.vue";
|
||||
37
src/layout/components/Sidebar/Item.vue
Normal file
37
src/layout/components/Sidebar/Item.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<i
|
||||
v-if="isElementIcon"
|
||||
:class="`icon ${icon}`"
|
||||
/>
|
||||
<svg-icon
|
||||
class="icon"
|
||||
v-else-if="!!icon"
|
||||
:name="icon"
|
||||
/>
|
||||
<span>{{title}}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: ["title", "icon"],
|
||||
setup({ icon }) {
|
||||
const isElementIcon = computed(() => icon && icon.startsWith("el-icon"));
|
||||
|
||||
return {
|
||||
isElementIcon,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: currentColor;
|
||||
}
|
||||
</style>
|
||||
@ -5,7 +5,7 @@
|
||||
src="~@/assets/logo.png"
|
||||
@click="goHome"
|
||||
>
|
||||
<div class="title">ERP管理系统</div>
|
||||
<div class="title">Vue3 Element Admin</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -22,7 +22,7 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.brand {
|
||||
height: 48px;
|
||||
padding: 0 8px;
|
||||
@ -36,7 +36,7 @@ export default defineComponent({
|
||||
}
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
margin-left: 8px;
|
||||
|
||||
@ -6,12 +6,16 @@
|
||||
:uniqueOpened="true"
|
||||
:router="true"
|
||||
:default-active="activePath"
|
||||
background-color="#2d3a4b"
|
||||
text-color="#fff"
|
||||
active-text-color="#fff"
|
||||
:background-color="variables.menuBg"
|
||||
:text-color="variables.menuTextColor"
|
||||
:active-text-color="variables.menuActiveTextColor"
|
||||
>
|
||||
|
||||
<submenu :menus="menus" />
|
||||
<submenu
|
||||
v-for="menu in menus"
|
||||
:key="menu.url"
|
||||
:menu="menu"
|
||||
/>
|
||||
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
@ -21,6 +25,7 @@ import { computed, defineComponent } from "vue";
|
||||
import Submenu from "./Submenu.vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useRoute } from "vue-router";
|
||||
import config from "./config/menu.module.scss";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -33,19 +38,70 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const menus = computed(() => store.state.menu.menus);
|
||||
const activePath = computed(() => route.path);
|
||||
const store = useStore();
|
||||
store.dispatch(
|
||||
"menu/generateMenus",
|
||||
store.state.account.userinfo && store.state.account.userinfo.role
|
||||
);
|
||||
|
||||
return {
|
||||
menus,
|
||||
activePath,
|
||||
menus: computed(() => store.state.menu.menus),
|
||||
activePath: computed(() => route.path),
|
||||
variables: computed(() => config),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss">
|
||||
// menu hover
|
||||
.el-menu-item,
|
||||
.el-submenu__title {
|
||||
&:hover {
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu {
|
||||
.el-menu-item,
|
||||
.el-submenu .el-submenu__title {
|
||||
background-color: $subMenuBg !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $subMenuHover !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu-item.is-active {
|
||||
background-color: $menuActiveBg !important;
|
||||
&:hover {
|
||||
background-color: $menuActiveBg !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
.el-menu-item.is-active,
|
||||
.el-submenu.is-active > .el-submenu__title {
|
||||
position: relative;
|
||||
background-color: $collapseMenuActiveBg !important;
|
||||
color: $collapseMenuActiveColor !important;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: $collapseMenuActiveBorderWidth;
|
||||
height: 100%;
|
||||
background-color: $collapseMenuActiveBorderBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu__title i {
|
||||
color: $arrowColor;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.scroll {
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
@ -54,7 +110,4 @@ export default defineComponent({
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
::v-deep(.el-menu-item.is-active) {
|
||||
background: #0174df !important;
|
||||
}
|
||||
</style>
|
||||
@ -1,30 +1,49 @@
|
||||
<template>
|
||||
<el-submenu
|
||||
<el-menu-item
|
||||
v-if="!menu.children"
|
||||
:index="menu.url"
|
||||
>
|
||||
<item
|
||||
:icon="menu.icon"
|
||||
:title="menu.title"
|
||||
/>
|
||||
</el-menu-item>
|
||||
<el-submenu
|
||||
v-else
|
||||
:index="menu.url"
|
||||
v-for="(menu, index) in menus"
|
||||
:key="index"
|
||||
>
|
||||
<template #title>
|
||||
<i class="el-icon-location"></i>
|
||||
<span>{{menu.title}}</span>
|
||||
<item
|
||||
:icon="menu.icon"
|
||||
:title="menu.title"
|
||||
/>
|
||||
|
||||
</template>
|
||||
<el-menu-item
|
||||
:index="submenu.url"
|
||||
v-for="(submenu, subindex) in menu.children"
|
||||
:key="subindex"
|
||||
>{{submenu.title}}</el-menu-item>
|
||||
<submenu
|
||||
v-for="submenu in menu.children"
|
||||
:key="submenu.url"
|
||||
:is-nest="true"
|
||||
:menu="submenu"
|
||||
/>
|
||||
</el-submenu>
|
||||
</template>
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import Item from "./Item.vue";
|
||||
export default defineComponent({
|
||||
name: "Submenu",
|
||||
components: {
|
||||
Item,
|
||||
},
|
||||
props: {
|
||||
menus: {
|
||||
type: Array,
|
||||
menu: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
5
src/layout/components/Sidebar/config/menu.module.scss
Normal file
5
src/layout/components/Sidebar/config/menu.module.scss
Normal file
@ -0,0 +1,5 @@
|
||||
:export {
|
||||
menuBg: $menuBg;
|
||||
menuTextColor: $menuTextColor;
|
||||
menuActiveTextColor: $menuActiveTextColor;
|
||||
}
|
||||
@ -30,10 +30,10 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.left {
|
||||
width: 210px;
|
||||
background: #2d3a4b;
|
||||
background: $menuBg;
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="tabs"></div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.tabs {
|
||||
height: 32px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
>
|
||||
<el-breadcrumb-item
|
||||
v-for="(item, index) in breadcrumbs"
|
||||
:key="item.path"
|
||||
:key="index"
|
||||
:class="{no_link: index === breadcrumbs.length - 1}"
|
||||
:to="index < breadcrumbs.length - 1 ? item.path : ''"
|
||||
>
|
||||
@ -56,7 +56,7 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumb {
|
||||
margin-left: 10px;
|
||||
::v-deep(a),
|
||||
|
||||
@ -22,7 +22,7 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.fold-btn {
|
||||
line-height: 48px;
|
||||
padding: 0 10px;
|
||||
|
||||
@ -29,9 +29,10 @@ export default defineComponent({
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const userinfo = computed(() => store.state.login.userinfo);
|
||||
const userinfo = computed(() => store.state.account.userinfo);
|
||||
const logout = () => {
|
||||
store.commit("app/clearToken");
|
||||
store.commit("account/clearUserinfo");
|
||||
router.push("/login");
|
||||
};
|
||||
return {
|
||||
@ -42,7 +43,7 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.userinfo {
|
||||
padding: 0 16px;
|
||||
line-height: 48px;
|
||||
|
||||
@ -23,7 +23,7 @@ export default defineComponent({
|
||||
setup() {},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
|
||||
@ -27,7 +27,7 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
26
src/main.js
26
src/main.js
@ -1,14 +1,28 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
// 引入路由
|
||||
import router from './router'
|
||||
// 引入store
|
||||
import store from './store'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 引入element-plus
|
||||
import ElementPlus from "element-plus";
|
||||
import "element-plus/lib/theme-chalk/index.css";
|
||||
import './assets/style/element-variables.scss'
|
||||
|
||||
// 引入路由
|
||||
import router from './router'
|
||||
|
||||
// 引入store
|
||||
import store from './store'
|
||||
|
||||
// 权限控制
|
||||
import './permission'
|
||||
|
||||
createApp(App).use(store).use(router).use(ElementPlus).mount('#app')
|
||||
// 引入svg图标注册脚本
|
||||
import 'vite-plugin-svg-icons/register';
|
||||
|
||||
// 注册全局组件
|
||||
import * as Components from './globalComponents'
|
||||
Object.entries(Components).forEach(([key, component]) => {
|
||||
app.component(key, component)
|
||||
})
|
||||
|
||||
app.use(ElementPlus).use(store).use(router).mount('#app')
|
||||
|
||||
@ -1,32 +1,49 @@
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import { TOKEN } from '@/store/modules/app' // TOKEN变量名
|
||||
// 白名单,里面是路由对象的name
|
||||
const WhiteList = ['login']
|
||||
|
||||
// vue-router4的路由守卫不再是通过next放行,而是通过return返回false或者一个路由地址
|
||||
const getPageTitle = title => {
|
||||
const appTitle = store.state.app.title;
|
||||
if (title) {
|
||||
return `${title} - ${appTitle}`
|
||||
}
|
||||
return appTitle
|
||||
}
|
||||
|
||||
// 白名单,里面是路由对象的name
|
||||
const WhiteList = ['login', 'forbidden', 'server-error', 'not-found']
|
||||
|
||||
// vue-router4的路由守卫不再是通过next放行,而是通过return返回true或false或者一个路由地址
|
||||
router.beforeEach(async (to) => {
|
||||
|
||||
document.title = getPageTitle(!!to.meta && to.meta.title)
|
||||
|
||||
if (WhiteList.includes(to.name)) {
|
||||
return true
|
||||
}
|
||||
if (!window.localStorage[TOKEN]) {
|
||||
if (!WhiteList.includes(to.name)) {
|
||||
return {
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.path // redirect是指登录之后可以跳回到redirect指定的页面
|
||||
},
|
||||
replace: true
|
||||
}
|
||||
return {
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: to.path // redirect是指登录之后可以跳回到redirect指定的页面
|
||||
},
|
||||
replace: true
|
||||
}
|
||||
} else {
|
||||
if (!store.state.login.userinfo) {
|
||||
// 获取用户信息,根据用户角色生成菜单和动态路由
|
||||
const userinfo = await store.dispatch("login/getUserinfo");
|
||||
const routes = await store.dispatch("menu/generateMenus", userinfo && userinfo.role)
|
||||
routes.forEach((item) => {
|
||||
router.addRoute(item);
|
||||
});
|
||||
return to.fullPath
|
||||
}
|
||||
let userinfo = store.state.account.userinfo
|
||||
if (!userinfo) {
|
||||
try {
|
||||
// 获取用户信息
|
||||
userinfo = await store.dispatch("account/getUserinfo");
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
// 如果没有权限,跳转到403页面
|
||||
if (!!to.meta && !!to.meta.roles && !to.meta.roles.includes(userinfo.role)) {
|
||||
return { path: '/403', replace: true }
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
@ -1,13 +1,16 @@
|
||||
// index.js
|
||||
import { createRouter, createWebHashHistory } from "vue-router"
|
||||
|
||||
import error from './modules/error'
|
||||
import login from './modules/login'
|
||||
import home from './modules/home'
|
||||
import user from './modules/user'
|
||||
import test from './modules/test'
|
||||
|
||||
|
||||
export const AllMenus = [
|
||||
...user
|
||||
// 左侧菜单(左侧菜单请配置在此,否则无法显示)
|
||||
export const allMenus = [
|
||||
...home,
|
||||
...test,
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
@ -18,7 +21,8 @@ const router = createRouter({
|
||||
redirect: '/home',
|
||||
},
|
||||
...login,
|
||||
...home,
|
||||
...allMenus,
|
||||
...error
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
60
src/router/modules/error.js
Normal file
60
src/router/modules/error.js
Normal file
@ -0,0 +1,60 @@
|
||||
import layout from '@/layout/index.vue'
|
||||
const Error = () => import("@/views/error/index.vue");
|
||||
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/error',
|
||||
component: layout,
|
||||
children: [
|
||||
{
|
||||
path: '403',
|
||||
name: 'error-forbidden',
|
||||
component: Error,
|
||||
props: {
|
||||
error: '403'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '500',
|
||||
name: 'error-server-error',
|
||||
component: Error,
|
||||
props: {
|
||||
error: '500'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
name: 'error-not-found',
|
||||
component: Error,
|
||||
props: {
|
||||
error: '404'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'forbidden',
|
||||
component: Error,
|
||||
props: {
|
||||
error: '403'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: 'server-error',
|
||||
component: Error,
|
||||
props: {
|
||||
error: '500'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
component: Error,
|
||||
props: {
|
||||
error: '404'
|
||||
}
|
||||
},
|
||||
]
|
||||
@ -6,6 +6,11 @@ export default [
|
||||
{
|
||||
path: '/home',
|
||||
component: layout,
|
||||
name: "Dashboard",
|
||||
meta: {
|
||||
title: "Dashboard",
|
||||
},
|
||||
icon: 'home',
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
|
||||
80
src/router/modules/test.js
Normal file
80
src/router/modules/test.js
Normal file
@ -0,0 +1,80 @@
|
||||
import layout from '@/layout/index.vue'
|
||||
const List = () => import("@/views/test/index.vue");
|
||||
const Add = () => import("@/views/test/Add.vue");
|
||||
const Auth = () => import("@/views/test/Auth.vue");
|
||||
const Nest = () => import("@/views/test/Nest.vue");
|
||||
const NestPage1 = () => import("@/views/test/nest/Page1.vue");
|
||||
const NestPage2 = () => import("@/views/test/nest/Page2.vue");
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/test',
|
||||
component: layout,
|
||||
name: "test",
|
||||
meta: {
|
||||
title: "测试页面",
|
||||
},
|
||||
icon: 'el-icon-location',
|
||||
roles: ["admin", "visitor"],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "testList",
|
||||
component: List,
|
||||
meta: {
|
||||
title: "列表",
|
||||
roles: ["admin", "visitor"],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "add",
|
||||
name: "testAdd",
|
||||
component: Add,
|
||||
meta: {
|
||||
title: "添加",
|
||||
roles: ["admin", "visitor"],
|
||||
},
|
||||
hidden: true, // 不在菜单中显示
|
||||
},
|
||||
{
|
||||
path: "auth",
|
||||
name: "testAuth",
|
||||
component: Auth,
|
||||
meta: {
|
||||
title: "权限页面",
|
||||
roles: ["admin"],
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "nest",
|
||||
name: "nest",
|
||||
component: Nest,
|
||||
redirect: '/test/nest/page1',
|
||||
meta: {
|
||||
title: "二级菜单",
|
||||
roles: ["admin", "visitor"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "page1",
|
||||
name: "nestPage1",
|
||||
component: NestPage1,
|
||||
meta: {
|
||||
title: "page1",
|
||||
roles: ["admin", "visitor"],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "page2",
|
||||
name: "nestPage2",
|
||||
component: NestPage2,
|
||||
meta: {
|
||||
title: "page2",
|
||||
roles: ["admin", "visitor"],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,36 +0,0 @@
|
||||
import layout from '@/layout/index.vue'
|
||||
const UserList = () => import("@/views/user/index.vue");
|
||||
const AddUser = () => import("@/views/user/AddUser.vue");
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/user',
|
||||
component: layout,
|
||||
name: "user",
|
||||
meta: {
|
||||
title: "用户管理",
|
||||
},
|
||||
roles: ["admin", "visitor"],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "userList",
|
||||
component: UserList,
|
||||
meta: {
|
||||
title: "用户列表",
|
||||
},
|
||||
roles: ["admin", "visitor"],
|
||||
},
|
||||
{
|
||||
path: "add",
|
||||
name: "addUser",
|
||||
component: AddUser,
|
||||
meta: {
|
||||
title: "添加用户"
|
||||
},
|
||||
hidden: true,
|
||||
roles: ["admin"],
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,13 +1,13 @@
|
||||
//index.js
|
||||
import { createStore } from "vuex";
|
||||
import app from "./modules/app";
|
||||
import login from "./modules/login";
|
||||
import account from "./modules/account";
|
||||
import menu from "./modules/menu";
|
||||
|
||||
export default createStore({
|
||||
modules: {
|
||||
app,
|
||||
login,
|
||||
account,
|
||||
menu
|
||||
},
|
||||
});
|
||||
@ -8,6 +8,9 @@ export default {
|
||||
mutations: {
|
||||
setUserinfo (state, data) {
|
||||
state.userinfo = data;
|
||||
},
|
||||
clearUserinfo (state) {
|
||||
state.userinfo = null;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@ -4,6 +4,7 @@ export const TOKEN = "TOKEN";
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
title: 'Vue3 Element Admin',
|
||||
authorization: getItem(TOKEN),
|
||||
sidebar: {
|
||||
collapse: getItem('collapse')
|
||||
|
||||
@ -1,35 +1,39 @@
|
||||
import { AllMenus } from '@/router'
|
||||
import { allMenus } from '@/router'
|
||||
import { GetMenus } from '@/api/menu'
|
||||
|
||||
const hasPermission = (role, route) => {
|
||||
if (!!route.roles && !route.roles.includes(role)) {
|
||||
if (!!route.meta && !!route.meta.roles && !route.meta.roles.includes(role)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const getRoleMenus = (arr, role, parentPath = '') => {
|
||||
const generateUrl = (path, parentPath) => {
|
||||
return path.startsWith('/') ? path : (!!path ? `${parentPath}/${path}` : parentPath)
|
||||
}
|
||||
|
||||
const getFilterMenus = (arr, role, parentPath = '') => {
|
||||
const menus = [];
|
||||
const routes = [];
|
||||
|
||||
arr.forEach(item => {
|
||||
if (hasPermission(role, item)) {
|
||||
if (hasPermission(role, item) && !item.hidden) {
|
||||
const menu = {
|
||||
url: item.path.startsWith('/') ? item.path : (!!item.path ? `${parentPath}/${item.path}` : parentPath),
|
||||
url: generateUrl(item.path, parentPath),
|
||||
title: item.meta.title,
|
||||
icon: item.icon,
|
||||
}
|
||||
const route = { ...item };
|
||||
if (item.children) {
|
||||
const { menus, routes } = getRoleMenus(item.children, role, menu.url)
|
||||
menu.children = menus
|
||||
route.children = routes
|
||||
if (item.children.length === 1) {
|
||||
menu.url = generateUrl(item.children[0].path, menu.url)
|
||||
} else {
|
||||
menu.children = getFilterMenus(item.children, role, menu.url)
|
||||
}
|
||||
}
|
||||
menus.push(menu)
|
||||
routes.push(route)
|
||||
}
|
||||
})
|
||||
|
||||
return { menus, routes }
|
||||
return menus
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -43,14 +47,17 @@ export default {
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
generateMenus ({ commit }, role) {
|
||||
if (!role || role === 'admin') {
|
||||
commit('SET_MENUS', AllMenus)
|
||||
} else {
|
||||
const { menus, routes } = getRoleMenus(AllMenus, role)
|
||||
commit('SET_MENUS', menus)
|
||||
return Promise.resolve(routes)
|
||||
}
|
||||
async generateMenus ({ commit }, role) {
|
||||
// 方式一:根据角色生成菜单
|
||||
const menus = getFilterMenus(allMenus, role)
|
||||
commit('SET_MENUS', menus)
|
||||
|
||||
// // 方式二:从后台获取菜单
|
||||
// const { code, data } = await GetMenus();
|
||||
// if (+code === 200) {
|
||||
// commit('SET_MENUS', data)
|
||||
// }
|
||||
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -12,7 +12,7 @@ const service = axios.create({
|
||||
// 拦截请求
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
const authorization = store.state.app;
|
||||
const { authorization } = store.state.app;
|
||||
if (authorization) {
|
||||
config.headers.Authorization = `Bearer ${authorization.token}`;
|
||||
}
|
||||
@ -41,13 +41,14 @@ service.interceptors.response.use(
|
||||
router.push("/login");
|
||||
|
||||
// 代码不要往后执行了
|
||||
return;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
// 如果有refresh_token,则请求获取新的 token
|
||||
try {
|
||||
const res = await axios({
|
||||
method: "PUT",
|
||||
url: "/api/authorizations",
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Authorization: `Bearer ${authorization.refresh_token}`,
|
||||
},
|
||||
@ -64,14 +65,16 @@ service.interceptors.response.use(
|
||||
return service(error.config);
|
||||
} catch (err) {
|
||||
// 如果获取失败,直接跳转 登录页
|
||||
// console.log('请求刷线 token 失败', err)
|
||||
// console.log('请求刷新 token 失败', err)
|
||||
router.push("/login");
|
||||
// 清除token
|
||||
store.commit("app/clearToken")
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.error(error.response.message);
|
||||
// console.dir(error) // 可在此进行错误上报
|
||||
ElMessage.error(error.message);
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
77
src/views/error/index.vue
Normal file
77
src/views/error/index.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="error">
|
||||
<template v-if="error === '403'">
|
||||
<span class="code-403">403</span>
|
||||
<img
|
||||
src="~@/assets/error-icons/403.svg"
|
||||
alt=""
|
||||
class="error-img"
|
||||
>
|
||||
<h2 class="title">您无权访问此页面</h2>
|
||||
</template>
|
||||
<template v-else-if="error === '500'">
|
||||
<img
|
||||
src="~@/assets/error-icons/500.svg"
|
||||
alt=""
|
||||
class="error-img"
|
||||
>
|
||||
<h2 class="title">服务器出错了</h2>
|
||||
</template>
|
||||
<template v-else-if="error === '404'">
|
||||
<img
|
||||
src="~@/assets/error-icons/404.svg"
|
||||
alt=""
|
||||
class="error-img"
|
||||
>
|
||||
<h2 class="title">您访问的页面不存在</h2>
|
||||
</template>
|
||||
|
||||
<router-link to="/">
|
||||
<el-button type="primary">返回首页</el-button>
|
||||
</router-link>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export default defineComponent({
|
||||
props: ["error"],
|
||||
setup({ error }) {
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
if (!!store.state.account.userinfo) {
|
||||
router.replace(`/error/${error}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding-top: 48px;
|
||||
.code-403 {
|
||||
position: absolute;
|
||||
font-size: 50px;
|
||||
top: 148px;
|
||||
left: 50%;
|
||||
transform: translateX(32px);
|
||||
font-family: arial;
|
||||
color: #ee5c42;
|
||||
}
|
||||
.error-img {
|
||||
width: 320px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
margin: 32px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,3 +1,8 @@
|
||||
<template>
|
||||
home
|
||||
</template>
|
||||
<div class="home">home</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.home {
|
||||
color: $mainColor;
|
||||
}
|
||||
</style>
|
||||
@ -113,7 +113,7 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.login {
|
||||
transition: transform 1s;
|
||||
transform: scale(1);
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<template>
|
||||
addUser
|
||||
添加
|
||||
</template>
|
||||
3
src/views/test/Auth.vue
Normal file
3
src/views/test/Auth.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
权限页面
|
||||
</template>
|
||||
4
src/views/test/Nest.vue
Normal file
4
src/views/test/Nest.vue
Normal file
@ -0,0 +1,4 @@
|
||||
<template>
|
||||
<h1>二级菜单</h1>
|
||||
<router-view />
|
||||
</template>
|
||||
3
src/views/test/index.vue
Normal file
3
src/views/test/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
列表
|
||||
</template>
|
||||
@ -1,3 +1,3 @@
|
||||
<template>
|
||||
user
|
||||
Page1
|
||||
</template>
|
||||
3
src/views/test/nest/Page2.vue
Normal file
3
src/views/test/nest/Page2.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
Page2
|
||||
</template>
|
||||
@ -2,6 +2,7 @@ import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from "path";
|
||||
import { viteMockServe } from "vite-plugin-mock";
|
||||
import viteSvgIcons from 'vite-plugin-svg-icons';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default env => {
|
||||
@ -15,7 +16,20 @@ export default env => {
|
||||
localEnabled: env.mode === "mock", // 指定在mock模式下才启动mock服务(可以在package.json的启动命令中指定mode为mock)
|
||||
supportTs: false, // mockPath目录中的文件是否支持ts文件,现在我们不使用ts,所以设为false
|
||||
}),
|
||||
viteSvgIcons({
|
||||
// 指定需要缓存的图标文件夹
|
||||
iconDirs: [path.resolve(__dirname, 'src/assets/svg')],
|
||||
// 指定symbolId格式
|
||||
symbolId: 'icon-[dir]-[name]',
|
||||
}),
|
||||
],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@import "./src/assets/style/global-variables.scss";' // 全局变量
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
|
||||
18
项目开发手册.md
18
项目开发手册.md
@ -633,7 +633,7 @@ server: {
|
||||
// 拦截请求
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
const authorization = store.state.app;
|
||||
const { authorization } = store.state.app;
|
||||
if (authorization) {
|
||||
config.headers.Authorization = `Bearer ${authorization.token}`;
|
||||
}
|
||||
@ -662,13 +662,14 @@ server: {
|
||||
router.push("/login");
|
||||
|
||||
// 代码不要往后执行了
|
||||
return;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
// 如果有refresh_token,则请求获取新的 token
|
||||
try {
|
||||
const res = await axios({
|
||||
method: "PUT",
|
||||
url: "/api/authorizations",
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Authorization: `Bearer ${authorization.refresh_token}`,
|
||||
},
|
||||
@ -685,14 +686,15 @@ server: {
|
||||
return service(error.config);
|
||||
} catch (err) {
|
||||
// 如果获取失败,直接跳转 登录页
|
||||
// console.log('请求刷线 token 失败', err)
|
||||
// console.log('请求刷新 token 失败', err)
|
||||
router.push("/login");
|
||||
// 清除token
|
||||
store.commit("app/clearToken")
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.error(error.response.message);
|
||||
ElMessage.error(error.message);
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@ -760,16 +762,16 @@ server: {
|
||||
|
||||
- css预处理器
|
||||
|
||||
本项目我们使用less,先安装less
|
||||
由于element-plus使用sass开发,所以本项目我们也使用sass,先安装sass
|
||||
|
||||
```powershell
|
||||
npm install -D less
|
||||
npm install -D sass
|
||||
```
|
||||
|
||||
使用less
|
||||
使用sass
|
||||
|
||||
```html
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
```
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user