This commit is contained in:
huzhushan 2021-03-26 20:06:32 +08:00
parent be7002e3d6
commit 668cfa8095
21 changed files with 467 additions and 240 deletions

View File

@ -21,7 +21,8 @@ export default [
message: "获取用户信息成功",
data: {
id: 1,
userName: 'admin',
name: 'zhangsan',
role: 'visitor',
avatar: "@image('48x48', '#fb0a2a')"
}
},

View File

@ -1,7 +1,13 @@
{
"name": "ec-admin-vue3",
"version": "0.0.0",
"name": "erp-vue3",
"version": "1.0.0",
"author": {
"name": "huzhushan",
"email": "huzhushan@126.com",
"url": "https://github.com/huzhushan"
},
"scripts": {
"start": "npm run mock",
"dev": "vite",
"mock": "vite --mode mock",
"build": "vite build",
@ -26,5 +32,14 @@
"mockjs": "^1.1.0",
"vite": "^2.1.0",
"vite-plugin-mock": "^2.3.0"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/huzhushan/erp-vue3.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/huzhushan/erp-vue3/issues"
},
"homepage": "https://github.com/huzhushan/erp-vue3"
}

View File

@ -1,8 +0,0 @@
import request from '@/utils/request'
// 获取用户信息
export const GetUserinfo = () => {
return request({
url: "/api/userinfo",
method: "get"
});
};

View File

@ -7,4 +7,12 @@ export const Login = data => {
method: "post",
data,
});
};
// 获取登录用户信息
export const GetUserinfo = () => {
return request({
url: "/api/userinfo",
method: "get"
});
};

View File

@ -0,0 +1,47 @@
<template>
<div class="brand">
<img
class="logo"
src="~@/assets/logo.png"
@click="goHome"
>
<div class="title">ERP管理系统</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { useRouter } from "vue-router";
export default defineComponent({
setup() {
const router = useRouter();
const goHome = () => {
router.push("/");
};
return { goHome };
},
});
</script>
<style lang="less" scoped>
.brand {
height: 48px;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: center;
.logo {
cursor: pointer;
max-width: 32px;
max-height: 32px;
}
.title {
color: #fff;
font-size: 16px;
font-weight: 700;
white-space: nowrap;
margin-left: 8px;
transition: all 0.5s;
}
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<el-scrollbar class="scroll">
<el-menu
class="menu"
:collapse="collapse"
:uniqueOpened="true"
:router="true"
:default-active="activePath"
background-color="#2d3a4b"
text-color="#fff"
active-text-color="#fff"
>
<submenu :menus="menus" />
</el-menu>
</el-scrollbar>
</template>
<script>
import { computed, defineComponent } from "vue";
import Submenu from "./Submenu.vue";
import { useStore } from "vuex";
import { useRoute } from "vue-router";
export default defineComponent({
components: {
Submenu,
},
props: {
collapse: {
type: Boolean,
default: false,
},
},
setup() {
const store = useStore();
const route = useRoute();
const menus = computed(() => store.state.menu.menus);
const activePath = computed(() => route.path);
return {
menus,
activePath,
};
},
});
</script>
<style lang="less" scoped>
.scroll {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
.menu {
border: none;
}
}
::v-deep(.el-menu-item.is-active) {
background: #0174df !important;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<el-submenu
:index="menu.url"
v-for="(menu, index) in menus"
:key="index"
>
<template #title>
<i class="el-icon-location"></i>
<span>{{menu.title}}</span>
</template>
<el-menu-item
:index="submenu.url"
v-for="(submenu, subindex) in menu.children"
:key="subindex"
>{{submenu.title}}</el-menu-item>
</el-submenu>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: {
menus: {
type: Array,
required: true,
},
},
setup() {},
});
</script>

View File

