Vue实战电商系统-五商品管理
商品管理
1.新建goods_cate子分支并上传码云
git branch
git checkout -b goods_cate
git push -u origin goods_cate
2.商品管理-商品分类
1.新建文件并配置路由
components/goods/Cate.vue
2.页面布局
<template>
<div>
<!-- 面包屑导航区域 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>商品管理</el-breadcrumb-item>
<el-breadcrumb-item>商品分类</el-breadcrumb-item>
</el-breadcrumb>
<!-- 卡片区域 -->
<el-card>
<!-- 添加分类按钮 -->
<el-row>
<el-col :span="4">
<el-button type="primary">添加分类</el-button>
</el-col>
</el-row>
<!-- 表格数据区域 -->
<el-table :data="userlist" style="width: 100%" border stripe :cell-style="rowClass" :header-cell-style="headClass">
<el-table-column type="index" label="#"></el-table-column>
<el-table-column prop="username" label="分类名称"></el-table-column>
<el-table-column prop="email" label="是否有效"></el-table-column>
<el-table-column prop="mobile" label="排序"></el-table-column>
<el-table-column label="操作" width="180px">
<template slot-scope="scope">
<el-button type="primary" icon="el-icon-edit" size="mini">编辑</el-button>
<el-button type="danger" icon="el-icon-delete" size="mini">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage4"
:page-sizes="[100, 200, 300, 400]"
:page-size="100"
layout="total, sizes, prev, pager, next, jumper"
:total="400">
</el-pagination>
</el-card>
</div>
</template>
3.获取分类列表数据
async getCateList() {
const { data: res } = await this.$http.get('categories', { params: this.queryInfo })
if (res.meta.status !== 200) return this.$message.error('获取分类列表信息失败!')
this.catelist = res.data.result
console.log(this.catelist)
this.total = res.data.total
},
4.将数据渲染在树形表格控件中
因为elementUI并没有提供树形表格控件,所以需要用到第三方组件库。
在vue可视化面板中安装一个新的运行依赖:vue-table-with-tree-grid
。如下图:
接着在main.js
中导入新依赖并全局注册为组件:
// 导入第三方的树形表格控件
import TreeTable from 'vue-table-with-tree-grid'
...
Vue.component('tree-table',TreeTable)
<!-- 表格数据区域 -->
<tree-table
:data="catelist"
:columns="columns"
:selection-type="false"
:expand-type="false"
:show-index="true"
index-text="#"
border
:show-row-hover="false">
<!-- 是否有效列的模板 -->
<template slot="isok" slot-scope="scope">
<i class="el-icon-success" v-if="scope.row.cat_deleted === false" style="color: lightgreen;"></i>
<i class="el-icon-error" v-else style="color: red;"></i>
</template>
<!-- 排序列的模板 -->
<template slot="order" slot-scope="scope">
<el-tag size="mini" v-if="scope.row.cat_level === 0">一级</el-tag>
<el-tag size="mini" type="success" v-else-if="scope.row.cat_level === 1">二级</el-tag>
<el-tag size="mini" type="warning" v-else>三级</el-tag>
</template>
<!-- 操作列的模板 -->
<template slot="opt" slot-scope="scope">
<el-button type="primary" icon="el-icon-edit" size="mini">编辑</el-button>
<el-button type="danger" icon="el-icon-delete" size="mini">删除</el-button>
</template>
</tree-table>
...
columns: [
{
label: '分类名称',
prop: 'cat_name'
},
{
label: '是否有效',
// 表示将当前列自定义为模板列
type: 'template',
// 表示当前这一列使用的自定义模板的名称
template: 'isok'
},
{
label: '排序',
type: 'template',
template: 'order'
},
{
label: '操作',
type: 'template',
template: 'opt'
}
]
5.实现分页
<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryInfo.pagenum"
:page-sizes="[3, 5, 10, 15]"
:page-size="queryInfo.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
...
// 监听pagesize改变事件
handleSizeChange(newSize) {
this.queryInfo.pagesize = newSize
this.getCateList()
},
// 监听页码值改变的函数
handleCurrentChange(newPage) {
this.queryInfo.pagenum = newPage
this.getCateList()
}
6.添加分类
在这里插入代码片
碰到级联选择器占满整个屏幕(高度太大)并且数据显示不全时,添加一个样式即可:
.el-cascader-panel { height: 300px; }
级联选择器默认只能选中最小层的选项,也就是说无法选择那些父级选项。要实现都可以选择的功能的话,添加一个属性:
change-on-select
。新版本在选项前会出现一个圆圈。
7.删除分类
删除,编辑这些功能逻辑和之前的用户管理,角色管理都类似。
// 删除分类
async deleteCateById(id) {
const confirmResult = await this.$confirm('确定将此分类删除吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(err => err)
console.log(confirmResult)
if (confirmResult !== 'confirm') {
return this.$message.info('已取消删除')
}
const { data: res } = await this.$http.delete('categories/' + id)
console.log(res)
if (res.meta.status !== 200) return this.$message.error('删除分类失败!')
this.$message.success('删除分类成功!')
this.getCateList()
},
8.编辑分类
// 展开编辑分类信息的对话框
async showEditCateDialog(id) {
const { data: res } = await this.$http.get('categories/' + id)
console.log(res)
this.editCateForm = res.data
this.editCatedialogVisible = !this.editCatedialogVisible
},
// 监听编辑分类对话框的关闭事件
editCatesDialogClosed() {
this.$refs.editCateFormRef.resetFields()
},
editCate() {
console.log(this.editCateForm)
this.$refs.editCateFormRef.validate(async valid => {
if (!valid) return
// 可以发起添加请求
const { data: res } = await this.$http.put('categories/' + this.editCateForm.cat_id, { cat_name: this.editCateForm.cat_name })
if (res.meta.status !== 200) {
this.$message.error('更新分类信息失败')
}
// 更新成功之后关闭对话框
this.editCatedialogVisible = !this.editCatedialogVisible
// 更新成功之后重新获取用户信息列表
this.getCateList()
// 修改成功后的提示信息
this.$message.success('更新用户信息成功!')
})
}
3.商品管理-分类参数
商品的参数可以大致分为两类:静态参数和动态参数。例如一台手机,静态参数就有品牌,上市时间,型号等等,动态参数就有颜色,存储容量(8+128GB,8+256GB)等等。
1.页面布局
2.获取所有分类参数列表(级联选择器)
data() {
return {
// 所有的分类数据列表
cateList: [],
// 指定级联选择器的配置对象
cascaderProps: {
value: 'cat_id',
label: 'cat_name',
children: 'children',
expandTrigger: 'hover'
},
// 选中的父级分类的id数组
selectedKeys: []
}
},
created() {
this.getCateList()
},
methods: {
// 获取所有的分类数据列表
async getCateList() {
const { data: res } = await this.$http.get('categories')
if (res.meta.status !== 200) return this.$message.error('获取商品分类失败!')
this.cateList = res.data
console.log(this.cateList)
},
// 监听级联菜单选择项改变事件
cateChanged() {
console.log(this.selectedKeys)
// 如果selectedKeys数组中的length > 0,证明选中了父级分类
// 反之就说明没有选择任何父级分类
if (this.selectedKeys.length > 0) {
} else {
}
}
}
注意,这里的级联菜单只允许为第三级分类设置相关参数,因此在监听菜单选择项改变事件中,如果selectedKeys
的长度不为3,那么就说明选择不合法,就需要重置选项。
实现代码如下所示:
// 将一级二级菜单选项设置为不可选
if (this.selectedKeys.length !== 3) {
this.$message.warning('注意:只允许为第三级分类设置相关参数!')
this.selectedKeys = []
}
3.Tabs标签页
在标签页中的添加参数和添加属性两个按钮会根据级联菜单中的选项是否被选择而变化(按钮可用,按钮不可用)。
<el-button type="primary" size="medium" :disabled="isBtnDisable">添加参数</el-button>
...
computed: {
// 如果按钮需要被禁用,返回true;否则就返回false
isBtnDisable() {
if (this.selectedKeys.length !== 3) {
return true
}
return false
}
}
4.参数数据获取
// 监听级联菜单选择项改变事件
cateChanged() {
console.log(this.selectedKeys)
// 将一级二级菜单选项设置为不可选
if (this.selectedKeys.length !== 3) {
this.$message.warning('注意:只允许为第三级分类设置相关参数!')
this.selectedKeys = []
}
// 已经正确地选择了三级分类
// 在级联菜单选项改变时请求数据
// 根据所选分类的id和所处面板获取对应的数据
this.getParamsData()
},
// Tab标签点击事件地处理函数
handleTabClick() {
console.log(this.activeName)
// 在动态参数和静态属性面板之间切换是也要动态调用方法获取数据
this.getParamsData()
},
// 获取参数列表数据
async getParamsData() {
const { data: res } = await this.$http.get(`categories/${this.cateId}/attributes`, { params: { sel: this.activeName } })
if (res.meta.status !== 200) return this.$message.error('获取参数/属性失败!')
console.log(res)
// 在获取到数据后要进行判断然后再渲染到对应的面板中去
// 如果获取到的数据 属于 动态参数
if (this.activeName === 'many') {
this.manyTableData = res.data
} else {
// 如果获取到的数据 属于 静态属性
this.onlyTableData = res.data
}
}
5.数据渲染
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="动态参数" name="many">
<el-button type="primary" size="medium" :disabled="isBtnDisable">添加参数</el-button>
<!-- 动态参数数据表格 -->
<el-table :data="manyTableData" border stripe :cell-style="rowClass" :header-cell-style="headClass">
<el-table-column type="expand"></el-table-column>
<el-table-column type="index" label="#"></el-table-column>
<el-table-column prop="attr_name" label="参数名称"></el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="primary" icon="el-icon-edit" size="mini">修改</el-button>
<el-button type="danger" icon="el-icon-delete" size="mini">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="静态属性" name="only">
<el-button type="primary" size="medium" :disabled="isBtnDisable">添加属性</el-button>
<!-- 静态属性数据表格 -->
<el-table :data="onlyTableData" border stripe :cell-style="rowClass" :header-cell-style="headClass">
<el-table-column type="expand"></el-table-column>
<el-table-column type="index" label="#"></el-table-column>
<el-table-column prop="attr_name" label="属性名称"></el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="primary" icon="el-icon-edit" size="mini">修改</el-button>
<el-button type="danger" icon="el-icon-delete" size="mini">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
6.添加动态参数/静态属性
添加动态参数/静态属性时的对话框仅仅是提示文字不同,样式基本相同因此只需要共用一个即可。
<el-dialog
:title="'添加' + dialogTitle"
:visible.sync="addParamsdialogVisible"
width="50%"
@close="addParamsdialogClosed">
<el-form :model="addParamsForm" :rules="addParamsFormrules" ref="addParamsFormRef" label-width="7px">
<el-form-item :label="dialogTitle + '名称'" prop="attr_name">
<el-input v-model="addParamsForm.attr_name"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addParamsdialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addParam">确 定</el-button>
</span>
</el-dialog>
...
// 增加动态参数/静态属性
addParam() {
console.log(this.addParamsForm)
this.$refs.addParamsFormRef.validate(async valid => {
if (!valid) return
// 可以发起添加请求
const { data: res } = await this.$http.post(`categories/${this.cateId}/attributes`, { attr_sel: this.activeName, attr_name: this.addParamsForm.attr_name })
console.log(res)
if (res.meta.status !== 201) {
this.$message.error('添加' + this.dialogTitle + '失败')
}
this.$message.success('添加' + this.dialogTitle + '成功!')
// 添加成功之后关闭对话框
this.adddialogVisible = !this.adddialogVisible
// 添加成功之后重新获取用户信息列表
this.getParamsData()
})
this.addParamsdialogVisible = !this.addParamsdialogVisible
},
// 监听添加动态参数/静态属性的对话框关闭事件
addParamsdialogClosed() {
this.$refs.addParamsFormRef.resetFields()
},
...
computed: {
// 获取当前添加动态参数还是静态属性
dialogTitle() {
if (this.activeName === 'many') {
return '动态参数'
} else {
return '静态属性'
}
}
}
7.编辑动态参数/静态属性
// 显示编辑动态参数/静态属性的对话框
async showeditParamsdialog(attrId) {
console.log(attrId)
// 根据角色id查询该角色
const { data: res } = await this.$http.get(`categories/${this.cateId}/attributes/` + attrId)
if (res.meta.status !== 200) return this.$message.error('拉取参数信息失败')
this.editParamsForm = res.data
this.editParamsdialogVisible = !this.editParamsdialogVisible
},
// 编辑动态参数/静态属性
editParam() {
this.$refs.editParamsFormRef.validate(async valid => {
if (!valid) return
// 可以发起添加请求
const { data: res } = await this.$http.put(`categories/${this.cateId}/attributes/${this.editParamsForm.attr_id}`, {
attr_name: this.editParamsForm.attr_name,
attr_sel: this.activeName
})
if (res.meta.status !== 200) {
this.$message.error('更新参数信息失败')
}
// 更新成功之后关闭对话框
this.editParamsdialogVisible = !this.editParamsdialogVisible
// 更新成功之后重新获取用户信息列表
this.getParamsData()
// 修改成功后的提示信息
this.$message.success('更新参数信息成功!')
})
},
// 监听编辑动态参数/静态属性的对话框关闭事件
editParamsdialogClosed() {
this.$refs.editParamsFormRef.resetFields()
}
8.删除动态参数/静态属性
async deleteParamsById(attrId) {
const confirmResult = await this.$confirm('确定将此' + this.dialogTitle + '删除吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(err => err)
console.log(confirmResult)
if (confirmResult !== 'confirm') {
return this.$message.info('已取消删除')
}
const { data: res } = await this.$http.delete(`categories/${this.cateId}/attributes/${attrId}`)
console.log(res)
if (res.meta.status !== 200) return this.$message.error('删除' + this.dialogTitle + '失败')
this.$message.success('删除' + this.dialogTitle + '成功')
// 重新拉取用户信息列表
this.getParamsData()
}
9.渲染动态参数下的可选项
<template slot-scope="scope">
<el-tag v-for="(item, index) in scope.row.attr_vals" :key="index" closable>{{ item }}</el-tag>
</template>
10.增加动态参数下的可选项
async handleInputConfirm(row) {
// 如果在输入时误打了空格
if (row.inputValue.trim().length === 0) {
row.inputValue = ''
row.inputVisible = !row.inputVisible
} else {
// 如果没有return说明用户输入了有效内容,此时
// 当文本框失去焦点时要将文本框重新变为按钮并且将内容生成Tag标签再清除
row.attr_vals.push(row.inputValue.trim())
row.inputValue = ''
row.inputVisible = !row.inputVisible
// 需要发起请求来保存这一次操作
const { data: res } = await this.$http.put(`categories/${this.cateId}/attributes/${row.attr_id}`, {
attr_name: row.attr_name,
attr_sel: row.attr_sel,
attr_vals: row.attr_vals.join(' ')
})
console.log(res)
if (res.meta.status !== 200) return this.$message.error('更新' + this.dialogTitle + '失败')
this.$message.success('更新' + this.dialogTitle + '成功')
}
}
在这里实际开发的话应该是先要将数据更新到数据库,如果发送的请求返回正确校验码再更新页面上的数据。如下代码:
const { data: res } = await this.$http.put(`categories/${this.cateId}/attributes/${row.attr_id}`, {
attr_name: row.inputValue.trim(),
attr_sel: row.attr_sel
})
console.log(res)
if (res.meta.status !== 200) return this.$message.error('更新' + this.dialogTitle + '失败')
this.$message.success('更新' + this.dialogTitle + '成功')
row.attr_vals.push(res.data.attr_name)
row.inputValue = ''
row.inputVisible = !row.inputVisible
11.删除动态参数下的可选项
// 动态参数下的可选项标签的点击删除事件
async handleClose(index, row) {
row.attr_vals.splice(index, 1)
const { data: res } = await this.$http.put(`categories/${this.cateId}/attributes/${row.attr_id}`, {
attr_name: row.attr_name,
attr_sel: row.attr_sel,
attr_vals: row.attr_vals.join(' ')
})
console.log(res)
if (res.meta.status !== 200) return this.$message.error('删除失败')
this.$message.success('删除成功')
},
12.功能优化
因为在级联菜单中进行选择时,是不允许选择非三级菜单的,但是实际如果选择了二级菜单,级联选择器的内容会被清空(没问题),添加按钮会被禁用(没问题),但是数据表格仍然会被渲染出来。因此需要做一下清除:
this.manyTableData = []
this.onlyTableData = []
13.上传代码
4.商品管理-商品列表
1.创建新分支并推送到码云
git checkout -b goods_list
git push -u origin goods_list
2.页面布局和数据渲染
页面布局和数据渲染同上面商品分类以及分类参数基本相同。
3.自定义格式化时间的全局过滤器
在商品列表中的创建时间这一列中,数据是以时间戳(单位为毫秒)的,因此可以在全局自定义一个格式化时间的过滤器。
main.js
Vue.filter('dateFormat', function(originVal) {
const dt = new Date(originVal)
const year = dt.getFullYear()
const month = (dt.getMonth() + 1 + '').padStart(2, '0')
const day = (dt.getDate() + '').padStart(2, '0')
const hour = (dt.getHours() + '').padStart(2, '0')
const minute = (dt.getMinutes() + '').padStart(2, '0')
const second = (dt.getSeconds() + '').padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
})
在页面中插入模板字符串:
<el-table-column prop="add_time" label="创建时间" width="140px">
<template slot-scope="scope">
{{scope.row.add_time | dateFormat}}
</template>
</el-table-column>
4.添加商品
添加商品功能模块和之前的不一样,不再是弹窗式的对话框了(因为商品信息比较多),而是跳转到另一个页面。
5.添加富文本编辑器
在vue项目中的富文本编辑器叫做vue-quill-editor
。配置过程如下:
1.首先在vue项目可视化面板中选择依赖-安装新依赖-运行依赖并搜索vue-quill-editor
然后安装。
2.接着进行全局注册,然后在页面上即可使用。具体步骤在vue-quill-editor使用文档说明。
在入口文件main.js
中导入:
// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// 导入富文本编辑器对应的样式
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
Vue.use(VueQuillEditor)
在页面上对应的组件代码为:
<!-- 富文本编辑器区域 -->
<quill-editor
ref="myQuillEditor"
// 内容的绑定
v-model="addGoodsForm.goods_introduce"
:options="editorOption"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@ready="onEditorReady($event)"
>
</quill-editor>
注意,如果富文本编辑器的文本输入区域高度不合适,可以自行指定。具体实现如下,注意类名ql-editor:
.ql-editor {
// 设置
min-height: 300px;
}
6.goods_cat类型问题
在级联选择器中,要求绑定的数据类型是数组,但是在发送添加商品的请求的时候又要求类型是字符串。因此两者存在矛盾,也就是说当在发送添加商品的请求前对goods_cat进行数据类型转换的同时,因为双向绑定所以会导致级联选择器的报错。
解决办法:深度拷贝。也就是说在发送请求时,转换数据类型不再在原来的数据对象上转换,而是拷贝一个一模一样的新对象进行类型转换。
首先在vue可视化面板中添加一个运行依赖:lodash
。
接着在vue中导入并使用了:
import _ from 'lodash'
...
const form = _.cloneDeep(this.addGoodsForm)
form.goods_cat = form.goods_cat.join(',')
7.attrs属性的处理
addGoods() {
// 查看富文本编辑器是否与对应属性值绑定
console.log(this.addGoodsForm.goods_introduce)
this.$refs.addGoodsFormRef.validate(async valid => {
if (!valid) {
return this.$message.error('请填写必要的表单项')
}
// 执行添加的逻辑操作
// 执行添加前需要将goods_cat从数组转换为字符串(接口要求)
// 深度拷贝 lodash cloneDeep(Obj)
const form = _.cloneDeep(this.addGoodsForm)
form.goods_cat = form.goods_cat.join(',')
// 处理动态参数
this.manyTableData.forEach(item => {
const newInfo = { attr_id: item.attr_id, attr_value: item.attr_vals.join(' ') }
this.addGoodsForm.attrs.push(newInfo)
})
// 处理静态属性
this.onlyTableData.forEach(item => {
const newInfo = { attr_id: item.attr_id, attr_value: item.attr_vals }
this.addGoodsForm.attrs.push(newInfo)
})
form.attrs = this.addGoodsForm.attrs
console.log(form)
// 发起添加商品的请求
const { data: res } = await this.$http.post('goods', form)
console.log(res)
if (res.meta.status !== 201) return this.$message.error('添加商品失败!')
this.$message.success('添加商品成功!')
// 跳转到商品列表页面
this.$router.push('/goods')
})
}