vue后台管理系统——商品管理模块

电商后台管理系统的功能——商品管理模块

商品管理概述

商品管理模块用于维护电商平台的商品信息,包括商品的类型、参数、图片、详情等信息。 通过商品管理模块可以实现商品的添加、修改、展示和删除等功能。  

1. 商品列表

在goods文件夹下创建List.vue组件

1.1 商品列表功能

  • 实现商品列表布局效果
  • 调用后台接口获取商品列表数据
const { data: res } = await this.$http.get('goods', { params: this.queryInfo })
if (res.meta.status !== 200) {
    return this.$message.error('初始化商品列表失败! ')
}
// 为商品列表赋值
this.goodsList = res.data.goods
// 为总数量赋值
this.total = res.data.total

获取到的数据中包含时间,但是所获取的时间不是我们想要的格式:

因此我们需要实现时间过滤器功能

在main.js入口文件中创建一个全局过滤器:所有的组件都可以使用

1.2 添加商品

点击添加商品按钮跳转到添加商品的页面

① 基本布局与分布条效果

需要使用step步骤条组件

  • 添加商品基本布局
  • 分布条组件用法  
<el-steps :active="activeName-0" finish-status="success" align-center>
    <el-step title="基本信息"></el-step>
    <el-step title="商品参数"></el-step>
    <el-step title="商品属性"></el-step>
    <el-step title="商品图片"></el-step>
    <el-step title="商品内容"></el-step>
    <el-step title="完成"></el-step>
</el-steps>

② 商品信息选项卡Tab布局效果

Tab 组件的基本使用 

<el-tabs tab-position="left" v-model="activeName" :before-leave="beforeTabLeave">
    <el-tab-pane label="基本信息" name="0"><!-- 基本信息面板 --></el-tab-pane>
    <el-tab-pane label="商品参数" name="1"><!-- 商品参数面板 --></el-tab-pane>
    <el-tab-pane label="商品属性" name="2"><!-- 商品静态属性面板 --></el-tab-pane>
    <el-tab-pane label="商品图片" name="3"><!-- 图片上传面板 --></el-tab-pane>
    <el-tab-pane label="商品内容" name="4"><!-- 商品描述面板 --></el-tab-pane>
</el-tabs>

实现步骤条与tab栏标签的数据联动:对于步骤条来说,如果想要修改激活项的话,必须找到步骤条的active属性,修改索引值;对于tab栏标签,有一个v-model属性,表示当前激活的面板,如果点击的是第一个标签栏,则将第一个面板的name属性值赋值给v-model。因此,只需要将tab标签栏的v-model属性绑定的值与步骤条的active属性绑定同样的值,就可以实现数据联动。

tab栏绑定的值是字符串类型的,但是步骤条中的active属性绑定的是数值类型的,因此需要做一个转换,将activeIndex转成数值类型

如果要将一个长得像数字一样的字符串转换为数值类型,只需要-0即可。

阻止标签页之间的切换:首先要监听标签页的切换事件,在事件处理函数中判断是否处于第一个页签,还要判断选中的商品分类是否为三级分类。实际上就是判断数组长度是否为3,如果不等于3,则阻止跳转

③ 商品基本信息

我们将一个商品信息拆分成了五个panel面板,每个面板里面只维护着当前商品的部分数据,只有将所有面板的数据合并起来才是一个完整的商品数据,因此应该在五个panel面板的外面嵌套一层form表单。

  • 商品基本信息表单布局
  • 表单数据绑定
  • 表单验证
addFormRules: {
    goods_name: [{ required: true, message: '请填写商品名称', trigger: 'blur' }],
    goods_price: [{ required: true, message: '请填写商品价格', trigger: 'blur' }],
    goods_weight: [{ required: true, message: '请填写商品重量', trigger: 'blur' }],
    goods_number: [{ required: true, message: '请填写商品数量', trigger: 'blur' }],
    goods_cat: [{ required: true, message: '请选择商品分类', trigger: 'blur' }]
}

