前言
使用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的 s e t 方 法 为 m o d e l 对 象 属 性 赋 值 , set方法为model对象属性赋值, set方法为model对象属性赋值,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。