目录

1. 权限管理功能开发

通过权限管理功能模块,可以通过对用户角色的分配,来定义用户的权限。即给每个用户提供一个特定的角色,每个角色包含不同的权限功能。

在权限管理中,有一个模块是给角色分配权限,点击“分配权限”,就会弹出对话框,上面显示所有的权限,并且已有的权限会被勾选,没有的权限没有被勾选,勾选权限就会给用户添加该权限。

【练习记录】基于Vue的电商后台管理系统(二)_数据

要实现这个功能,需要完成以下步骤:

分配权限按钮及对话框:

<el-button type="warning" icon="el-icon-setting" size="mini" @click="showSetRightDialog(scope.row)">分配权限</el-button>

<el-dialog
title="分配权限"
:visible.sync="setRightDialogVisible"
width="30%" @close= "setRightDialogClosed">
<el-tree :data="rightsList" :props="treeProps" show-checkbox node-key="id" default-expand-all="true" :default-checked-keys="defKeys" ref="treeRef"></el-tree>
<span slot="footer" class="dialog-footer">
<el-button @click="setRightDialogVisible = false"> </el-button>
<el-button type="primary" @click="allotRights"> </el-button>
</span>
</el-dialog>
  • 点击分配权限按钮,弹出对话框
async showSetRightDialog(role) {
// 获取当前跟节点的id值,并储存在roleId中
this.roleId = role.id
// 发起请求,获取所有权限列表
const result = await this.$http.get('rights/tree')
if (result.data.meta.status !== 200) {
return this.$message.error(result.data.meta.msg)
}
this.rightsList = result.data.data
// 遍历返回的数据,将其生成一棵树
this.getLeafKeys(role, this.defKeys)
// 展示对话框
this.setRightDialogVisible = true
}
  • 弹出对话框之前,使用递归,遍历出权限树,并渲染出来
// 递归遍历出所有的权限树
getLeafKeys(node, arr) {
if (!node.children) {
return arr.push(node.id)
}
node.children.forEach(item => this.getLeafKeys(item, arr))
}
  • 修改权限,并进行提交更新

这里使用到了树形组件的两个方法:

【练习记录】基于Vue的电商后台管理系统(二)_js_02


【练习记录】基于Vue的电商后台管理系统(二)_js_03


使用这两个方法就可以获取到所有被选中的一二三级权限的id值,使用展开运算符将他们存放在数组中。

async allotRights() {
const keys = [...this.$refs.treeRef.getCheckedKeys(), ...this.$refs.treeRef.getHalfCheckedKeys()]
const idStr = keys.join(',')

const result = await this.$http.post(
`roles/${this.roleId}/rights`,
{ rids: idStr }
)
if (result.data.meta.status !== 200) {
return this.$message.error(result.data.meta.msg)
}
this.$message.success(result.data.meta.msg)
// 重新获取角色列表
this.getRolesList()
// 关闭对话框
this.setRightDialogVisible = false
}
  • 清空储存已选中节点的数组

我们设置了​​:default-checked-keys="defKeys"​​,来获取所有被选中的节点的数组,当我们选择完一个对话框之后,再打开一个对话框,原有的还存在,这样数组会越来越多,不能正常显示该用户的节点。所每次关闭对话框之后,我们需要将defKeys数组进行清空处理:

setRightDialogClosed() {
this.defKeys = []
}

2. 商品管理功能开发

2.1 商品分类

在这部分中,总共有三个模块,其中商品分类模块,使用到了Vue的一个插件:​​vue-table-with-tree-grid​​,这是一个树形结构,用来展示多层分级。

在这个模块中,除了页面的渲染之外,最主要的是“添加分类”功能的实现,下面再来梳理一下这个功能实现的基本步骤。

// 添加分类对话框
<el-dialog
title="添加分类"
:visible.sync="addCateDialogVisible"
width="50%"
@close ="addCateDialogClosed">
<el-form :model="addCateForm" label-width="100px" :rules="addCateFormRules" ref="addCateFormRef">
// 分类名称
<el-form-item label="分类名称:" prop="cat_name">
<el-input v-model="addCateForm.cat_name"></el-input>
</el-form-item>
// 父级分类
<el-form-item label="父级分类:">
<el-cascader
v-model="selectedKeys"
:options="parentCateList"
:props="cascaderProps"
@change="parentCateChanged"
:change-on-select = "true"
clearable>
</el-cascader>
</el-form-item>
</el-form>
// 底部按钮
<span slot="footer" class="dialog-footer">
<el-button @click="addCateDialogVisible = false"> </el-button>
<el-button type="primary" @click="addCate"> </el-button>
</span>
</el-dialog>