使标签与文本框不在同一行:

④ 商品分类信息

  • 商品分类布局
  • 商品分类数据加载
<el-cascader expand-trigger="hover" :options="cateList" :props="cascaderConfig"
  v-model="addForm.goods_cat" @change="handleCascaderChange"></el-cascader>
const { data: res } = await this.$http.get('categories')
if (res.meta.status !== 200) {
    return this.$message.error('初始化商品分类失败! ')
}
this.cateList = res.data

⑤ 商品动态参数

  • 获取商品动态参数数据
  • 商品动态参数布局  
const { data: res } = await this.$http.get(`categories/${this.cateId}/attributes`, {
    params: { sel: 'many' }
})
if (res.meta.status !== 200) return this.$message.error('获取动态参数列表失败! ')
// 把动态参数中的每一项数据中的 attr_vals,都从字符串分割为数组
res.data.forEach(item => {
    item.attr_vals = item.attr_vals.length === 0 ? [] : item.attr_vals.split(' ')
})
this.manyData = res.data

使用到复选框组件:

⑥ 商品静态属性

  • 获取商品静态属性数据
  • 商品静态属性布局  
const { data: res } = await this.$http.get(`categories/${this.cateId}/attributes`, {
    params: { sel: 'only' }
})
if (res.meta.status !== 200) {
    return this.$message.error('获取动态参数列表失败! ')
}
this.onlyData = res.data

⑦ 商品图片上传

图片上传upload组件基本使用  

<el-upload
    action="http://47.96.21.88:8888/api/private/v1/upload"
    :headers="uploadHeaders"
    :on-preview="handlePreview"
    :on-remove="handleRemove"
    :on-success="handleSuccess"
    list-type="picture">
    <el-button size="small" type="primary">点击上传</el-button>
</el-upload>

注意,action不能直接写action="upload",这样的话表示图片要上传的是http://localhost:8080/#/goods/add/upload

我们后台API接口不是8080,http://47.96.21.88:8888/api/private/v1/才是后台服务器的请求路径

点击上传图片按钮,可以成功预览图片,但是实际上并没有上传成功这是因为除了登录接口之外的所有接口都是有权限的,在调用这些接口的时候提供一个token值,我们在main.js入口文件中添加了一个axios拦截器,为每一个请求挂载了一个Authorization字段,字段的值就是token。但是在调用upload组件上传图片的时候并没有用到axios拦截器,这个组件内部自己封装了一套Ajax,是没有携带Authorization字段的

解决方法如下:

图片预览 

实现图片的预览效果:图片预览实际上就是弹出一个对话框,只不过没有取消和确定按钮而已

// 预览图片时候,触发的方法
handlePreview(result) {
    this.previewImgSrc = result.response.data.url
    this.previewVisible = true
}

图片删除  

// 当移除图片,会触发这个方法
handleRemove(result) {
    // 根据 result.response.data.temp_path 从 addForm.pics 数组中,找到要删除那个对象的索引值
    const index = this.addForm.pics.findIndex(item => item.pic ===
    result.response.data.tmp_path)
    // 根据索引删除对应的图片信息对象
    this.addForm.pics.splice(index, 1)
}

完成图片上传 

// 图片上传成功
handleSuccess(result) {
    if (result.meta.status === 200) {
        // 把上传成功后,图片的临时路径,保存到 addForm.pics 数组中,作为对象来保存
        this.addForm.pics.push({
            pic: result.data.tmp_path
        })
    }
}

⑧ 商品详情

富文本编辑器基本使用

// 安装vue-quill-editor
npm install vue-quill-editor -S

或者使用vue ui图形化界面安装:

import VueQuillEditor from 'vue-quill-editor‘
Vue.use(VueQuillEditor)

<quill-editor v-model="addForm.goods_introduce"></quill-editor>

⑨ 完成商品添加

  • 处理商品相关数据格式
  • 调用接口完成商品添加
