一.后台搭建完成后,安装依赖
yarn add element-ui axios moment
二.在根目录新建 .env.development
文件
VUE_APP_BASE_API=在线接口地址
三.封装 axios 新增 src/utils/request.js
文件
import axios from "axios";
import { MessageBox, Message } from "element-ui";
import store from "@/store"; // 1、导入 store
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
// process.env.VUE_APP_BASE_API
timeout: 5000,
});
// request拦截器
service.interceptors.request.use(
(config) => {
const token = store.getters.token; // 2、从 getters 里面获取 token
if (token) {
// 让每个请求携带token
config.headers["token"] = token;
}
return config;
},
(error) => {
console.log(error);
return Promise.reject(error);
}
);
// response拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
if (res.code !== 20000) {
Message({
message: res.message || "Error",
type: "error",
duration: 5 * 1000,
});
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// 重新登录
MessageBox.confirm(
"你已被登出,可以取消继续留在该页面,或者重新登录",
"确定登出",
{
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
}
).then(() => {
// 3、通过 store 的 dispatch 方法请求 auth/resetToken
store.dispatch("auth/resetToken").then(() => {
location.reload();
});
});
}
return Promise.reject(new Error(res.message || "Error"));
} else {
return res;
}
},
(error) => {
console.log("err" + error);
Message({
message: error.message,
type: "error",
duration: 5 * 1000,
});
return Promise.reject(error);
}
);
export default service;
四.配置全局过滤器 新建 src/filters/index.js
文件
import moment from "moment";
/**
* "Fri Dec 10 2021 01:01:49 GMT+0800 (China Standard Time)" => "2021年12月10日凌晨1点01分"
* @param {number} num
*/
export function dateFormat(value) {
moment.locale("zh-cn");
return moment(value).format("LLL");
}
/**
* 10000 => "10,000"
* @param {number} num
*/
export function thousandFormat(num) {
return (+num || 0)
.toString()
.replace(/^-?\d+/g, (m) => m.replace(/(?=(?!\b)(\d{3})+$)/g, ","));
}
五.CSS文件 新建 src/styles/common.scss
文件
a {
color: #1f99b0;
text-decoration: none;
}
.clear:after {
display: block;
content: "clear";
height: 0;
clear: both;
overflow: hidden;
visibility: hidden;
}
.thumb {
max-height: 65px;
max-width: 120px;
}
.gray {
background: #e6e8ec;
}
新建 src/styles/element-ui.scss
文件
.el-menu-item.is-active {
color: #1f99b0 !important;
}
.el-switch.is-checked .el-switch__core {
border-color: #1f99b0 !important;
background-color: #1f99b0 !important;
}
.el-button--primary {
color: #fff !important;
background-color: #1f99b0 !important;
border-color: #1f99b0 !important;
}
.el-menu-item:focus, .el-menu-item:hover {
background-color: #e1f0f0 !important;
}
.el-submenu__title:hover {
background-color: #e1f0f0 !important;
}
.el-tag {
background-color: #e1f0f0;
border-color: #e1f0f0;
color: #1f99b0;
}
.el-button.el-button--default:focus, .el-button.el-button--default:hover {
color: #1f99b0;
border-color: #dcdef6;
background-color: #e1f0f0;
}
.el-radio__input.is-checked .el-radio__inner {
border-color: #1f99b0;
background: #1f99b0;
}
.el-radio__input.is-checked + .el-radio__label {
color: #1f99b0;
}
.el-checkbox__input.is-checked .el-checkbox__inner, .el-checkbox__input.is-indeterminate .el-checkbox__inner {
background-color: #1f99b0;
border-color: #1f99b0;
}
.el-button--text{
color: #1f99b0;
}
.el-button--text:focus, .el-button--text:hover {
color: #1f99b0;
text-decoration: underline;
}
.el-input.is-active .el-input__inner, .el-input__inner:focus {
border-color: #1f99b0;
outline: 0;
}
.el-cascader-node.in-active-path, .el-cascader-node.is-active, .el-cascader-node.is-selectable.in-checked-path {
color: #1f99b0;
}
.el-radio__inner:hover {
border-color: #1f99b0;
}
新建 src/styles/layouts.scss
文件
html, body {
height: 100%;
margin: 0;
}
#app {
height: 100%;
}
.el-container, .el-aside, .el-aside .el-col, .el-menu {
height: 100%;
}
.el-header, .el-footer {
background-color: #e1f0f0;
color: #333;
height: 60px;
line-height: 60px;
}
.el-header {
position: fixed;
top: 0;
width: 100%;
z-index: 2;
.logo {
height: 40px;
margin-top: 10px;
float: left;
}
h1 {
margin: 0 0 0 20px;
font-size: 18px;
font-weight: normal;
float: left;
line-height: 60px;;
}
}
.el-footer {
position: fixed;
bottom: 0;
width: 100%;
z-index: 2;
text-align: center;
}
.el-aside {
position: fixed;
top: 60px;
width: 100%;
z-index: 2;
background-color: #e1f0f0;
color: #333;
line-height: 200px;
}
.el-main {
color: #333;
overflow: visible;
margin: 60px 0 0 200px;
padding-bottom: 80px;
}
.user-info {
float: right;
}
新建 src/styles/table.scss
文件
.sort-input input{
width: 50px;
text-align: center;
}
.el-table .cell .delete{
margin-left: 10px;
}
新建 src/styles/tree.scss
文件
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
新建 src/styles/uploads.scss
文件
.image-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.image-uploader .el-upload:hover {
border-color: #409EFF;
}
.image-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px !important;
text-align: center;
}
新建 src/styles/index.scss
文件
@import './layouts.scss';
@import './common.scss';
@import './element-ui.scss';
@import './table.scss';
@import './tree.scss';
@import './uploads.scss';
五.布局模板
新建 src/views/layouts/components/NavBarView.vue
组件
<template>
<el-header>
<img
src="https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png"
alt="百度Logo"
class="logo"
/>
<h1>后台管理</h1>
<div class="user-info">
<el-dropdown @command="handleCommand">
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">安全退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>{{ username }}</span>
</div>
</el-header>
</template>
<script>
// 1、引入 mapGetters 辅助函数
import { mapGetters } from "vuex";
export default {
// 2、把 username 这个 Getters 映射过来,最后在模板中渲染
computed: mapGetters(["username"]),
methods: {
// 绑定 command 事件对应的方法 handleCommand
async handleCommand(command) {
// 若 command 的值为 logout,请求退出接口,跳转到登录页
if (command === "logout") {
console.log(123);
await this.$store.dispatch("auth/resetToken");
await this.$router.push("/sign_in");
}
},
},
};
</script>
新建 src/views/layouts/components/SideBar.vue
组件
<template>
<el-aside width="200px">
<el-col>
<el-menu :router="true" default-active="/" class="el-menu-vertical-demo">
<el-menu-item index="/">
<i class="el-icon-pie-chart"></i>
<span slot="title">仪表盘</span>
</el-menu-item>
<el-submenu index="article_manage">
<template slot="title">
<i class="el-icon-document"></i>
<span>内容管理</span>
</template>
<el-menu-item index="/list">栏目管理</el-menu-item>
<el-menu-item index="/articles">文章管理</el-menu-item>
</el-submenu>
<el-submenu index="advert_manage">
<template slot="title">
<i class="el-icon-picture-outline-round"></i>
<span>广告管理</span>
</template>
<el-menu-item index="/ad_categories">广告分类</el-menu-item>
<el-menu-item index="/ads">广告管理</el-menu-item>
</el-submenu>
<el-submenu index="system_manage">
<template slot="title">
<i class="el-icon-setting"></i>
<span>系统管理</span>
</template>
<el-menu-item index="/permissions">菜单与权限</el-menu-item>
<el-menu-item index="/roles">用户组</el-menu-item>
<el-menu-item index="/users">用户</el-menu-item>
</el-submenu>
</el-menu>
</el-col>
</el-aside>
</template>
新建 src/views/layouts/IndexView.vue
组件
<template>
<el-container>
<Navbar />
<el-container>
<Sidebar />
<el-container>
<el-main>
<router-view />
</el-main>
<el-footer> 123132213231222222222 </el-footer>
</el-container>
</el-container>
</el-container>
</template>
<script>
import Navbar from "./components/NavBarView";
import Sidebar from "./components/SideBarView";
export default {
components: {
Navbar,
Sidebar,
},
};
</script>
修改 src/App.vue
文件
<template>
<div id="app">
<router-view />
</div>
</template>
修改 src/views/HomeView.vue
文件
<template>
<div class="home">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">首页
<el-breadcrumb-item>仪表盘</el-breadcrumb-item>
</el-breadcrumb>
<el-divider></el-divider>
<el-card class="box-card"
><div><echartsView /></div
></el-card>
</div>
</template>
<script>
export default {
name: "HomeView",
};
</script>
<style scoped lang="scss">
.info {
font-size: 14px;
}
</style>
修改 src/main.js
文件
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import "@/styles/index.scss";
// 路由守卫
import "./permission";
// 全局过滤器
import { dateFormat, thousandFormat } from "./filters";
// 注册全局过滤器,一定要放在new Vue之前
Vue.filter("dateFormat", dateFormat);
Vue.filter("thousandFormat", thousandFormat);
Vue.use(ElementUI);
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
修改 src/router/index.js
文件
import Vue from "vue";
import VueRouter from "vue-router";
import Layout from "@/views/layouts/IndexView";
Vue.use(VueRouter);
const routes = [
{
path: "/sign_in",
component: () => import("@/views/auth/SignInView"),
meta: { title: "登录" },
},
{
path: "/",
component: Layout,
children: [
{
path: "",
component: () => import("@/views/HomeView"),
meta: { title: "首页" },
},
{
path: "/list",
name: "list",
component: () => import("@/views/list/ListView"),
meta: { title: "列表" },
},
{
path: "/list/:id",
name: "index",
component: () => import("@/views/list/IndexView"),
meta: { title: "列表ID" },
},
],
},
// {
// path: "/about",
// name: "about",
// // route level code-splitting
// // this generates a separate chunk (about.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () =>
// import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
// },
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
export default router;
封装登录接口 创建api文件夹 下auth.js
import request from "@/utils/request";
// 登录
export function login(data) {
return request({
url: "/admin/auth/login",
method: "post",
data,
});
}
创建登录组件及功能实现 新建 src/views/auth/SignIn.vue
<template>
<div class="sign-in">
<el-row>
<el-col :span="12" :offset="6">
<h1>后台管理系统</h1>
<el-form
:model="loginForm"
:rules="loginRules"
ref="ruleForm"
label-width="100px"
class="demo-ruleForm"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="loginForm.password" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin('ruleForm')">
立即登录
</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script>
// import { login } from "@/api/auth";
export default {
data() {
return {
loginForm: {
username: "",
password: "",
},
loginRules: {
name: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
},
};
},
methods: {
handleLogin(formName) {
this.$refs[formName].validate(async (valid) => {
if (valid) {
// const res = await login(this.loginForm);
await this.$store.dispatch("auth/login", this.loginForm);
await this.$router.push({ path: "/" });
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
},
},
};
</script>
<style scoped lang="scss">
.sign-in {
padding-top: 100px;
}
h1 {
text-align: center;
}
</style>
1.封装 cookie 文件
yarn add js-cookie
2、新建 utils/auth.js
import Cookies from "js-cookie";
// cookie中key的命名
const TokenKey = "Admin-Token";
/**
* 获取cookie中的token
* @returns {*}
*/
export function getToken() {
return Cookies.get(TokenKey);
}
/**
* 设置token到cookie中
* @param token
* @param remember
* @returns {*}
*/
export function setToken(token, remember = false) {
// 记住我
if (remember) {
return Cookies.set(TokenKey, token, { expires: 7 });
}
return Cookies.set(TokenKey, token);
}
/**
* 删除cookie中的token
* @returns {*}
*/
export function removeToken() {
return Cookies.remove(TokenKey);
}
创建 store 登录状态管理器 1、新建 src/store/modules/auth.js
import { getToken, setToken, removeToken } from "@/utils/auth";
import { login } from "@/api/auth";
import { getMe } from "@/api/users";
const state = () => ({
token: getToken(),
username: "",
role: {},
routers: {},
menus: {},
});
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token;
},
SET_USERNAME: (state, username) => {
state.username = username;
},
SET_ROLE: (state, role) => {
state.role = role;
},
SET_ROUTERS: (state, routers) => {
state.routers = routers;
},
SET_MENUS: (state, menus) => {
state.menus = menus;
},
};
const actions = {
async login({ commit }, payload) {
const { username, password } = payload;
const res = await login({
username: username.trim(),
password: password.trim(),
});
const { data } = res;
setToken(data.token);
commit("SET_TOKEN", data.token);
},
// 1、getInfo函数用于获取用户信息,用户组
async getInfo({ commit }) {
// 2、请求 getMe 接口
const res = await getMe();
console.log(res); // 这个接口给我们返回了user,role,routers,menus这些数据
const { data } = res;
const { user, role, routers, menus } = data;
// 4、把所有数据全部提交给 mutations
commit("SET_USERNAME", user.username);
commit("SET_ROLE", role);
commit("SET_ROUTERS", routers);
commit("SET_MENUS", menus);
return data;
},
resetToken({ commit }) {
commit("SET_TOKEN", "");
commit("SET_USERNAME", "");
commit("SET_ROLE", "");
commit("SET_ROUTERS", "");
commit("SET_MENUS", "");
removeToken();
},
};
export default {
namespaced: true,
state,
actions,
mutations,
};
2、新建 store/getters.js
const getters = {
token: (state) => state.auth.token,
username: (state) => state.auth.username,
role: (state) => state.auth.role,
routers: (state) => state.auth.routers,
menus: (state) => state.auth.menus,
};
export default getters;
3、在 src/store/index.js
中
import Vue from "vue";
import Vuex from "vuex";
import auth from "./modules/auth"; // 1、引入 auth
import getters from "@/store/getters"; // 2、引入 getters
Vue.use(Vuex);
export default new Vuex.Store({
getters, // 3、注册 getters,由于getters是全局属性,所以我们注册到 Store 实例中
modules: {
auth, // 4、注册 auth
},
});
登录实现
1、修改 src/store/modules/auth.js
代码,按步骤理解 前面已经修改
2、修改 views/auth/SignIn.vue
代码 前面已修改
验证 token
1、修改request.js 前面第三部已经修改
2、新建 src/permission.js
import router from "@/router";
import { getToken } from "@/utils/auth";
import store from "./store";
import { Message } from "element-ui";
router.beforeEach(async (to, from, next) => {
document.title = `${to.meta.title} - 后台`;
const hasToken = getToken();
if (hasToken) {
if (to.path === "/sign_in") {
next({ path: "/" });
} else {
try {
// 3、调用 auth/getInfo 函数获取用户信息
await store.dispatch("auth/getInfo");
// 如果用户信息出错了。表示token可能过期了,直接清空,跳转到登录页
} catch (error) {
// 删除token,跳转到登录页
await store.dispatch("auth/resetToken");
Message.error(error || "Has Error");
next("/sign_in");
}
}
next();
} else {
if (to.path === "/sign_in") {
next();
} else {
next("/sign_in");
}
}
});
3、在 main.js
中 前面已经修改
获取用户信息
1、修改 src/store/modules/auth.js
中的代码 前面已经修改
2、在 src/permission.js
中调用 前面已经修改
3、显示用户名,在 views/layout/components/NavBar.vue
中 前面已经修改