// data
data() {
return {
// 表单是否显示的属性
addCateDialogVisible: false,
// 添加分类表单的参数
addCateForm: {
cat_name: '',
cat_id: 0,
cat_level: 0
},
// 添加表单的验证规则
addCateFormRules: {
cat_name: [
{ required: true, message: '请输入分类名称', trigger: 'blur' }
]
},
// 父级分类的列表
parentCateList: [],
cascaderProps: {
expandTrigger: 'hover',
value: 'cat_id',
label: 'cat_name',
children: 'children'
},
// 选中的父级的id,它是一个数组,因为父级可能不止是一个
selectedKeys: []
}
},
  • 首先,点击按钮,展示添加分类的对话框,并请求父级分类的数据
showAddCateDialog() {
// 请求父级分类的列表
this.getParentCateList()
// 显示对话框
this.addCateDialogVisible = true
}
// 请求父级分类的函数
async getParentCateList() {
const result = await this.$http.get('categories', {
params: {
type: 2 // 2表示三级分类
}
})
if (result.data.meta.status !== 200) {
return this.$message.error(result.data.meta.msg)
}
// 将获取到的数据储存起来
this.parentCateList = result.data.data
}
  • 监听父级分类的变化,把获取到的父类的值保存在表单的参数列表中
parentCateChanged() {
if (this.selectedKeys.length > 0) {
// 父级分类的id,等于数组中最后一个父级分类的id
this.addCateForm.cat_id = this.selectedKeys[this.selectedKeys.length - 1]
// 分类等级,等于数组的长度,也就是有几层妒父级分类
this.addCateForm.cat_level = this.selectedKeys.length
} else {
this.addCateForm.cat_id = 0
this.addCateForm.cat_level = 0
}
}
  • 点击确定按钮,进行预验证,预验证通过后,进行提交处理
addCate() {
// 表单预验证
this.$refs.addCateFormRef.validate(async valid => {
if (!valid) {
return 0
}
// 发起请求
const result = await this.$http.post('categories', this.addCateForm)
if (result.data.meta.status !== 201) {
return this.$message.error(result.data.meta.msg)
}
this.$message.success(result.data.meta.msg)
// 重新获取分类列表
this.getCateList()
// 关闭对话框
this.addCateDialogVisible = false
})
}
  • 对话框关闭后的一些后续工作,以便于下次添加不出错
addCateDialogClosed() {
// 清空表单中的数据
this.$refs.addCateFormRef.resetFields()
// 清空已选择的父级分类的id数据
this.selectedKeys = []
// 将获取到的参数列表初始化,便于下次使用
this.addCateForm.level = 0
this.addCateForm.cat_id = 0
}

在这个过程中,还遇到一个小问题,就是Element UI中的级联选择器的选择框的高度会溢出,高度和浏览器高度一样,和预期效果不一致。解决办法就是:在全局样式中给级联选择器的菜单指定一个高度值:

.el-cascader-menu { height: 300px; }

总的来说,别看是一个小小的添加功能,涉及到的知识还是蛮多的,需要慢慢理解。

2.2 分类参数

商品参数用于显示商品的固定的特征信息,可以在电商平台的商品详情页看到。

商品参数分为两种:静态参数动态参数,下面来举例说明

动态参数:

【练习记录】基于Vue的电商后台管理系统(二)_数组_04


静态参数:

【练习记录】基于Vue的电商后台管理系统(二)_数据_05


在获取参数列表时,我们需要对数据进行处理:

async getParamsData() {
// 判断是否选中的是三级分类,如果不是,就把选中项的id数组清空,还要把表单中的数据清空
if (this.selectedCateKeys.length !== 3) {
this.selectedCateKeys = []
this.manyTableData = []
this.onlyTableData = []
return 0
}
// 发起数据请求,获取参数
const result = await this.$http.get(`categories/${this.cateId}/attributes`, {
params: {
sel: this.activeName
}
})
if (result.data.meta.status !== 200) {
return this.$message.error(result.data.meta.msg)
}
// 获取到的属性是一个以空格分隔的字符串,需要将其转化为数组
result.data.data.forEach(item => {
// 如果这个属性为空,就要把他设置我饿一个空数组,不然会渲染出一个空的Tag标签
item.attr_vals = item.attr_vals ? item.attr_vals.split(' ') : []
// 控制文本框的显示与隐藏
item.inputVisible = false
// 文本框中输入的值
item.inputValue = ''
})
// 判断请求到的数据是静态属性还是动态参数
if (this.activeName === 'many') {
this.manyTableData = result.data.data
} else {
this.onlyTableData = result.data.data
}
}