@ -3,77 +3,22 @@
class="left"
:class="{collapse:collapse}"
>
<div class="brand">
<img
class="logo"
src="~@/assets/logo.png"
>
<div class="title">ERP管理系统</div>
</div>
<el-menu
class="menu"
:collapse="collapse"
:uniqueOpened="true"
default-active="2"
background-color="#2d3a4b"
text-color="#fff"
active-text-color="#fff"
>
<el-submenu index="1">
<template #title>
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item-group>
<template #title>分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template #title>选项4</template>
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<template #title>导航二</template>
</el-menu-item>
<el-menu-item
index="3"
disabled
>
<i class="el-icon-document"></i>
<template #title>导航三</template>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<template #title>导航四</template>
</el-menu-item>
<el-submenu index="5">
<template #title>
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item-group>
<template #title>分组一</template>
<el-menu-item index="5-1">选项1</el-menu-item>
<el-menu-item index="5-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="5-3">选项3</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
<logo />
<menus :collapse="collapse" />
</div>
</template>
<script>
import { defineComponent, computed } from "vue";
import Logo from "./Logo.vue";
import Menus from "./Menus.vue";
import { useStore } from "vuex";
export default defineComponent({
components: {
Logo,
Menus,
},
setup() {
const store = useStore();
const collapse = computed(() => !!store.state.app.sidebar.collapse);
@ -91,36 +36,13 @@ export default defineComponent({
background: #2d3a4b;
transition: all 0.3s;
overflow: hidden;
display: flex;
flex-direction: column;
&.collapse {
width: 64px;
.brand .title {
::v-deep(.brand .title) {
display: none;
}
}
.brand {
height: 48px;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: center;
.logo {
max-width: 32px;
max-height: 32px;
}
.title {
color: #fff;
font-size: 16px;
font-weight: 700;
white-space: nowrap;
margin-left: 8px;
transition: all 0.5s;
}
}
.menu {
border: none;
}
::v-deep(.el-menu-item.is-active) {
background: #0174df !important;
}
}
</style>

View File

@ -14,34 +14,39 @@
</el-breadcrumb>
</template>
<script>
import { defineComponent, ref, onBeforeMount } from "vue";
import { useRoute, useRouter, onBeforeRouteUpdate } from "vue-router";
import {
defineComponent,
ref,
onBeforeMount,
getCurrentInstance,
watch,
} from "vue";
export default defineComponent({
setup() {
const route = useRoute();
const router = useRouter();
const routes = router.getRoutes();
const { ctx } = getCurrentInstance();
const route = ctx.$router.currentRoute; // 使useRoutewatch
const breadcrumbs = ref([]);
const getBreadcrumbs = (route) => {
const res = [{ path: "/", meta: { title: "首页" } }];
const { parentBreadcrumb } = route.meta;
if (!!parentBreadcrumb) {
const parents = routes.filter((item) =>
parentBreadcrumb.includes(item.name)
const home = [{ path: "/", meta: { title: "首页" } }];
if (route.name === "home") {
return home;
} else {
const matched = route.matched.filter(
(item) => !!item.meta && !!item.meta.title
);
res.push(...parents);
return [...home, ...matched];
}
if (route.name !== "home") res.push(route);
breadcrumbs.value = res;
};
onBeforeMount(() => {
getBreadcrumbs(route);
breadcrumbs.value = getBreadcrumbs(route.value);
});
onBeforeRouteUpdate((to) => {
getBreadcrumbs(to);
watch(route, (newRoute) => {
breadcrumbs.value = getBreadcrumbs(newRoute);
});
return {

View File

@ -8,7 +8,7 @@
<img
class="avatar"
:src="userinfo.avatar"
/> {{userinfo.userName}}
/> {{userinfo.name}}
</template>
</div>
<template #dropdown>
@ -29,7 +29,7 @@ export default defineComponent({
setup() {
const store = useStore();
const router = useRouter();
const userinfo = computed(() => store.state.app.userinfo);
const userinfo = computed(() => store.state.login.userinfo);
const logout = () => {
store.commit("app/clearToken");
router.push("/login");
@ -53,7 +53,8 @@ export default defineComponent({
background: #f5f5f5;
}
.el-icon-user {
font-size: 16px;
font-size: 20px;
margin-right: 8px;
}
.avatar {
margin-right: 8px;

View File

@ -13,30 +13,17 @@
</div>
</template>
<script>
import { defineComponent, onBeforeMount } from "vue";
import { useStore } from "vuex";
import { defineComponent } from "vue";
import Sidebar from "./components/Sidebar/index.vue";
import Topbar from "./components/Topbar/index.vue";
import Tabsbar from "./components/Tabsbar/index.vue";
import { GetUserinfo } from "@/api/app";
export default defineComponent({
components: {
Sidebar,
Topbar,
Tabsbar,
},
setup() {
const store = useStore();
const getUserinfo = async () => {
const { code, data } = await GetUserinfo();
if (+code === 200) {
store.commit("app/setUserinfo", data);
}
};
onBeforeMount(() => {
getUserinfo();
});
},
});
</script>

View File

@ -8,4 +8,7 @@ import store from './store'
import ElementPlus from "element-plus";
import "element-plus/lib/theme-chalk/index.css";
// 权限控制
import './permission'
createApp(App).use(store).use(router).use(ElementPlus).mount('#app')

32
src/permission.js Normal file
View File

@ -0,0 +1,32 @@
import router from '@/router'
import store from '@/store'
import { TOKEN } from '@/store/modules/app' // TOKEN变量名
// 白名单里面是路由对象的name
const WhiteList = ['login']
// vue-router4的路由守卫不再是通过next放行而是通过return返回false或者一个路由地址
router.beforeEach(async (to) => {
if (!window.localStorage[TOKEN]) {
if (!WhiteList.includes(to.name)) {
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
}
}
})

View File

@ -1,39 +1,25 @@
// index.js
import { createRouter, createWebHashHistory } from "vue-router"
import layout from '@/layout/index.vue'
import login from './modules/login'
import home from './modules/home'
import user from './modules/user'
import { TOKEN } from '@/store/modules/app'
export const AllMenus = [
...user
]
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
component: layout,
redirect: '/home',
children: [
...home,
...user
]
},
...login
...login,
...home,
],
});
// vue-router4的路由守卫不再是通过next放行而是通过return返回false或者一个路由地址
router.beforeEach((to, from) => {
if (!window.localStorage[TOKEN] && to.name !== 'login') {
return {
name: 'login',
query: {
redirect: to.path // redirect是指登录之后可以跳回到redirect指定的页面
},
replace: true
}
}
})
export default router;

View File

@ -1,13 +1,21 @@
// home.js
import layout from '@/layout/index.vue'
const Home = () => import("@/views/home/index.vue");
export default [
{
path: "/home",
name: "home",
component: Home,
meta: {
title: "首页",
}
}
path: '/home',
component: layout,
children: [
{
path: "",
name: "home",
component: Home,
meta: {
title: "首页",
}
}
]
},
]

View File

@ -1,23 +1,36 @@
const User = () => import("@/views/user/index.vue");
import layout from '@/layout/index.vue'
const UserList = () => import("@/views/user/index.vue");
const AddUser = () => import("@/views/user/AddUser.vue");
export default [
{
path: "/user",
path: '/user',
component: layout,
name: "user",
component: User,
meta: {
title: "用户管理",
}
},
{
path: "/user/add",
name: "addUser",
component: AddUser,
meta: {
title: "添加用户",
parentBreadcrumb: ["user"]
}
},
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"],
}
]
}
]

View File

@ -1,9 +1,13 @@
//index.js
import { createStore } from "vuex";
import app from "./modules/app";
import login from "./modules/login";
import menu from "./modules/menu";
export default createStore({
modules: {
app
app,
login,
menu
},
});

View File

@ -7,8 +7,7 @@ export default {
authorization: getItem(TOKEN),
sidebar: {
collapse: getItem('collapse')
},
userinfo: null
}
},
mutations: {
setToken (state, data) {
@ -30,12 +29,7 @@ export default {
state.sidebar.collapse = '';
// 保存到localStorage
removeItem('collapse');
},
setUserinfo (state, data) {
state.userinfo = data;
}
},
actions: {
},
actions: {},
};

View File

@ -0,0 +1,22 @@
import { GetUserinfo } from '@/api/login'
export default {
namespaced: true,
state: {
userinfo: null
},
mutations: {
setUserinfo (state, data) {
state.userinfo = data;
}
},
actions: {
async getUserinfo ({ commit }) {
const { code, data } = await GetUserinfo();
if (+code === 200) {
commit("setUserinfo", data);
return Promise.resolve(data)
}
}
},
};

56
src/store/modules/menu.js Normal file
View File

@ -0,0 +1,56 @@
import { AllMenus } from '@/router'
const hasPermission = (role, route) => {
if (!!route.roles && !route.roles.includes(role)) {
return false
}
return true
}
const getRoleMenus = (arr, role, parentPath = '') => {
const menus = [];
const routes = [];
arr.forEach(item => {
if (hasPermission(role, item)) {
const menu = {
url: item.path.startsWith('/') ? item.path : (!!item.path ? `${parentPath}/${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
}
menus.push(menu)
routes.push(route)
}
})
return { menus, routes }
}
export default {
namespaced: true,
state: {
menus: []
},
mutations: {
SET_MENUS (state, data) {
state.menus = data;
}
},
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)
}
}
},
};

View File

@ -93,6 +93,42 @@ npm run dev
> 目前项目中还没有路由、状态管理、UI组件库、ajax库
### 组件库 Element Plus
- 安装element-plus并且在main.js中引入
```powershell
npm install element-plus
```
引入element-plus
```js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// 引入element-plus
import ElementPlus from "element-plus";
import "element-plus/lib/theme-chalk/index.css";
// 使用use注册ElementPlus
createApp(App).use(ElementPlus).mount('#app')
```
- vue3中如何全局调用element-plus中的提示插件
```js
import {getCurrentInstance} from 'vue';
setup () {
const { ctx } = getCurrentInstance(); // 可以把ctx当成vue2中的this
ctx.$message.success("yes")
ctx.$loading()
}
```
### 路由
- 在src目录中创建views目录用来存放页面
@ -177,6 +213,16 @@ npm run dev
export default router;
```
- 修改App.vue
```html
```
<template>
<router-view />
</template>
```
- 挂载路由
> 在main.js中挂载路由
@ -185,15 +231,23 @@ npm run dev
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// 引入element-plus
import ElementPlus from "element-plus";
import "element-plus/lib/theme-chalk/index.css";
// 引入路由
import router from './router'
// 使用use注册路由
createApp(App).use(router).mount('#app')
```
// 使用use注册路由
createApp(App).use(ElementPlus).use(router).mount('#app')
```
- 配置alias路径别名
> 我们可以看到上面的引入路径比较深这个时候可以配置一个src目录的别名需要在vite.config.js中配置
> 文件引入路径比较深的时候,使用相对路径需要写很多`../`,例如上面的`router/modules/home.js`文件
>
> 所以我们可以配置一个src目录的别名需要在vite.config.js中配置
```js
// vite.config.js
@ -209,7 +263,7 @@ npm run dev
"@": path.resolve(__dirname, "src"),
},
},
})
})
```
然后我们就可以修改路由组件的引用路径了在router/modules/home.js中
@ -274,51 +328,19 @@ npm run dev
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// 引入路由
import router from './router'
// 引入store
import store from './store'
// 使用use注册路由和store
createApp(App).use(router).use(store).mount('#app')
```
### 组件库 Element Plus
- 安装element-plus并且在main.js中引入
```powershell
npm install element-plus
```
引入element-plus
```js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// 引入路由
import router from './router'
// 引入store
import store from './store'
// 引入element-plus
import ElementPlus from "element-plus";
import "element-plus/lib/theme-chalk/index.css";
// 引入路由
import router from './router'
// 引入store
import store from './store'
// 使用use注册路由和store
createApp(App).use(router).use(store).use(ElementPlus).mount('#app')
```
- vue3中如何全局调用element-plus中的提示插件
```js
import {getCurrentInstance} from 'vue';
setup () {
const { ctx } = getCurrentInstance(); // 可以把ctx当成vue2中的this
ctx.$message.success("yes")
ctx.$loading()
}
createApp(App).use(ElementPlus).use(router).use(store).mount('#app')
```
### 接口管理
@ -440,6 +462,14 @@ server: {
"@": path.resolve(__dirname, "src"),
},
},
server: {
proxy: {
"/api": {
target: "http://dev.erp.com",
changeOrigin: true,
},
},
},
});
};
```
@ -692,18 +722,22 @@ server: {
### 权限控制
> 如果没有登录或者token失效的时候页面需要重定向到登录页
>
> 还有一些例如找回密码的页面应该也可以访问所以我们还是设置一个白名单没有token的时候只能访问白名单的页面
- 全局路由守卫
> 编辑router/index.js
- 在src目录创建permission.js
```js
// 引入TOKEN变量
import { TOKEN } from '@/store/modules/app'
// permission.js
// 全局路由守卫注意vue-router4的路由守卫不需要next跳转而是通过return返回false或者一个路由地址
router.beforeEach((to, from) => {
if (!window.localStorage[TOKEN] && to.name !== 'login') {
import router from '@/router'
import { TOKEN } from '@/store/modules/app' // TOKEN变量名
// 白名单里面是路由对象的name
const WhiteList = ['login']
// vue-router4的路由守卫不再是通过next放行而是通过return返回false或者一个路由地址
router.beforeEach((to) => {
if (!window.localStorage[TOKEN] && !WhiteList.includes(to.name)) {
return {
name: 'login',
query: {
@ -715,6 +749,13 @@ server: {
})
```
- 在main.js中引入permission
```js
// 权限控制
import './permission'
```
### css
- css预处理器