This commit is contained in:
huzhushan 2021-03-25 18:29:58 +08:00
parent 83cca9fbd1
commit be7002e3d6
19 changed files with 512 additions and 28 deletions

View File

@ -12,4 +12,18 @@ export default [
}
},
},
{
url: "/api/userinfo",
method: "get",
timeout: 100,
response: {
code: 200,
message: "获取用户信息成功",
data: {
id: 1,
userName: 'admin',
avatar: "@image('48x48', '#fb0a2a')"
}
},
},
]

8
src/api/app.js Normal file
View File

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

View File

@ -0,0 +1,126 @@
<template>
<div
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>
</div>
</template>
<script>
import { defineComponent, computed } from "vue";
import { useStore } from "vuex";
export default defineComponent({
setup() {
const store = useStore();
const collapse = computed(() => !!store.state.app.sidebar.collapse);
return {
collapse,
};
},
});
</script>
<style lang="less" scoped>
.left {
width: 210px;
background: #2d3a4b;
transition: all 0.3s;
overflow: hidden;
&.collapse {
width: 64px;
.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

@ -0,0 +1,9 @@
<template>
<div class="tabs"></div>
</template>
<style lang="less" scoped>
.tabs {
height: 32px;
border-bottom: 1px solid #eaeaea;
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<el-breadcrumb
separator="/"
class="breadcrumb"
>
<el-breadcrumb-item
v-for="(item, index) in breadcrumbs"
:key="item.path"
:class="{no_link: index === breadcrumbs.length - 1}"
:to="index < breadcrumbs.length - 1 ? item.path : ''"
>
{{item.meta.title}}
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
import { defineComponent, ref, onBeforeMount } from "vue";
import { useRoute, useRouter, onBeforeRouteUpdate } from "vue-router";
export default defineComponent({
setup() {
const route = useRoute();
const router = useRouter();
const routes = router.getRoutes();
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)
);
res.push(...parents);
}
if (route.name !== "home") res.push(route);
breadcrumbs.value = res;
};
onBeforeMount(() => {
getBreadcrumbs(route);
});
onBeforeRouteUpdate((to) => {
getBreadcrumbs(to);
});
return {
breadcrumbs,
};
},
});
</script>
<style lang="less" scoped>
.breadcrumb {
margin-left: 10px;
::v-deep(a),
::v-deep(.is-link) {
font-weight: normal;
}
.no_link {
::v-deep(.el-breadcrumb__inner) {
color: #97a8be !important;
}
}
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<i
class="fold-btn el-icon-s-fold"
:class="{collapse:collapse}"
@click="handleToggleMenu"
></i>
</template>
<script>
import { defineComponent, computed } from "vue";
import { useStore } from "vuex";
export default defineComponent({
setup() {
const store = useStore();
const collapse = computed(() => !!store.state.app.sidebar.collapse);
const handleToggleMenu = () => {
store.commit("app/setCollapse", +!collapse.value);
};
return {
collapse,
handleToggleMenu,
};
},
});
</script>
<style lang="less" scoped>
.fold-btn {
line-height: 48px;
padding: 0 10px;
font-size: 18px;
cursor: pointer;
&:hover {
background: #f5f5f5;
}
&.collapse {
transform: scale(-1, 1);
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<el-dropdown>
<div class="userinfo">
<template v-if="!userinfo">
<i class="el-icon-user" /> admin
</template>
<template v-else>
<img
class="avatar"
:src="userinfo.avatar"
/> {{userinfo.userName}}
</template>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item>锁定屏幕</el-dropdown-item>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script>
import { computed, defineComponent } from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
export default defineComponent({
setup() {
const store = useStore();
const router = useRouter();
const userinfo = computed(() => store.state.app.userinfo);
const logout = () => {
store.commit("app/clearToken");
router.push("/login");
};
return {
userinfo,
logout,
};
},
});
</script>
<style lang="less" scoped>
.userinfo {
padding: 0 16px;
line-height: 48px;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
background: #f5f5f5;
}
.el-icon-user {
font-size: 16px;
}
.avatar {
margin-right: 8px;
width: 32px;
height: 32px;
border-radius: 50%;
}
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div class="header">
<div class="navigation">
<hamburger />
<breadcrumbs />
</div>
<div class="action">
<userinfo />
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import Hamburger from "./Hamburger.vue";
import Breadcrumbs from "./Breadcrumbs.vue";
import Userinfo from "./Userinfo.vue";
export default defineComponent({
components: {
Hamburger,
Breadcrumbs,
Userinfo,
},
setup() {},
});
</script>
<style lang="less" scoped>
.header {
height: 48px;
border-bottom: 1px solid #eaeaea;
display: flex;
justify-content: space-between;
.navigation {
display: flex;
align-items: center;
}
}
</style>

63
src/layout/index.vue Normal file
View File

@ -0,0 +1,63 @@
<template>
<div class="wrapper">
<sidebar />
<div class="right">
<div class="top">
<topbar />
<tabsbar />
</div>
<div class="main">
<router-view />
</div>
</div>
</div>
</template>
<script>
import { defineComponent, onBeforeMount } from "vue";
import { useStore } from "vuex";
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>
<style lang="less" scoped>
.wrapper {
display: flex;
height: 100%;
.right {
flex: 1;
display: flex;
flex-direction: column;
.top {
background: #fff;
}
.main {
flex: 1;
background: #f0f2f5;
padding: 16px;
overflow: auto;
}
}
}
</style>

View File

@ -1,18 +1,24 @@
// index.js
import { createRouter, createWebHistory } from "vue-router"
import home from './modules/home'
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/user'
import { TOKEN } from '@/store/modules/app'
const router = createRouter({
history: createWebHistory(),
history: createWebHashHistory(),
routes: [
{
path: '/',
redirect: '/home'
component: layout,
redirect: '/home',
children: [
...home,
...user
]
},
...home,
...login
],
});

View File

@ -6,5 +6,8 @@ export default [
path: "/home",
name: "home",
component: Home,
meta: {
title: "首页",
}
}
]

View File

@ -0,0 +1,23 @@
const User = () => import("@/views/user/index.vue");
const AddUser = () => import("@/views/user/AddUser.vue");
export default [
{
path: "/user",
name: "user",
component: User,
meta: {
title: "用户管理",
}
},
{
path: "/user/add",
name: "addUser",
component: AddUser,
meta: {
title: "添加用户",
parentBreadcrumb: ["user"]
}
}
]

View File

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

View File

@ -5,6 +5,10 @@ export default {
namespaced: true,
state: {
authorization: getItem(TOKEN),
sidebar: {
collapse: getItem('collapse')
},
userinfo: null
},
mutations: {
setToken (state, data) {
@ -17,6 +21,21 @@ export default {
// 保存到localStorage
removeItem(TOKEN);
},
setCollapse (state, data) {
state.sidebar.collapse = data;
// 保存到localStorage
setItem('collapse', data);
},
clearCollapse (state) {
state.sidebar.collapse = '';
// 保存到localStorage
removeItem('collapse');
},
setUserinfo (state, data) {
state.userinfo = data;
}
},
actions: {
},
actions: {},
};

View File

@ -12,7 +12,7 @@ const service = axios.create({
// 拦截请求
service.interceptors.request.use(
(config) => {
const authorization = store.state.user;
const authorization = store.state.app;
if (authorization) {
config.headers.Authorization = `Bearer ${authorization.token}`;
}
@ -36,7 +36,7 @@ service.interceptors.response.use(
// 响应拦截器中的 error 就是那个响应的错误对象
if (error.response && error.response.status === 401) {
// 校验是否有 refresh_token
const { authorization } = store.state;
const { authorization } = store.state.app;
if (!authorization || !authorization.refresh_token) {
router.push("/login");
@ -54,7 +54,7 @@ service.interceptors.response.use(
});
// 如果获取成功,则把新的 token 更新到容器中
// console.log('刷新 token 成功', res)
store.commit("setToken", {
store.commit("app/setToken", {
token: res.data.data.token, // 最新获取的可用 token
refresh_token: authorization.refresh_token, // 还是原来的 refresh_token
});
@ -67,7 +67,7 @@ service.interceptors.response.use(
// console.log('请求刷线 token 失败', err)
router.push("/login");
// 清除token
store.commit("clearToken")
store.commit("app/clearToken")
}
}

View File

@ -97,7 +97,7 @@ export default defineComponent({
router.push(!!targetPath ? targetPath : "/");
},
});
store.commit("user/setToken", data);
store.commit("app/setToken", data);
} else {
ctx.$message.error(message);
}

View File

@ -0,0 +1,3 @@
<template>
addUser
</template>

3
src/views/user/index.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
user
</template>

View File

@ -32,7 +32,6 @@ Vue3 + Element-plus + Vite
> 我们将使用vite和vue3手动进行前端项目架构。为方便大家从vue2过渡到vue3本项目我们先不使用ts。
>
> ts项目我们后面再讲并且我们将会使用更加集成化的框架就不需要手动搭建项目了。
使用vite的命令创建项目
@ -159,12 +158,12 @@ npm run dev
```js
// index.js
import { createRouter, createWebHistory } from "vue-router"
import { createRouter, createWebHashHistory } from "vue-router"
import home from './modules/home'
import login from './modules/login'
const router = createRouter({
history: createWebHistory(),
history: createWebHashHistory(),
routes: [
{
path: '/',
@ -242,10 +241,10 @@ npm run dev
> 状态管理也使用模块化的方式
在store目录中创建modules目录modules中创建一个模块比如叫user.js
在store目录中创建modules目录modules中创建一个模块app.js
```js
// user.js
// app.js
export default {
namespaced: true,
state: {},
@ -259,11 +258,11 @@ npm run dev
```js
//index.js
import { createStore } from "vuex";
import user from "./modules/user";
import app from "./modules/app";
export default createStore({
modules: {
user,
app,
},
});
@ -525,12 +524,12 @@ server: {
password: '123456'
})
if (+code === 200) {
store.commit("user/setToken", data);
store.commit("app/setToken", data);
}
}
```
store/modules/user.js
store/modules/app.js
```js
import { getItem, setItem, removeItem } from "@/utils/storage"; //getItem和setItem是封装的操作localStorage的方法
@ -604,7 +603,7 @@ server: {
// 拦截请求
service.interceptors.request.use(
(config) => {
const authorization = store.state.user;
const authorization = store.state.app;
if (authorization) {
config.headers.Authorization = `Bearer ${authorization.token}`;
}
@ -628,7 +627,7 @@ server: {
// 响应拦截器中的 error 就是那个响应的错误对象
if (error.response && error.response.status === 401) {
// 校验是否有 refresh_token
const { authorization } = store.state;
const { authorization } = store.state.app;
if (!authorization || !authorization.refresh_token) {
router.push("/login");
@ -646,7 +645,7 @@ server: {
});
// 如果获取成功,则把新的 token 更新到容器中
// console.log('刷新 token 成功', res)
store.commit("setToken", {
store.commit("app/setToken", {
token: res.data.data.token, // 最新获取的可用 token
refresh_token: authorization.refresh_token, // 还是原来的 refresh_token
});
@ -659,7 +658,7 @@ server: {
// console.log('请求刷线 token 失败', err)
router.push("/login");
// 清除token
store.commit("clearToken")
store.commit("app/clearToken")
}
}
@ -700,7 +699,7 @@ server: {
```js
// 引入TOKEN变量
import { TOKEN } from '@/store/modules/user'
import { TOKEN } from '@/store/modules/app'
// 全局路由守卫注意vue-router4的路由守卫不需要next跳转而是通过return返回false或者一个路由地址
router.beforeEach((to, from) => {