// 先处理好商品相关的数据格式,然后再提交
const newForm = _.cloneDeep(this.addForm)
newForm.goods_cat = newForm.goods_cat.join(',')
// 到此位置,商品相关数据已经准备好,可以提交了
const { data: res } = await this.$http.post('goods', newForm)
if (res.meta.status !== 201) return this.$message.error(res.meta.msg)
this.$message.success('添加商品成功! ')
// 跳转到商品列表页
this.$router.push('/goods/list')

实现商品内容的添加,在添加之前要对整个表单进行数据的校验,检验表单的数据是否合法

上面的一行代码执行后会报错,这是因为在基本信息面板中的级联选择器必须双向绑定要一个数组上,但是在提交商品信息时,执行add()函数后,就将goods_cat转换为了字符串,这样就会报错

如何解决呢?可以在拼接操作goods_cat之前将addForm对象做一下深拷贝(将这个对象原封不动的拷贝一份出来,两个对象之间互不相干)

可以使用第三方模块lodash的cloneDeep(obj)方法:官网:https://www.lodashjs.com/

 

有两个bug还未解决:

① 当不选择商品价格、商品重量和商品数量时,后台服务器会报错,显示必须输入这些值,但实际上这些属性是有一个默认值的0,为什么会显示为空数据呢?

② 添加商品名称不能一致,但是删除了该商品之后,再添加一个与删除商品名称一致的商品却不行,这是为什么呢?后台数据库中没有删数据吗?知道原因了,sp_goods_pics这个表中已经删除该商品,但是在sp_goods表中却仍然保留着该商品的id值,是后台数据接口的问题。

1.3 提交商品列表功能到码云

  • 使用git checkout -b goods_list创建一个新分支并切换到该分支上
  • 使用git branch查看当前所处的分支,所有代码的修改也一起被切换到了goods_list子分支中
  • 使用git status命令检查当前分支的代码状态
  • 使用git add .命令统一增加到暂存区
  • 使用git commit -m "完成商品列表功能的开发"命令把goods_list分支提交到本地仓库中

本地goods_list的代码就是最新的了

使用git push -u origin goods_list命令把本地的goods_list分支推送到云端中

master是主分支,但是它的代码还是旧的,应该立即把所有的代码合并到主分支上

  • 使用git checkout master命令切换到主分支
  • 使用git merge goods_list命令从主分支上把goods_list分支上的代码合并过来
  • 使用git push命令将本地的master分支的代码推送到云端,这样master分支上的代码也是最新的了

2. 分类管理

在components目录下创建goods文件夹,然后创建cate.vue组件

2.1 商品分类概述

商品分类用于在购物时,快速找到所要购买的商品,可以通过电商平台主页直观的看到。  

2.2 商品分类列表

基本布局与数据获取 

  • 实现基本布局
  • 实现分类列表数据加载

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
this.total = res.data.total

2.3 树形表格

将商品分类的数据渲染为树形表格,但elementUI中并没有提供这样的组件,我们需要依赖第三方的插件来实现树形的table表格:

① 第三方树形表格的基本使用

npm i vue-table-with-tree-grid -S

或者使用vue ui图形化界面来安装依赖包:

  • 基本使用

我们使用手动注册的方式:在main.js入口文件中导入,ZkTable是要注册的插件,ZkTable.name是注册的名称

import Vue from 'vue'
import ZkTable from 'vue-table-with-tree-grid'
Vue.use(ZkTable)

② 实现分类树形列表

  • 实现树形列表布局并进行数据填充
  • 自定义表格列
<tree-table :data="cateList" :columns="columns" border :selection-type="false"
:expand-type="false" show-index index-text="#" class="tree-table">
    <!-- 操作的模板列 -->
    <!-- 排序的模板列 -->
    <!-- 是否有效的模板列 -->
    <template slot="isok" slot-scope="scope">
        <i class="el-icon-success" style="color:#20B2AA;" v-if="scope.row.cat_deleted