在属性操作时,我们需要对添加属性值来做处理:

【练习记录】基于Vue的电商后台管理系统(二)_数组_06


这个New Tag按钮,点击时会变成一个输入框,变成输入框的时候,自动获得输入焦点。输入值之后,点击回车按钮或者失去焦点值,就可以提交数据。

<el-table-column type="expand">
<template slot-scope="scope">
<!-- 循环渲染Tag标签 -->
<el-tag v-for="(item, i) in scope.row.attr_vals" :key="i" closable @close="handleClose(i, scope.row)">{{item}}</el-tag>
<!-- 输入的文本框 -->
<el-input class="input-new-tag" v-if="scope.row.inputVisible" v-model="scope.row.inputValue" ref="saveTagInput" size="small" @keyup.enter.native="handleInputConfirm(scope.row)" @blur="handleInputConfirm(scope.row)">
</el-input>
<!-- 添加按钮 -->
<el-button v-else class="button-new-tag" size="small" @click="showInput(scope.row)">+ New Tag</el-button>
</template>
</el-table-column>

点击New Tag标签时,显示输入框的操作:

showInput(row) {
// 展示输入框
row.inputVisible = true
// 让文本框自动获得焦点
// $nextTick 方法的作用:当页面上元素被重新渲染之后,才会指定回调函数中的代码
this.$nextTick(_ => {
this.$refs.saveTagInput.$refs.input.focus()
})
}

输入框失去焦点或者按下Enter键时出发该函数:

async handleInputConfirm(row) {
// 如果输入的值无效,就将输入框清空,并把输入框变回Tag标签
if (row.inputValue.trim().length === 0) {
row.inputValue = ''
row.inputVisible = false
return
}
// 如果没有return,则证明输入的内容有效,将输入的内容保存在数据中,并且清空输入框,变回Tag标签
row.attr_vals.push(row.inputValue.trim())
row.inputValue = ''
row.inputVisible = false
// 发起请求,重新渲染所有的属性标签
this.saveAttrVals(row)
}

删除属性值比较简单:

handleClose(i, row) {
// 从数组中找到需要删除项的索引,从数组中删除即可
row.attr_vals.splice(i, 1)
// 发起请求,重新渲染所有的属性标签
this.saveAttriVals(row)
}

开始的时候,我们把 输入框的以下这两个属性的属性值都设置成了一个布尔值,但是这就引发了一个问题,我们给一个属性添加属性值时,表单的其他属性也会同步显示输入框以及正在输入的值。为了解决这个问题,我们把这两个属性设置成为动态的:

// 修改前
v-if="inputVisible"
v-model="inputValue"‘’

// 修改后
v-if="scope.row.inputVisible"
v-model="scope.row.inputValue"

这样这两个属性就变成了这一行的属性,改变他的属性值,只会改变这一行的显示隐藏以及数据值。

这一部分还有一个Bug:就是点击之后变成的输入框默认为表格的宽度,对宽度进行设置之后不生效,查了一些资料,也没查出个所以然来…

然后在​​style​​​上加上了​​scoped​​属性,就可以了,只对该文件中的样式起作用。

2.3 商品列表

前端获取到的时间数据是一个以毫秒计时的数字,所以我们需要将这个数据进行转化。这里可以注册一个全局过滤器,对数据进行过滤操作:

// 该过滤器的名字为:dataFormat,originVal为传递的毫秒数
Vue.filter('dataFormat', function(originVal) {
const dt = new Date(originVal)
const y = dt.getFullYear()
const m = (dt.getMonth() + 1 + '').padStart(2, '0')
const d = (dt.getDate() + 1 + '').padStart(2, '0')
const hh = (dt.getHours() + 1 + '').padStart(2, '0')
const mm = (dt.getMinutes() + 1 + '').padStart(2, '0')
const ss = (dt.getSeconds() + 1 + '').padStart(2, '0')
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
})

基本使用:

<template slot-scope="scope">
{{scope.row.add_time | dataFormat}}
</template>

在商品列表模块中,商品的展示比较简单,单纯的数据渲染以及一些简单的操作,最主要的是添加商品的功能。

我们在上方添加一个步骤条来显示当前操作的模块,当前激活的步骤是number类型;左侧是一个标签页,当前标签的绑定的是是一个string类型。所以我们设置一个​​activeIndex​​​为字符串类型,在步骤条中只要绑定​​activeIndex-0​​就是一个数字类型了。这样步骤条和标签就可以同步了。

1. 商品图片上传功能的实现

在商品图片上传模块,我们需要点击上传进行图片的上传,来梳理一下整个过程:

  • 上传按钮的静态部分
