记录尚品汇后台管理系统的开发过程,功能模块包括登录、首页、品牌管理、平台属性管理、SKU管理、SPU管理、用户管理、角色管理、菜单管理模块。后台管理系统是CMS内容管理系统的一个子集,通过项目实战可以彻底搞明白菜单权限、按钮权限如何实现,掌握市场中数据可视化ECharts、V-charts的运用。
主要涵盖的技术点:Vue-cli、Axios、Vuex、Element-UI、菜单权限、按钮权限、数据可视化、Scss……
由于项目写的太长了,编辑器真的很卡呜呜呜,因此将项目分成了上、中、下三部分.
【Vue】项目:尚品汇后台管理系统(中)
【Vue】项目:尚品汇后台管理系统(下)
1.开发后台管理系统项目也是网页,用于进行信息系统管理的应用。类似学生管理系统等。
2.后台管理项目有一些模板可以使用,能够方便程序员搭建项目框架。
简洁版: https://github.com/PanJiaChen/vue-admin-template (本次使用)
加强版: https://github.com/PanJiaChen/vue-element-admin
(如果github进不去可以在gitee上下载:https://gitee.com/panjiachen/vue-admin-template?_from=gitee_search)
3.模板的文件与文件夹的认知【简洁版】
build ----index.js webpack配置文件【很少修改这个文件】
mock ----mock数据的文件夹【模拟一些假的数据mockjs实现的】,因为实际开发的时候,利用的是真是接口
node_modules ------项目依赖的模块
public ------ico图标,静态页面,publick文件夹里面经常放置一些静态资源,而且在项目打包的时候webpack不会编译这个文件夹,原封不动的打包到dist文件夹里面
src
-----程序员源代码的地方
-----api文件夹:涉及请求相关的
-----assets文件夹:里面放置一些静态资源(一般共享的),放在aseets文件夹里面静态资源,在webpack打包的时候,会进行编译
-----components文件夹:一般放置非路由组件获取全局组件
-----icons这个文件夹的里面放置了一些svg矢量图
-----layout文件夹:他里面放置一些组件与混入
-----router文件夹:与路由相关的
----store文件夹:一定是与vuex先关的
----style文件夹:与样式先关的
-----utils文件夹:request.js是axios二次封装文件
-----views文件夹:里面放置的是路由组件
App.vue:根组件
main.js:入口文件
permission.js:与导航守卫相关
settings:项目配置项文件
1.首先从github上下载简洁项目。
2.原始项目中并没有依赖环境,因此需要运行cmd,cnpm install 安装依赖环境。
3.运行项目:查看package.json
可知运行项目需要使用的语句是,npm run dev
遇到报错:* core-js/modules/es. …
4.后台管理系统API接口在线文档:
http://39.98.123.211:8170/swagger-ui.html
http://39.98.123.211:8216/swagger-ui.html
1.找到登录业务所在位置,并修改文字。
2.书写API,将api user.js中的接口换成在线文档中真实的接口
// api/user.js中
// 引入axios(axios进行二次封装)
import request from '@/utils/request'
// 对外暴露登录接口函数
export function login(data) {
return request({
url: '/admin/acl/index/login',
method: 'post',
data
})
}
// 对外暴露获取用户信息的函数
export function getInfo(token) {
return request({
url: '/admin/acl/index/info',
method: 'get',
params: { token }
})
}
// 对外暴露退出登录的函数
export function logout() {
return request({
url: '/admin/acl/index/logout',
method: 'post'
})
}
if (res.code !== 20000 && res.code !== 200)
4.换成真实接口之后需要解决代理跨域问题:webpack涉及的知识
,可以去webpack官网-配置-devServer-devServer.proxy中寻找
// vue.config.js
// 配置代理跨域
proxy: {
'/dev-api': {
target: 'http://39.98.123.211',
pathRewrite: { '^/dev-api': '' }
}
}
静态页面的更改:layout中包含系统的框架,比如菜单栏等,其中Navbar.vue中有退出等按钮,修改即可。
同时也要修改路由为真实路由。
删除views中不需要的组件,只留下dashboard和login,同时删除相关路由。
创建project文件夹,里面存放管理系统需要的功能(tradeMark,Attr,Sku,Spu),分别创建vue文件。
模仿其他路由的书写方式,编写router中的index.js路由部分
{
path: '/product',
component: Layout,
name: 'Product',
meta: {
title:'商品管理',icon:'el-icon-goods'
},
children: [
{
path: 'trademark',
name: 'Trademark',
component: () => import('@/views/product/tradeMark'),
meta: {title:'品牌管理'}
},
{
path: 'attr',
name: 'Attr',
component: () => import('@/views/product/Attr'),
meta: {title:'品牌属性'}
},
{
path: 'sku',
name: 'Sku',
component: () => import('@/views/product/Sku'),
meta: {title:'Sku管理'}
},
{
path: 'spu',
name: 'Spu',
component: () => import('@/views/product/Spu'),
meta: {title:'Spu管理'}
}
]
}
.app-main {
padding: 20px;
}
<el-button type="primary" icon="el-icon-plus" style="margin: 10px 0">添加el-button>
<el-table style="width: 100%" border>
<el-table-column prop="prop" label="序号" width="80px" align="center" type="index">
el-table-column>
<el-table-column prop="prop" label="品牌名称" width="width">
el-table-column>
<el-table-column prop="prop" label="品牌LOGO" width="width">
el-table-column>
<el-table-column prop="prop" label="操作" width="width">
el-table-column>
el-table>
border:添加边框 ----整个表格是有边框的
label:显示的标题 ----每一列的表头是什么
width:对应列的宽度 ----第一列的宽度不一样,width的就是均分
align:对齐方式 左 中 右 ----序号列是居中对齐
<el-pagination
style="margin-top:20px;text-align:center"
:current-page="1"
:page-sizes="[3, 5, 10]"
:page-size="3"
:pager-count="7"
layout="prev, pager, next, jumper,->,sizes,total"
:total="99"
@current-change="getPageList"
@size-change="handleSizeChange"
>
</el-pagination>
current-page当前第几页
total总页数
page-size每页数据数
page-sizes每页数据数备选
layout布局位置 — ->是居右
pager-count页面有的页码数,连续页码数为pager-count - 2 个
数据获取,首先书写获取数据的请求,在线文档中有API.
// api/tradeMark.js中
import request from '@/utils/request'
export const reqTradeMarkList = (page, limit) => request({ url: `/admin/product/baseTrademark/${page}/${limit}`, method: 'get' });
// main.js中
// 引入相关API请求接口
import API from '@/api'
//将API挂载在原型上
// 任何组件可以使用API相关接口
Vue.prototype.$API = API
data() {
return {
page:1,
limit:3,
total:0,
list:[]
}
mounted(){
this.getPageList()
},
methods:{
async getPageList(pager = 1){
this.page = pager
//解构参数
const {page,limit} = this
let result = await this.$API.trademark.reqTradeMarkList(page,limit)
if(result.code == 200){
this.total = result.data.total
this.list = result.data.records
}
},
}
数据展示
数组的数据在el-table中用data表示,每一列的数据用el-table-column中的prop表示。
如果表格中含有图片、按钮等其他结构,需要使用定义域插槽
。定义域插槽的理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定
。
<el-table style="width: 100%" border :data="this.list">
<el-table-column prop="prop" label="序号" width="80px" align="center" type="index">
el-table-column>
<el-table-column prop="tmName" label="品牌名称" width="width">
el-table-column>
<el-table-column prop="prop" label="品牌LOGO" width="width">
<template slot-scope="{row,$index}">
<img :src="row.logoUrl" alt="" style="width:100px;height:100px">
template>
el-table-column>
<el-table-column prop="prop" label="操作" width="width">
<template slot-scope="{row,$index}">
<el-button icon="el-icon-edit" type="warning" size="mini">修改el-button>
<el-button icon="el-icon-delete" type="danger" size="mini">删除el-button>
template>
el-table-column>
el-table>
分页器点击的实现
分页器还包括两个事件
@current-change=“handleCurrentChange” ----点击页码的时候触发,会携带着点击的页码paper,可以和数据加载放在一起写,因为更换页码后也要重新请求。
@size-change=“handleSizeChange” ----修改每页展示的数目,会携带这改变后的个数。
分页器相关代码
<el-pagination
style="margin-top:20px;text-align:center"
:current-page="page"
:page-sizes="[3, 5, 10]"
:page-size="3"
:pager-count="7"
layout="prev, pager, next, jumper,->,sizes,total"
:total="total"
@current-change="getPageList"
@size-change="handleSizeChange"
>
methods:{
async getPageList(pager = 1){
this.page = pager
//解构参数
const {page,limit} = this
let result = await this.$API.trademark.reqTradeMarkList(page,limit)
if(result.code == 200){
this.total = result.data.total
this.list = result.data.records
}
},
// 当分页器某一页需要展示数据条数发生变化时会触发
handleSizeChange(limit){
this.limit = limit
this.getPageList()
}
}
点击添加或者修改会弹出相同的遮罩层,element-ui中有这个组件。图片上传也有相应的组件。
相关代码
<el-dialog title="添加品牌" :visible.sync="dialogFormVisible">
<el-form style="width: 80%" :model="tmForm">
<el-form-item label="品牌名称" label-width="100px">
<el-input autocomplete="off" v-model="tmForm.tmName">el-input>
el-form-item>
<el-form-item label="品牌LOGO" label-width="100px">
<el-upload
class="avatar-uploader"
action="dev-api/admin/product/fileUpload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="tmForm.logoUrl" :src="tmForm.logoUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon">i>
<div slot="tip" class="el-upload__tip">
只能上传jpg/png文件,且不超过500kb
div>
el-upload>
el-form-item>
el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消el-button>
<el-button type="primary" @click="addOrUpdateTradeMark">确 定el-button>
div>
el-dialog>
export const reqAddOrUpdateTradeMark = (tradeMark) => {
if (tradeMark.id) {
//修改品牌
return request({ url: '/admin/product/baseTrademark/update', method: 'put', data: tradeMark })
} else {
// 新增品牌
return request({ url: '/admin/product/baseTrademark/save', method: 'post', data: tradeMark })
}
}
data() {
return {
tmForm: {
logoUrl: "",
tmName: "",
},
};
},
添加品牌请求携带的参数是品牌名称和品牌图片,因此首先要收集到弹出框的信息,element-ui中用于收集表单元素的标签是 :model,可以告诉表单把数据收集到哪个元素身上。文字的收集采用v-model,图片的收集要使用action,可以设置图片的上传的地址。el-upload标签还有两个事件,:on-success:可以监测图片上次成功,会执行一次,:before-upload 可以在上传图片之前,会执行一次。
图片上传成功之前,要对图片进行格式大小判断,上传之后,要将图片url传递给tmForm中的logoUrl,展示的图片也要变成logoUrl。(见八的相关代码)
点击确认以后,首先隐藏弹出框,然后待着参数发请求,并弹出结果,重新获取数据。
// 点击添加
showDialog() {
this.dialogFormVisible = true;
// 每次点击添加之前都要清空数据,避免残留
this.tmForm.logoUrl = ""
this.tmForm.tmName = ""
},
//图片上传成功
handleAvatarSuccess(res, file) {
this.tmForm.logoUrl = res.data;
},
//图片上传之前
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;
},
//添加或者修改品牌
async addOrUpdateTradeMark(){
this.dialogFormVisible = false
const result = await this.$API.trademark.reqAddOrUpdateTradeMark(this.tmForm)
if(result.code == 200){
//弹出成功信息
this.$message({
type:'success',
message:this.tmForm.id?'修改品牌成功':'添加品牌成功'
})
this.getPageList(this.tmForm.id?this.page:1)
}
}
async updataTradeMark(row) {
this.dialogFormVisible = true;
// 不能直接修改tmForm,因为如果修改后取消了,但是页面已经展示出来了。因此,使用浅拷贝
this.tmForm = {...row}
},
1.添加和修改弹框中,应该对表单数据进行验证。包括品牌名称的长度以及是否上传了图片。
2.element-ui中包括表单验证的内容,分为表单验证和自定义规则验证。在Form表单模块中。
3.表单验证的使用:在需要验证的表单中加上:rules=“rules”,并在data()中书写rules规则
rules: {
tmName: [
// required是否必须校验 message提示信息 trigger用户行为设置 blur失焦 change文本变化
{ required: true, message: "请输入品牌名称", trigger: "blur" },
{
min: 2,
max: 10,
message: "长度在 2 到 10 个字符",
trigger: "change",
},
],
logoUrl: [{ required: true, message: "请选择品牌的图片" }],
},
4.自定义规则验证:放弃了原本有的最大值最小值设定,改为自己在data中书写规则
var validateTMName = (rule, value, callback) => {
if (value.length < 2 || value.length > 10) {
callback(new Error("长度在 2 到 10 个字符"));
} else {
callback();
}
};
return {
rules: {
tmName: [
// required是否必须校验 message提示信息 trigger用户行为设置 blur失焦 change文本变化
{ required: true, message: "请输入品牌名称", trigger: "blur" },
// 自定义校验规则
{ validator: validateTMName, trigger: "blur" },
],
logoUrl: [{ required: true, message: "请选择品牌的图片" }],
},
1.点击删除以后,会弹出一个弹框,这个弹框在element-ui中是MessageBox 弹框
,因此需要在点击删除品牌的回调函数中加入弹框相关代码,并修改文字。
2.如果点击的是弹框的确定,则进入请求阶段,API在线文档中包含删除品牌的请求,操作与之前的请求方式都是相同的。
3.需要注意的小点是,删除完毕后,如果当前列表的长度大于1,那么就返回当前页码的页面,如果小于1,则返回上一页。
4.相关代码(省略API的请求部分)
deleteTradeMark(row) {
//MessageBox 弹框
this.$confirm(`你确定删除${row.tmName}品牌吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async() => {
this.$message({
type: "success",
message: "删除成功!",
});
let result = await this.$API.trademark.reqDeletTradeMark(row.id)
if(result.code === 200){
this.getPageList(this.list.length>1?this.page:this.page-1)
}
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},