=== false"></i>
        <i class="el-icon-error" style="color:#F92672;" v-else></i>
    </template>
</tree-table>

data属性是树形表格的数据源,column是每列的信息,是个数组

隐藏掉树形表格前面的复选框和展开行的效果

需要在前面加一个索引列:

自定义索引列的标题:

设置表格列与列之间的边框:

实现'是否有效'这一列:需要用到作用域插槽

先将这一列用type指定为template,再使用template属性来指定具体的作用域插槽。在表格的中间用一个template来定义这个插槽,然后使用scope属性取个名字来与column中的template名称对应,并且通过slot-scope属性来接收这一行的数据

2.4 分页功能

  • 实现分页组件效果
  • 分页组件数据处理
<!-- 分页区域 -->
<el-pagination
  @current-change="handleCurrentChange"
  :current-page="queryInfo.pagenum"
  :page-size="queryInfo.pagesize"
  layout="total, prev, pager, next, jumper"
  :total="total">
</el-pagination>

2.5 添加分类

① 实现分类树形列表

  • 实现添加分类对话框布局
  • 控制对话框显示和隐藏

实现添加分类按钮的功能:

父级分类是没有验证规则的,如果为空,则是将当前的分类名称添加为一级分类,如果选择了一个(大家电),则是二级分类(大家电的子分类),如果选择了两个,则是三级分类。只需要获取前两级的分类,即请求的type设为2,不需要pagenum和pagesize,一次性的将前两级分类获取过来

<el-dialog title="添加分类" :visible.sync="addDialogVisible" width="50%" @close="resetForm">
    <el-form :model="addForm" :rules="addFormRules" ref="addFormRef" label-width="100px">
        <el-form-item label="分类名称: " prop="cat_name">
            <el-input v-model="addForm.cat_name"></el-input>
        </el-form-item>
        <el-form-item label="父级分类: ">
            <!-- 分类菜单 -->
        </el-form-item>
    </el-form>
</el-dialog>

② 实现分类级联菜单效果

  • 实现级联菜单效果
  • 级联菜单数据加载与填充

级联选择器Cascader::options指的是级联选择器的数据源,props属性是级联选择器的配置选项,你选中的值、展示的值、以及通过哪个属性来实现父子嵌套都要通过props预先进行配置

<el-cascader
  expand-trigger="hover"
  :options="parentCateList"
  :props="cascaderConfig"
  v-model="selectedCateList"
  @change="handleChange"
  change-on-select
  clearable>
</el-cascader>
// 先获取所有父级分类的数据列表
const { data: res } = await this.$http.get('categories', { params: { type: 2 } })
if (res.meta.status !== 200) return this.$message.error('获取父级分类失败! ')
// 把父级分类数据,挂载到data中
this.parentCateList = res.data

解决el-cascader级联选择器供选择的列表太长的问题

③ 控制父级分类的选择

父级分类选择时,获取对应的分类 id。

handleChange() {
    if (this.selectedCateList.length === 0) {
        // 证明没有选中任何父级分类
        this.addForm.cat_pid = 0
        this.addForm.cat_level = 0
    } else {
        // 选中父级分类
        this.addForm.cat_pid = this.selectedCateList[this.selectedCateList.length - 1]
        // 设置分类等级
        this.addForm.cat_level = this.selectedCateList.length
    }
}

④ 完成分类添加

将分类名称、分类等级和父分类 id 提交到后台,完成分类添加。

const { data: res } = await this.$http.post('categories', this.addForm)
if (res.meta.status !== 201) {
    return this.$message.error('添加分类失败! ')
}
this.$message.success('添加分类成功! ')

 

2.6 提交分类管理功能到码云

  • 使用git checkout -b goods_cate创建一个新分支并切换到该分支上
  • 使用git branch查看当前所处的分支,所有代码的修改也一起被切换到了goods_cate子分支中
  • 使用git status命令检查当前分支的代码状态
  • 使用git add .命令统一增加到暂存区
  • 使用git commit -m "完成分类管理功能的开发"命令把goods_cate分支提交到本地仓库中