<el-upload
:action="uploadUrl" //上传的服务器的路径,是一个全路径,可以在data属性中定义
:headers="headerObj" // 配置请求头,携带token
:on-preview="handlePreview" // 点击图片预览的事件
:on-remove="handleRemove" // 删除图片触发的事件
list-type="picture" // 图片展示的类型
:on-success="handleSuccess"> // 图片上传成功触发的事件
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>

// 图片预览对话框
<el-dialog
title="图片预览"
:visible.sync="previewVisible"
width="50%">
<img :src="previewPath" class="previewImg">
</el-dialog>

// 数据
data() {
return {
// 这里还有图片上传的服务器的地址,
headerObj: {
Authorization: window.sessionStorage.getItem('token')
},
previewPath: '',
previewVisible: false
}
}
  • 上传成功的方法

因为表单提交时,pics的属性值是一个数组,数组中的每一个图片地址是一个对象:

【练习记录】基于Vue的电商后台管理系统(二)_javascript_07


先来看一下返回的数据:

【练习记录】基于Vue的电商后台管理系统(二)_数组_08


所以要对返回的数据做一些处理:

handleSuccess(res) {
const picInfo = {
pic: res.data.tmp_path
}
this.addForm.pics.push(picInfo)
}
  • 删除图片时触发的事件

首先来看一下,点击删除时,会返回一个该图片的数据:

【练习记录】基于Vue的电商后台管理系统(二)_数据_09


我们想要删除这个图片,只要找到该图片的路径。然后从数组中的图片找到和这个路径一致的图片的索引值,从数组中删除即可:

handleRemove(file) {
// 获取当前文件的路径
const filePath = file.response.data.tmp_path
// 在数组中寻找和这个路径一致的图片的索引
const i = this.addForm.pics.findIndex(x => x.pic === filePath)
// 根据索引删除对应的图片
this.addForm.pics.splice(i, 1)
}

这里使用到了数组的​​findIndex​​​方法,下面是关于​​findIndex​​方法的解释:

【练习记录】基于Vue的电商后台管理系统(二)_vue_10

  • 最后一步就是图片的展示功能了
handlePreview(file) {
// 获取图片的URL地址,并赋值给需要渲染的图片
this.previewPath = file.response.data.url
// 展示对话框
this.previewVisible = true
}

2. 商品内容功能

在一块中,我们需要使用富文本编辑器,这里使用到了一个Vue的插件:​​vue-quill-editor​​,点击可以查看对应的官方文档。

3. 表单数据的梳理

最后一步,也是比较繁杂的一步,就是需要按照接口文档中提交表单的标准,对表单的数据进行梳理。

格式要求:

【练习记录】基于Vue的电商后台管理系统(二)_数据_11


添加商品的方法,在里面进行数据的梳理:

注意,这个表单数据的​​goods_cat​​​是一个数组,我们要将其转化为一个字符串,但是不能直接操作,因为级联选择器中必须要使用这个数组,如果直接修改成字符串,会报错。所以我们可以对已获得的表单进行​​深拷贝​​,对拷贝后的数据进行操作,就不会有问题了。

这里使用一个插件:​​Lodash​​,这是一个实用的JavaScript工具库,上面有很多实用的方法。

动态参数和静态属性的格式要求:

【练习记录】基于Vue的电商后台管理系统(二)_javascript_12

// 先安装lodash,再引入
import _ from 'lodash'

add() {
// 表单的预验证
this.$refs.addFormRef.validate(async valid => {
if (!valid) {
return this.$message.error('请输入必要的商品信息!')
}
// 对表单进行深拷贝
const form = _.cloneDeep(this.addForm)
// 将拷贝后的对象中的goods_cat数组进行拼接,拼接成一个字符串
form.goods_cat = form.goods_cat.join(',')

// 对动态参数数据进行操作
this.manyTableData.forEach(item => {
const newInfo = {
attr_id: item.attr_id,
attr_vals: item.attr_vals.join(' ')
}
this.addForm.attrs.push(newInfo)
})

// 对静态属性数据进行操作
this.onlyTableData.forEach(item => {
const newInfo = {
attr_id: item.attr_id,
attr_vals: item.attr_vals
}
this.addForm.attrs.push(newInfo)
})
form.attrs = this.addForm.attrs

const result = await this.$http.post('goods', form)
if (result.data.meta.status !== 201) {
return this.$message.error(result.data.meta.msg)
}
this.$message.success(result.data.meta.msg)
// 跳转回商品列表的页面
this.$router.push('/goods')
})
}

这样商品列表的功能就基本实现了。