電商後臺管理系統用於管理使用者賬號、商品分類、商品資訊、訂單、資料統計等業務功能
電商後臺管理系統整體採用前後端分離的開發模式,其中前端專案是基於 Vue 技術棧的 SPA(單頁應用程式) 專案
詳見【Vue】vue-cli - 圖形化建立專案 - 設定專案 - axios - elementUI - 碼雲SSH公鑰設定 - Gitee同步
cd E:\YKcode\web\myProject01\api\vue_api_server
cnpm install
出錯了就多 npm install
或 cnpm install
幾下
node app.js
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210314154316727.png?x-oss-process=image/waterma
根據檔案測試
偵錯成功
① 在登入頁面輸入使用者名稱和密碼
② 呼叫後臺介面進行驗證
③ 通過驗證之後,根據後臺的響應狀態跳轉到專案主頁
相關參考【徹底理解cookie,session,token - 墨顏丶 - 部落格園】
通過 Element-UI 元件實現佈局
建立分支
git checkout -b login
檢視分支
git branch
import Login from '../components/Login.vue'
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login }
]
<div id="app">
<!-- 路由預留位置 -->
<router-view></router-view>
</div>
/* 全域性樣式表 */
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
min-width: 1366px;
}
// 匯入全域性樣式表
import './assets/css/global.css'
<div class="login_container">
<div class="login_box"></div>
</div>
.login_container {
background-color: #409eff;
height: 100%;
}
.login_box {
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
<!-- 頭像區域 -->
<div class="avatar_box">
<img src="../assets/YK.jpg" alt="" />
</div>
.avatar_box {
height: 130px;
width: 130px;
border: 1px solid #eee;
border-radius: 50%;
padding: 10px;
box-shadow: 0 0 10px #ddd;
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
}
img {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #eee;
}
import Vue from 'vue'
import { Button, Form, FormItem, Input } from 'element-ui'
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
<!-- 登入表單區域 -->
<el-form label-width="0px" class="login_form">
<!-- 使用者名稱 -->
<el-form-item>
<el-input></el-input>
</el-form-item>
<!-- 密碼 -->
<el-form-item>
<el-input></el-input>
</el-form-item>
<!-- 按鈕 -->
<el-form-item>
<el-button type="primary" class="btns">登入</el-button>
<el-button type="info">重置</el-button>
</el-form-item>
</el-form>
.login_form {
position: absolute;
bottom: 0;
width: 100%;
padding: 0 20px;
box-sizing: border-box;
}
.btns {
display: flex;
justify-content: flex-end;
}
https://element.eleme.cn/
官方圖示庫 https://element.eleme.cn/
<el-input prefix-icon="el-icon-user"></el-input>
<el-input prefix-icon="el-icon-lock"></el-input>
import './assets/fonts/iconfont.css'
<el-form :model="loginForm">
<el-input v-model="loginForm.username"></el-input>
<el-input v-model="loginForm.password" type="password"></el-input>
</el-form>
export default {
data () {
return {
// 這是登入表單的資料繫結物件
loginForm: {
username: 'admin',
password: '123456'
}
}
}
}
<el-form :rules="loginFormRules">
<el-form-item prop="username">
<el-input v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password"></el-input>
</el-form-item>
</el-form>
export default {
data () {
return {
loginFormRules: {
// 驗證使用者名稱是否合法
username: [
{ required: true, message: '請輸入登入名稱', trigger: 'blur' },
{ min: 3, max: 5, message: '長度在 3 到 5 個字元', trigger: 'blur' }
],
// 驗證密碼是否合法
password: [
{ required: true, message: '請輸入登入密碼', trigger: 'blur' },
{ min: 6, max: 15, message: '長度在 6 到 15 個字元', trigger: 'blur' }
]
}
}
}
}
<el-form ref="loginFormRef"></el-form>
<el-button type="info" @click="resetLoginForm">重置</el-button>
methods: {
// 點選重置按鈕,重置登入表單
resetLoginForm () {
// console.log(this) // VueComponent
this.$refs.loginFormRef.resetFields()
}
}
<el-button type="primary" @click="login">登入</el-button>
import axios from 'axios'
// 設定請求的根路徑
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
Vue.prototype.$http = axios
import { Message } from 'element-ui'
Vue.prototype.$message = Message // 彈框提示元件掛在在Vue原型上
methods: {
login () {
this.$refs.loginFormRef.validate(async (valid) => {
// console.log(valid) // false/ture
if (!valid) return
const { data: result } = await this.$http.post('login', this.loginForm)
// console.log(result) //Promise
if (result.meta.status !== 200) return this.$message.error('登入失敗')
this.$message.success('登入成功')
// 1. 將登入成功之後的 token 儲存到使用者端的 sessionStorage 中(對談期間的儲存機制)(所以不放在loaclStorage中)
window.sessionStorage.setItem('token', result.data.token)
// 2. 通過程式設計式導航跳轉到後臺主頁,路由地址是 /home
this.$router.push('/home')
})
}
}
import Home from '../components/Home.vue'
const routes = [
{ path: '/home', component: Home, }
]
如果使用者沒有登入,但是直接通過URL存取特定頁面,需要重新導航到登入頁面
router/index.js
// 掛載路由導航守衛
router.beforeEach((to, from, next) => {
// to 將要存取的路徑
// from 代表從哪個路徑跳轉而來
// next 是一個函數, 表示放行 ①next()放行 ②next('/login')強制跳轉
if (to.path === '/login') return next() // 存取登入頁,直接放行
// 獲取token
const tokenStr = window.sessionStorage.getItem('token')
if (!tokenStr) return next('/login') // 沒有token 強制跳轉
next() // 否則(有token)直接放行
})
基於token的方式實現退出比較簡單,只需要銷燬原生的token即可。
這樣,後續的請求就不會攜帶token,必須重新登入生成一個新的token之後才可以存取頁面
<el-button type="info" @click="logout">退出</el-button>
methods: {
logout() {
window.sessionStorage.clear()
this.$router.push('/login')
}
}
<template>
<div class="login_container">
<div class="login_box">
<!-- 頭像區域 -->
<div class="avatar_box">
<img src="../assets/YK.jpg" alt="" />
</div>
<!-- 登入表單區域 -->
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginFormRules"
label-width="0px"
class="login_form"
>
<!-- 使用者名稱 -->
<el-form-item prop="username">
<el-input
prefix-icon="el-icon-user"
v-model="loginForm.username"
></el-input>
</el-form-item>
<!-- 密碼 -->
<el-form-item prop="password">
<el-input
prefix-icon="el-icon-lock"
v-model="loginForm.password"
type="password"
></el-input>
</el-form-item>
<!-- 按鈕 -->
<el-form-item class="btns">
<el-button type="primary" @click="login">登入</el-button>
<el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
export default {
data () {
return {
// 這是登入表單的資料繫結物件
loginForm: {
username: 'admin',
password: '123456'
},
// 這是表單驗證規則物件
loginFormRules: {
// 驗證使用者名稱是否合法
username: [
{ required: true, message: '請輸入登入名稱', trigger: 'blur' },
{ min: 3, max: 5, message: '長度在 3 到 5 個字元', trigger: 'blur' }
],
// 驗證密碼是否合法
password: [
{ required: true, message: '請輸入登入密碼', trigger: 'blur' },
{
min: 6,
max: 15,
message: '長度在 6 到 15 個字元',
trigger: 'blur'
}
]
}
}
},
methods: {
// 點選重置按鈕,重置登入表單
resetLoginForm () {
// console.log(this) // VueComponent
this.$refs.loginFormRef.resetFields()
},
login () {
this.$refs.loginFormRef.validate(async (valid) => {
// console.log(valid) // false/ture
if (!valid) return
const { data: result } = await this.$http.post('login', this.loginForm)
// console.log(result)
if (result.meta.status !== 200) return this.$message.error('登入失敗')
this.$message.success('登入成功')
// 將登入成功之後的token 儲存到使用者端的 sessionStorage 中(對談期間的儲存機制)(所以不放在loaclStorage中)
console.log(result)
window.sessionStorage.setItem('token', result.data.token)
// 通過程式設計式導航跳轉到後臺主頁,路由地址是 /home
this.$router.push('/home')
})
}
}
}
</script>
<style scoped>
.login_container {
background-color: #409eff;
height: 100%;
}
.login_box {
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.avatar_box {
height: 130px;
width: 130px;
border: 1px solid #eee;
border-radius: 50%;
padding: 10px;
box-shadow: 0 0 10px #ddd;
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
}
img {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #eee;
}
.login_form {
position: absolute;
bottom: 0;
width: 100%;
padding: 0 20px;
box-sizing: border-box;
}
.btns {
display: flex;
justify-content: flex-end;
}
</style>
git status
git add .
git commit -m "完成了登入功能"
git branch
git checkout master
git merge login
git push
https://gitee.com/ykang2020/vue_shop
git checkout login
git push -u origin login
<el-container class="home_container">
<!-- 頭部區域 -->
<el-header>Header</el-header>
<!-- 頁面主體區域 -->
<el-container>
<!-- 左邊側邊欄 -->
<el-aside width="200px">Aside</el-aside>
<!-- 右側內容主體 -->
<el-main>Main</el-main>
</el-container>
</el-container>
.home_container {
height: 100%;
}
.el-header {
background-color: #373d41;
}
.el-aside {
background-color: #333744;
}
.el-main {
background-color: #eaedf1;
}
<!-- 頭部區域 -->
<el-header>
<div>
<img src="../assets/heima.png" alt="" />
<span>電商後臺管理系統</span>
</div>
<el-button type="info" @click="logout">退出</el-button>
</el-header>
.el-header {
background-color: #373d41;
display: flex;
justify-content: space-between;
padding-left: 0;
align-items: center;
color: #fff;
font-size: 20px;
}
.el-header > div {
display: flex;
align-items: center;
}
.el-header > div > span {
margin-left: 15px;
}
選單分為二級,並且可以摺疊
<!-- 側邊欄選單區域 -->
<el-menu>
<el-submenu>
<!-- 這個 template 是一級選單的內容模板 -->
<i class="el-icon-menu"></i>
<span>一級選單</span>
<!-- 在一級選單中,可以巢狀二級選單 -->
<el-menu-item>
<i class="el-icon-menu"></i>
<span slot="title">二級選單</span>
</el-menu-item>
</el-submenu>
</el-menu>
需要授權的 API ,必須在請求頭中使用 Authorization欄位提供 token 令牌
後臺除了登入介面之外,都需要token許可權驗證,我們可以通過新增axios請求攔截器來新增token,以保證擁有獲取資料的許可權
在main.js中新增程式碼,在將axios掛載到vue原型之前新增下面的程式碼
// 通過axios請求攔截器新增token,保證擁有獲取資料的許可權
axios.interceptors.request.use(config => {
// 為請求頭物件,新增 Token 驗證的 Authorization 欄位
// console.log(config)
config.headers.Authorization = window.sessionStorage.getItem('token')
// 在最後必須 return config
return config
})
Home.vue
頁面載入之前就要獲取資料,定義生命週期函數
data() {
return {
// 左側選單資料
menulist: []
}
},
created() {
this.getMenuList()
this.activePath = window.sessionStorage.getItem('activePath')
},
methods: {
// 獲取所有的選單
async getMenuList() {
const { data: result } = await this.$http.get('menus')
if (result.meta.status !== 200) return this.$message.error(result.meta.msg)
this.menulist = result.data
console.log(result)
}
}
<!-- 一級選單 -->
<el-submenu :index="item.id+''" v-for="item in menulist" :key="item.id">
<!-- 一級選單的模板區域 -->
<template slot="title">
<!-- 文字 -->
<span>{{item.authName}}</span>
</template>
<!-- 二級選單 -->
<el-menu-item :index="subItem.id+''" v-for="subItem in item.children" :key="subItem.id">
<!-- 圖示 -->
<i class="el-icon-menu"></i>
<!-- 文字 -->
<span>{{subItem.authName}}</span>
</el-menu-item>
</el-submenu>
通過更改el-menu的active-text-color屬性可以設定側邊欄選單中點選的啟用項的文字顏色
<el-menu background-color="#333744" text-color="#fff" active-text-color="rgb(64,158,255)">
通過更改選單項模板(template)中的i標籤的類名,可以將左側選單欄的圖示進行設定
在資料中新增一個iconsObj,然後將圖示類名進行資料繫結
iconsObj: {
125: 'el-icon-s-custom',
103: 'el-icon-s-check',
101: 'el-icon-s-goods',
102: 'el-icon-s-order',
145: 'el-icon-s-marketing'
}
一級選單圖示繫結iconsObj中的資料:
<!-- 圖示 -->
<i :class="iconsObj[item.id]"></i>
為了保持左側選單每次只能開啟一個,顯示其中的子選單,我們可以在el-menu中新增一個屬性unique-opened
或者也可以資料繫結進行設定(此時true認為是一個bool值,而不是字串) :unique-opened=「true」
<el-menu unique-opened>
解決邊框問題
.el-menu {
border-right: none;
}
<!-- 左邊側邊欄 -->
<el-aside :width="isCollapse ? '64px':'200px'">
<div class="toggle-button" @click="toggleCollapse">|||</div>
<!-- 側邊欄選單區域 -->
<el-menu :collapse="isCollapse" :collapse-transition="false">
.toggle-button {
background-color: #4A5064;
font-size: 10px;
line-height: 24px;
color: rgb(144,147,153);
text-align: center;
letter-spacing: 0.2em;
cursor: pointer
}
// 點選按鈕切換選單摺疊與展開
toggleCollapse() {
this.isCollapse = !this.isCollapse
}
import Welcome from '../components/Welcome.vue'
const routes = [
{
path: '/home',
component: Home,
redirect: '/welcome',
children: [{ path: '/welcome', component: Welcome }]
}
]
<!-- 右側內容主體 -->
<el-main>
<!-- 路由預留位置 -->
<router-view></router-view>
</el-main>
<el-menu router>
<!-- 二級選單 -->
<el-menu-item :index="'/'+subItem.path" v-for="subItem in item.children" :key="subItem.id">
<template>
<el-container class="home_container">
<!-- 頭部區域 -->
<el-header>
<div>
<img src="../assets/heima.png" alt="" />
<span>電商後臺管理系統</span>
</div>
<el-button type="info" @click="logout">退出</el-button>
</el-header>
<!-- 頁面主體區域 -->
<el-container>
<!-- 左邊側邊欄 -->
<el-aside :width="isCollapse ? '64px':'200px'">
<div class="toggle-button" @click="toggleCollapse">|||</div>
<!-- 側邊欄選單區域 -->
<el-menu
background-color="#333744"
text-color="#fff"
active-text-color="rgb(64,158,255)"
unique-opened
:collapse="isCollapse"
:collapse-transition="false"
router
:default-active="activePath"
>
<!-- 一級選單 -->
<el-submenu :index="item.id+''" v-for="item in menulist" :key="item.id">
<!-- 一級選單的模板區域 -->
<template slot="title">
<!-- 圖示 -->
<i :class="iconsObj[item.id]"></i>
<!-- 文字 -->
<span>{{item.authName}}</span>
</template>
<!-- 二級選單 -->
<el-menu-item :index="'/'+subItem.path" v-for="subItem in item.children" :key="subItem.id" @click="saveNavState('/'+subItem.path)">
<!-- 圖示 -->
<i class="el-icon-menu"></i>
<!-- 文字 -->
<span>{{subItem.authName}}</span>
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<!-- 右側內容主體 -->
<el-main>
<!-- 路由預留位置 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
export default {
data() {
return {
// 左側選單資料
menulist: [],
// 定義一個字型圖示物件
iconsObj: {
125: 'el-icon-s-custom',
103: 'el-icon-s-check',
101: 'el-icon-s-goods',
102: 'el-icon-s-order',
145: 'el-icon-s-marketing'
},
isCollapse: false,
// 被啟用的連結地址
activePath: ''
}
},
created() {
this.getMenuList()
this.activePath = window.sessionStorage.getItem('activePath')
},
methods: {
logout() {
window.sessionStorage.clear()
this.$router.push('/login')
this.$message.warning('已退出')
},
// 獲取所有的選單
async getMenuList() {
const { data: result } = await this.$http.get('menus')
if (result.meta.status !== 200) return this.$message.error(result.meta.msg)
this.menulist = result.data
console.log(result)
},
// 點選按鈕切換選單摺疊與展開
toggleCollapse() {
this.isCollapse = !this.isCollapse
},
// 儲存連結的啟用狀態
saveNavState(activePath) {
window.sessionStorage.setItem('activePath', activePath)
this.activePath = activePath
}
}
}
</script>
<style scoped>
.home_container {
height: 100%;
}
.el-header {
background-color: #373d41;
display: flex;
justify-content: space-between;
padding-left: 0;
align-items: center;
color: #fff;
font-size: 20px;
}
.el-header > div {
display: flex;
align-items: center;
}
.el-header > div > span {
margin-left: 15px;
}
.el-aside {
background-color: #333744;
}
.toggle-button {
background-color: #4A5064;
font-size: 10px;
line-height: 24px;
color: rgb(144,147,153);
text-align: center;
letter-spacing: 0.2em;
cursor: pointer
}
.el-menu {
border-right: none;
}
.el-main {
background-color: #eaedf1;
}
</style>