本地goods_cate的代码就是最新的了

使用git push -u origin goods_cate命令把本地的goods_cate分支推送到云端中

master是主分支,但是它的代码还是旧的,应该立即把所有的代码合并到主分支上

  • 使用git checkout master命令切换到主分支
  • 使用git merge goods_cate命令从主分支上把goods_cate分支上的代码合并过来
  • 使用git push命令将本地的master分支的代码推送到云端,这样master分支上的代码也是最新的了

3. 参数管理

在goods文件夹下创建Params.vue组件

3.1 参数管理概述

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

动态参数: 

静态属性:  

3.2 商品分类选择

① 选择商品分类

  • 页面基本布局
  • 加载商品分类数据
  • 实现商品分类的级联选择效果  

// 获取所有商品的分类列表
async getAllCateList() {
    const { data: res } = await this.$http.get('categories')
    if (res.meta.status !== 200) {
        return this.$message.error('获取商品分类列表失败! ')
    }
    this.cateList = res.data
}

② 控制级联菜单分类选择

  • 只允许选择三级分类
  • 通过计算属性的方式获取分类 ID  

级联选择器默认只能选择选择框的最后一项,但是我们只允许选择三级分类,如果只有二级分类的选项,其实是可以选中二级分类的。那如何解决呢?

我们只需要监听选择器的change事件,在change事件中只需要判断选中项的长度,如果长度不等于3,则说明选择的不是三级分类,只需要将v-model绑定的数组(当前选中的值)清空即可,对应的选择器中的选择项也会被清空。

cascaderChanged() {
    if (this.selectedCateList.length !== 3) {
        // 没有选中三级分类,把分类重置为空
        this.selectedCateList = []
        this.manyTableData = []
        this.onlyTableData = []
    } else {
        // 选中了三级分类后,获取该分类对应的参数列表数据
        this.getParamsList()
    }
}
cateId() {
    if (this.selectedCateList.length === 3) {
        return this.selectedCateList[this.selectedCateList.length - 1]
    } else {
        return null
    }
}

如果在级联框中选中了一项,则启用tab标签栏,否则,tab标签栏处于禁用状态

如何实现呢?定义一个计算属性,根据级联选择数组的长度返回一个bool值,从而控制按钮的启用和禁用

3.3 实现参数列表

① 根据选择的商品分类加载对应的参数数据

  • 参数列表布局
  • 根据分类 id 加载参数列表数据  

// 获取所有商品的分类列表
const { data: res } = await this.$http.get(`categories/${this.cateId}/attributes`, {
    params: { sel: this.activeName }
})

级联选择框发生变化时需要获取参数值,tab标签栏发生点击切换时,也需要获取参数值,所以需要提取出来单独封装为一个函数

添加参数和添加属性共用一个弹出对话框:也通过计算属性绑定

② 处理标签数据格式

将字符串形式的数据分隔为数组。 

