前言
使用vue和element搭建的管理後臺筆記。github地址:待補充(github在家裏打不開啊啊啊)
物品管理頁面
增加左側菜單導航入口:
<el-submenu index="2">
<template slot="title"><i class="el-icon-message"></i>物品管理</template>
<el-menu-item-group>
<template slot="title">分類</template>
<el-menu-item index="/items/create">新建物品</el-menu-item>
<el-menu-item index="/items/list">物品列表</el-menu-item>
</el-menu-item-group>
</el-submenu>
編輯和列表頁面:複製CategoryEdit.vue和CategoryList.vue爲Itemxxx,並修改對應內容。
增加路由:
{
path: "/items/create",//增加物品
component: ItemEdit
},
{
path: "/items/edit/:id", //編輯物品
component: ItemEdit,//編輯與增加使用同一個組件
props: true, //Props爲true,表示將任何url參數都注入到 categoryedit的組件內
},
{
path: "/items/list",//物品列表
component: ItemList,
},
進行測試,
圖片上傳
圖片一般需要上傳到圖片服務器,這裏使用el-upload實現
//ItemEdit.vue
<el-upload class="avatar-uploader" :action="fileUploadPath" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="model.icon" :src="model.icon" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
......
//圖片上傳成功處理
handleAvatarSuccess(res) {
this.$set(this.model, "icon", res.imgTrueUrl);
},
//上傳文件類型及大小判斷
beforeAvatarUpload(file) {
const isJPG = file.type === "image/jpeg";
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
this.$message.error("上傳頭像圖片只能是 JPG 格式!");
}
if (!isLt2M) {
this.$message.error("上傳頭像圖片大小不能超過 2MB!");
}
return isJPG && isLt2M;
}
-
el-upload 使用自身的ajax來實現http的post方法,因此需要將圖片實際上傳地址賦值給 aciton ,這裏使用
$http.defaults.baseURL+
/upload``前半部分是我們定義在axios中的基本地址,後半部分是路徑,注意要給aciton前加冒號,表示動態綁定,否則只能傳入變量名稱字符串,在這裏卡了好久。。。 -
beforeAvatarUpload 是在圖片上傳前執行的函數,可以用來驗證圖片格式和大小,返回false圖片就不繼續上傳了。
-
handleAvatarSuccess 用於在圖片post成功後處理返回的圖片鏈接,這裏將圖片反顯。
-
使用vue的set類似於react 的setState。原因是 vue 框架data中定義對象model而沒有爲model內定義icon屬性,之後對model內屬性賦值( this.model.icon = res.imgTrueUrl)不會造成更新,所以使用$set.
英雄管理頁面
複製ItemEdit.vue和ItemList.vue到HeroEdit.vue和HeroList.vue並修改頁面內容,將接口請求資源改爲heros。
修改main.vue ,增加左側菜單入口 ,(與上類似,省略,下同)
修改路由文件。
根據前端頁面要求的字段修改後臺錄入頁面,與物品管理不同的是,英雄配置內容較多,用到了 el-tabs、el-select 等控件,而且在一個表單內容太多時,可以拆分爲兩個tab頁,這裏拆成了基礎信息和技能信息。
<el-form label-width="120px" @submit.native.prevent="save">
<el-tabs type="border-card" value="skills">
<el-tab-pane label="基礎信息" name="basic">
<el-form-item label="名稱">
<el-input v-model="model.name"></el-input>
</el-form-item>
<el-form-item label="頭像">
<el-upload class="avatar-uploader" :action="fileUploadPath" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="model.avatar" :src="model.avatar" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item label="稱號">
<el-input v-model="model.title"></el-input>
</el-form-item>
<el-form-item label="分類">
<el-select v-model="model.categories" placeholder="請選擇" multiple>
<el-option v-for="(item,index) in allCategories" :key="index" :label="item.name" :value="item._id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="難度">
<el-rate style="margin-top:0.6rem" :max="9" v-model="model.scores.difficult"></el-rate>
</el-form-item>
<el-form-item label="技能">
<el-rate style="margin-top:0.6rem" :max="9" v-model="model.scores.skills"></el-rate>
</el-form-item>
<el-form-item label="攻擊">
<el-rate style="margin-top:0.6rem" :max="9" v-model="model.scores.attack"></el-rate>
</el-form-item>
<el-form-item label="生存">
<el-rate style="margin-top:0.6rem" :max="9" v-model="model.scores.survive"></el-rate>
</el-form-item>
<el-form-item label="順風出裝">
<el-select v-model="model.items1" placeholder="請選擇" multiple>
<el-option v-for="(item,index) in Items" :key="index" :label="item.name" :value="item._id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="逆風出裝">
<el-select v-model="model.items2" placeholder="請選擇" multiple>
<el-option v-for="(item,index) in Items" :key="index" :label="item.name" :value="item._id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="使用技巧">
<el-input type="textarea" v-model="model.usageTips">
</el-input>
</el-form-item>
<el-form-item label="對抗技巧">
<el-input type="textarea" v-model="model.battleTips">
</el-input>
</el-form-item>
<el-form-item label="團戰技巧">
<el-input type="textarea" v-model="model.teamTips">
</el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="技能" name="skills">
<el-button type="primary" @click="model.skills.push({})"> <i class="el-icon-plus"></i>添加技能 </el-button>
<!-- 注意這裏採用了flex佈局,動態增加方框內容,特別酷 -->
<el-row type="flex" style="flex-wrap:wrap">
<el-col :md="12" v-for="(item,index) in model.skills" :key="index">
<el-form-item label='名稱'>
<el-input v-model="item.name"></el-input>
</el-form-item>
<el-form-item label="圖片">
<!-- 注意這裏上傳成功回調函數, -->
<el-upload class="avatar-uploader" :action="fileUploadPath" :show-file-list="false" :on-success="res=>$set(item, 'icon',res.imgTrueUrl)" :before-upload="beforeAvatarUpload">
<img v-if="item.icon" :src="item.icon" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item label="描述">
<el-input type="textarea" v-model="item.description">
</el-input>
</el-form-item>
<el-form-item label="小提示">
<el-input type="textarea" v-model="item.tips">
</el-input>
</el-form-item>
<el-form-item>
<!-- 刪除元素 -->
<el-button type="danger" @click="model.skills.splice(index,1)">刪除</el-button>
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
<el-form-item>
<el-button style="margin-top:15px" type="primary" native-type="submit"> 保存</el-button>
</el-form-item>
</el-form>
-
每個el-tab-pane有一個 name,當在父級 el-tabs 設置的 value與name相同時,默認載入該標籤頁
-
flex佈局中 flex-warp:warp 用於指定換行
-
elementui中不像antd,可以直接寫 小寫形式的style 比如 style=“margin-top:10px”
-
技能錄入中使用了一種非常酷的效果,點擊 新增技能 則動態生成一個技能卡片用來錄入信息。實現的原理是 技能卡片被el-col包裹,以v-for循環model.skills數組方式利用flex佈局展示在一個el-row中,其中el-row設置爲換行,當點擊 增加 按鈕時push到skills數組中一個新的空對象,完成新卡片的動態展示。刪除同理,利用splice函數將位於index的item切掉
-
在技能卡片中有上傳圖片功能,上傳完畢後設置圖片回顯url使用$set實現, item.icon= res.imgTrueUrl無效,因爲沒有觸發重新渲染。
具體效果如下:
文章管理
與上述英雄相同,新增ArticleEdit.vue和ArticleList.vue文件,修改內容,新增左側菜單入口,新增路由入口。大體相同。唯一不同的是文章管理多了一個富文本編輯器
富文本編輯器
使用vue2-editor,安裝
npm install vue2-editor --save
基本使用
import { VueEditor } from "vue2-editor";
......
<el-form-item label="內容">
<vue-editor v-model="model.body"></vue-editor>
</el-form-item>
- 導入組件
- v-model用於綁定富文本編輯器顯示的內容
通過利用瀏覽器工具查看頁面元素不難發現,富文本編輯器對文字樣式的操作就是對html標籤的操作,比如加粗,就是在選中的文字上加入了strong標籤,保存到數據庫時就是將編輯的文檔以html形式保存到數據中。
圖片自定義上傳位置
當vue2-editor插入一個圖片時通過瀏覽器工具可以發現他是將圖片轉在base64編碼在本地進行顯示,當提交到數據庫時也是將base64編碼作爲html中的一個字符串提交。如下
base64上傳下載會導致接口通信數據載荷太大,如上179KB,這樣是不合理的。
一般這種類型是將圖片上傳到專門的圖片服務器,然後將圖片地址保存到數據庫中,和之前所做的相同,所以我們要修改編輯器對圖片的處理方式。
<el-form-item label="內容">
<vue-editor useCustomImageHandler @image-added="handleImageAdded" v-model="model.body"></vue-editor>
</el-form-item>
......
async handleImageAdded(file, Editor, cursorLocation, resetUploader) {
var formData = new FormData();
formData.append("file", file);
let res = await this.$http.post("upload", formData);
Editor.insertEmbed(cursorLocation, "image", res.data.imgTrueUrl);
resetUploader();
}
- vue2-editor 提供 @image-added 鉤子函數來自定義上傳方式,handleImageAdded入參分別是 選擇的文件、編輯器、光標當前位置和一個復位函數。
- FormData是html中自帶對象,用來生成表單數據。我們利用原先的接口來上傳圖片,file是接口中定義圖片的字段。
用戶管理
與上述相同,增加頁面,修改路由,增加菜單入口,不在贅述
登錄頁面
新建登錄文件Login.vue,然後 使用el-card來實現頁面,如下:
<template>
<div class="login-container">
<el-card header="請先登錄" class="login-card">
<el-form @submit.native.prevent="login">
<el-form-item label="用戶名">
<el-input v-model="model.username"></el-input>
</el-form-item>
<el-form-item label="密碼">
<el-input v-model="model.password" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit">登錄</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
model: {}
};
},
methods: {
async login() {
const res = await this.$http.post("login", this.model);
//保存token
localStorage.token = res.data.token;
//跳轉首頁
this.$router.push("/");
//提示登錄成功
this.$message({
type: "success",
message: "登錄成功"
});
}
}
};
</script>
<style >
.login-card {
width: 30rem;
margin: 50px auto;
}
</style>
- login函數實現登錄接口請求並將返回的token保存在localstorage中
接口請求鑑權
後端設置每次接口請求都要驗證是否具有token來判斷請求的合法性,因此我們要在每一個請求上加上Authorization認證信息,爲了簡化操作,可以使用axios自帶的請求攔截器,如下:
http.interceptors.request.use((config) => {
if (localStorage.token) {
config.headers.Authorization = "bearer " + localStorage.token; //在請求頭增加token,其中Authorzation是規定的認證頭 bearer是業界通用的字符,注意後邊有一個字符
}
return config;
}, (error) => {
return Promise.reject(error)
})
- 請求攔截器實現只要是axios發出的請求,都會
上傳文件鑑權
由於後端nodejs在每個接口都加了token鑑權,而在axios的intercepter攔截中加的headers設置不會生效到el-upload組件中,因爲el-upload組件使用了自己實現的ajax來進行http請求,所以這裏需要爲el-upload配置一個headers。
爲方便獲取localStorage中的token,使用到了Vue的mixin ,如下:
//main.js
Vue.mixin({
computed: {
uploadUrl() {//上傳文件的url
return this.$http.defaults.baseURL + "/upload"
}
},
methods: {
getAuthHeaders() {
return { Authorization: `Bearer ${localStorage.token}` }
}
}
})
- 定義mixin(混入) 必須在new vue之前
- mixin定義之後,可以在Vue任意地方訪問了,
- mixin的定義和vue組件的定義相同,包括methods、computed等
使用mixin
//ItemEdits.js
<el-form-item label="圖標">
<!-- 注意uploadUrl和 getAuthHeaders 是定義在 main.js中的mixin -->
<el-upload class="avatar-uploader" :action="uploadUrl" :headers="getAuthHeaders()" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="model.icon" :src="model.icon" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
- 圖片上傳使用el-upload中headers來設置http請求的header,這裏用到了mixin中定義的函數,返回token。