res.data.forEach(item => {
    // 把字符串的可选项,分割为数组,重新赋值给 attr_vals
    item.attr_vals = item.attr_vals.length > 0 ? item.attr_vals.split(‘, ') : []
})

实现展开列里面的数据渲染:

在getParamsData()函数中赋值之前添加下面这一段代码

出现一个bug:当我们新增一个参数后,展开列中应该没有内容,但是却有一个空的标签:

这是因为,如果attr_vals为空,则用split按空格分割之后得到的数组是包含一项的,就是空字符串,所以就渲染出了一个空白的el-tag

如何解决呢?过滤掉空字符串

③ 控制添加标签文本框的显示

$nextTick 的执行时机:DOM 更新完毕之后

<el-button size="small" v-else @click="showTagInput(scope.row)">+ New Tag</el-button>
showTagInput(row) {
    row.tagInputVisible = true
    // 当我们修改了 data 中 tagInputVisible 的值以后,如果要操作文本框,必须等页面重新渲染完毕之后才可以,
    // 所以,必须把操作文本框的代码放到 $nextTick 中,当作回调去执行( $nextTick 的执行时机,是在DOM 更新完毕之后)
    this.$nextTick(() => {
        this.$refs.saveTagInput.$refs.input.focus()
    })
}

实现添加按钮与输入文本框之间的切换显示:使用v-if和v-else实现

出现一个bug:展开两项,然后点击其中一项的输入new tag按钮,但是两项都变成了输入文本框,并且输入其中一个文本框,另一个会有联动的效果

这是因为所有的input绑定的都是同一个if判断条件,数据双向绑定的也是同一个值。每渲染一个展开行,都公用同一个value值和bool值

 

如何解决呢?只需要给每一行的数据单独提供个value值和bool值

把inputVisible设置为true之后,页面上的元素还没有被重新渲染,也就是说页面上展示的还是button按钮,并不是文本输入框。所以要等到渲染完成之后,才去执行。

优化:当文本框中输入的内容后,失去焦点之后,再次点击按钮显示文本框,之前输入文本框中的内容并没有被清空。但是我们想要清除不合法的输入内容,比如说一串空格

出现一个bug:当我们选中一个三级分类之后,下面会显示相应的参数,然后当我们再切换选择为一个二级分类,但是不允许我们选择,所以级联选择框会自动被清空,但是下面的参数却还在。这样如果我们添加新tag时,会因为没有商品的id值而报错

解决方法:

④ 实现标签动态添加的文本框控制逻辑

  • 控制标签输入域的显示和隐藏
  • 对输入的内容进行数据绑定
res.data.forEach(item => {
    // 把字符串的可选项,分割为数组,重新赋值给 attr_vals
    item.attr_vals = item.attr_vals.length > 0 ? item.attr_vals.split(‘, ') : []
    // 为每个数据行,添加自己的 tagInputVisible ,从而控制自己展开行中的输入框的显示与隐藏
    item.tagInputVisible = false
    // 把文本框中输入的值,双向绑定到 item.tagInputValue 上
    item.tagInputValue = ''
})

⑤ 实现标签的添加和删除操作

添加标签和删除标签使用的是同一个接口,参数是一样的。

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(' ')
    }
)
if (res.meta.status !== 200) {
    return this.$message.error('更新参数项失败! ')
}
this.$message.success('更新参数项成功! ')

3.4 实现动态参数与静态属性添加

  • 动态参数与静态属性表单重用
  • 添加动态参数与静态属性使用的是同一个接口,参数是一样的
const { data: res } = await this.$http.post(`categories/${this.cateId}/attributes`, {
    // 参数的名称
    attr_name: this.addForm.attr_name,
    // 参数类型 many only
    attr_sel: this.activeName
})
if (res.meta.status !== 201) return this.$message.error('添加参数失败! ')
this.$message.success('添加参数成功! ')

3.5 提交分类参数功能到码云

  • 使用git checkout -b goods_params创建一个新分支并切换到该分支上
  • 使用git branch查看当前所处的分支,所有代码的修改也一起被切换到了goods_params子分支中
  • 使用git status命令检查当前分支的代码状态
  • 使用git add .命令统一增加到暂存区
  • 使用git commit -m "完成分类参数功能的开发"命令把goods_params分支提交到本地仓库中

本地goods_params的代码就是最新的了

使用git push -u origin goods_params命令把本地的goods_params分支推送到云端中

master是主分支,但是它的代码还是旧的,应该立即把所有的代码合并到主分支上

  • 使用git checkout master命令切换到主分支
  • 使用git merge goods_params命令从主分支上把goods_params分支上的代码合并过来
  • 使用git push命令将本地的master分支的代码推送到云端,这样master分支上的代码也是最新的了

代码地址:https://github.com/Emily-zcy/Backstage-Management-System-Based-on-vue

posted @ 2020-08-07 22:34  浮华夕颜  Views(5879)  Comments(0Edit  收藏  举报