猿实战10——动态表单之实现类目属性绑定

时间:2022-07-24
本文章向大家介绍猿实战10——动态表单之实现类目属性绑定,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

猿实战是一个原创系列文章,通过实战的方式,采用前后端分离的技术结合SpringMVC Spring Mybatis,手把手教你撸一个完整的电商系统,跟着教程走下来,变身猿人找到工作不是问题。想要一起实战吗?关注公号即可获取基础代码!

上一个章节,猿人君教会了你实现了后台类目,今天开始我们来讲述,类目和属性之间的关系绑定。

为什么需要这种绑定关系

大家都知道,商品是有类别的,而类别之所以能够分门别类,是因为这些类别,本身就具备一些特性,而这些特性就是我们之前提到过的商品属性。

关于这个话题还不太理解的朋友们,建议您回过头去看看之前的文章,搞明白之后,相信可以解决您的疑惑。

功能概览

在之前的文章中,我们其实已经分析过类目和属性关系的设计了,简而言之,属性描述着类目的特性,而类目在将来,会把这些特性赋予具体的商品。

有的朋友可能比较着急,像快速的去知道这是怎样的一个链路,不过胖子认为,要搞清这样一个链路,还是先做一些比较实际的工作为好。量变之所能够质变,是量让你学会思考,产生了质的东西,才是属于你自己的。

我们先看看这一块儿的整体功能概览。

在类目与属性的关系中,在整体功能上,可以分为类目属性列表、类目属性值列表,在列表页面都提供了新增编辑功能。类目的选择,支持多级联动,新增/编辑属性或者属性值,支持输入检索,勾选检索内容,实现快速新增属性/或者属性值的功能。检索支持模糊查询,而且需要同时支持检索属性/属性组,属性值/属性值组。

数据库设计

根据之前的后台类目设计文章,我们很清楚的获知了后台类目的一些特征,我们根据之前的设计猿设计5——真电商之颠覆你的类目认知猿设计6——真电商之属性的套路你了解吗将这些设计落地为数据库的物理设计,大家可以看一下。

就类目属性的整体设计而言,类目属性更加明确了属性的类型,我们之前的属性库,作为基础数据,为类目属性提供数据来源。类目的特性,在绑定类目属性关系时来确定某一类属性的特性。

类目属性整体前端

类目属性的绑定,从功能上讲,依然是一种整体和部分的关系,属性值列表的展示依赖于,类目属性列表的选择,而属性/属性值的新增和编辑功能,则依赖着各自的组件。最后,由一个view来组织和整合它们就好了。

<template>
  <div id="attributeLibraryDiv">
    <div v-if="backgroundCategory">
      <el-card shadow="never">
        <div>
          <el-form ref="listQuery" :model="listQuery" :inline="true">
            <el-form-item label="所属类目:" style="width:46%;" prop="stair">
              <el-select v-model="listQuery.fristCategoryId" clearable filterable allow-create placeholder="请选择" style="width:30%;" @change="selectCatOne">
                <el-option
                  v-for="item in fristList"
                  :key="item.categoryId + '^-^'"
                  :label="item.categoryName"
                  :value="item.categoryId"
                />
              </el-select>
              <el-select v-model="listQuery.secondCategoryId" clearable filterable allow-create placeholder="请选择" style="width:30%;" @change="selectCatTwo">
                <el-option
                  v-for="item in secondList"
                  :key="item.categoryId + '^-^'"
                  :label="item.categoryName"
                  :value="item.categoryId"
                />
              </el-select>
              <el-select v-model="listQuery.thridCategoryId" clearable filterable allow-create placeholder="请选择" style="width:30%;" @change="selectCatThree">
                <el-option
                  v-for="item in thirdList"
                  :key="item.categoryId + '^-^'"
                  :label="item.categoryName"
                  :value="item.categoryId"
                />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" icon="el-icon-search" @click="fetchData()">查询</el-button>
              <el-button type="primary" icon="el-icon-edit" @click="addDate()">新增属性</el-button>
              <el-button icon="el-icon-s-tools" @click="resetForm('listQuery')">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
      </el-card>
      <div style="height:20px;" />
      <div>
        <el-table
          ref="table"
          v-loading="listLoading"
          :data="list"
          style="width: 100%"
          border
        >
          <el-table-column label="类目属性ID">
            <template slot-scope="scope">{{ scope.row.categoryPropertyId }}</template>
          </el-table-column>
          <el-table-column label="属性ID">
            <template slot-scope="scope">{{ scope.row.propertyId }}</template>
          </el-table-column>
          <el-table-column label="属性名">
            <template slot-scope="scope">{{ scope.row.propertyName }}</template>
          </el-table-column>
          <el-table-column label="排序">
            <template slot-scope="scope">{{ scope.row.sortOrder }}</template>
          </el-table-column>
          <el-table-column label="状态">
            <template slot-scope="scope">{{ scope.row.status == 1 ? "启用" : "停用" }}</template>
          </el-table-column>
          <el-table-column label="属性类型">
            <template slot-scope="scope">{{ getPropertyType(scope.row.propertyType) }}</template>
          </el-table-column>
          <el-table-column label="录入方式">
            <template slot-scope="scope">
              {{ getInputType(scope.row.inputType) }}
            </template>
          </el-table-column>
          <el-table-column label="是否导航属性">
            <template slot-scope="scope">{{ scope.row.nav == 1 ? "是" : "否" }}</template>
          </el-table-column>
          <el-table-column label="是否必填">
            <template slot-scope="scope">
              {{ scope.row.required == 1 ? "必填" : "选填" }}
            </template></el-table-column>
          <el-table-column label="操作" width="300">
            <template slot-scope="scope">
              <el-button
                type="primary"
                size="mini"
                @click="handleLook(scope.$index,scope.row)"
              >查看属性值
              </el-button>
              <el-button
                type="primary"
                size="mini"
                @click="handleUpdate(scope.row)"
              >修改
              </el-button>
              <el-button
                size="mini"
                type="danger"
                @click="handleDelete(scope.$index, scope.row)"
              >删除
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.pageSize" @pagination="getList" />
    </div>
    <div v-if="attributeValue">
      <attributeValueListSearch ref="attributeValueListSearch" :showflag.sync="showflag" :cpid="categoryPropertyId" :cid="categoryId" @returnBack="returnBack" />
    </div>
    <!-- 新增/编辑弹框 -->
    <el-dialog v-if="dialogFormVisible" :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
      <attributeCUpdate ref="attributeCUpdate" :attributeflag.sync="attributeFlag" @closeClick="closeClick" @addClick="addClick" />
    </el-dialog>
  </div>
</template>
<script>
import Pagination from '@/components/Pagination' // secondary package based on el-pagination
import attributeValueListSearch from '@/components/productManage/categoryAttributeValueListSearch'
import attributeCUpdate from '@/components/productManage/attributeCUpdate'
import { fetchCategoryList, fetchCategoryPropertyList, deleteMallCategoryProperty } from '@/api/product-manage'
export default {
  components: {
    attributeValueListSearch, Pagination, attributeCUpdate },
  data() {
    return {
      attributeFlag: true,
      // 弹框是否显示
      dialogFormVisible: false,
      dialogStatus: '',
      textMap: {
        update: '属性修改',
        create: '新增属性'
      },
      categoryPropertyId: 0,
      categoryId: 0,
      // 标识
      showflag: true,
      backgroundCategory: false,
      attributeValue: false,
      // 分页
      total: 0,
      // loading
      listLoading: true,
      // table集合
      list: null,
      listCategoryQuery: {
        parentId: null,
        page: 1,
        pageSize: 500
      },
      listQuery: {
        // 所属类目:
        stair: '',
        //
        fristCategoryId: null,
        //
        secondCategoryId: null,
        //
        thridCategoryId: null,
        categoryId: 0,
        page: 1,
        pageSize: 10
      },
      // 一级
      fristList: [],
      // 二级
      secondList: [],
      // 三级
      thirdList: [],
      propertyTypeList: [
        {
          value: 1,
          label: '普通属性'
        }, {
          value: 2,
          label: '关键属性'
        },
        {
          value: 3,
          label: '文字销售属性'
        }, {
          value: 4,
          label: '图片销售属性'
        }
      ],
      inputTypeList: [
        {
          value: 0,
          label: '单选'
        }, {
          value: 1,
          label: '多选'
        },
        {
          value: 2,
          label: '输入'
        }
      ]
    }
  },
  created() {
    this.backgroundCategory = true
    // 列表查询
    this.getList()
    this.initFristList()
  },
  methods: {
    // 编辑新增
    addClick(data, flag) {
      this.dialogFormVisible = true
    },
    // 关闭
    closeClick() {
      this.dialogFormVisible = false
      this.getList()
    },
    // 返回
    returnBack() {
      this.backgroundCategory = true
      this.attributeValue = false
    },
    // 删除
    handleDelete(index, row) {
      console.log(row)
      deleteMallCategoryProperty(row).then(response => {
        this.$notify({
          title: 'Success',
          message: 'Delete Successfully',
          type: 'success',
          duration: 2000
        })
 
        this.getList()
      })
    },
    // 修改
    handleUpdate(row) {
      console.log(row)
      this.attributeFlag = true
      this.dialogStatus = 'update'
      this.dialogFormVisible = true
      setTimeout(() => {
        this.$refs.attributeCUpdate.updateDate(this.attributeFlag, row)
      }, 50)
    },
    // 查看属性值
    handleLook(index, row) {
      this.backgroundCategory = false
      this.categoryPropertyId = row.categoryPropertyId
      this.categoryId = row.categoryId
      this.attributeValue = true
    },
    // 列表查询
    getList() {
      this.listLoading = true
      this.chooseCategoryId()
      fetchCategoryPropertyList(this.listQuery).then(response => {
        this.list = response.model
        this.total = response.totalItem
 
        // Just to simulate the time of the request
        setTimeout(() => {
          this.listLoading = false
        }, 1.5 * 1000)
      })
    },
    // 查询方法
    fetchData() {
      this.getList()
    },
    // 重置表单
    resetForm(formName) {
      this.listQuery.fristCategoryId = null
      //
      this.listQuery.secondCategoryId = null
      //
      this.listQuery.thridCategoryId = null
 
      this.listQuery.categoryId = 0
 
      this.getList()
    },
    // 新增
    addDate() {
      this.chooseCategoryId()
      if (this.listQuery.categoryId === 0) {
        this.$message({
          message: '请先选择需要新增属性的类目!',
          type: 'success'
        })
        return false
      }
      const row = {}
      this.attributeFlag = false
      this.dialogStatus = 'create'
      this.dialogFormVisible = true
      setTimeout(() => {
        this.$refs.attributeCUpdate.updateDate(this.attributeFlag, row, this.listQuery.categoryId, this.listQuery.fristCategoryId, this.listQuery.secondCategoryId, this.listQuery.thridCategoryId)
      }, 50)
    },
    // 列表查询
    initFristList() {
      this.listLoading = true
      fetchCategoryList(this.listCategoryQuery).then(response => {
        this.fristList = response.model
        // Just to simulate the time of the request
        setTimeout(() => {
          this.listLoading = false
        }, 1.5 * 1000)
      })
    },
    selectCatOne(option) {
      this.listLoading = true
      this.listCategoryQuery.parentId = option
      this.listQuery.fristCategoryId = option
      // this.listCategoryQuery.parentId = item.categoryId
      fetchCategoryList(this.listCategoryQuery).then(response => {
        this.secondList = response.model
        // Just to simulate the time of the request
        setTimeout(() => {
          this.listLoading = false
        }, 1.5 * 1000)
        this.getList()
      })
    },
    selectCatTwo(option) {
      this.listLoading = true
      this.listCategoryQuery.parentId = option
      this.listQuery.secondCategoryId = option
      this.secondCategoryId = option
      // this.listCategoryQuery.parentId = item.categoryId
      fetchCategoryList(this.listCategoryQuery).then(response => {
        this.thirdList = response.model
        // Just to simulate the time of the request
        setTimeout(() => {
          this.listLoading = false
        }, 1.5 * 1000)
        this.getList()
      })
    },
    selectCatThree(option) {
      this.listQuery.thridCategoryId = option
      this.thridCategoryId = option
      this.listLoading = true
      this.getList()
    },
    chooseCategoryId() {
      if (this.listQuery.fristCategoryId !== null && this.listQuery.fristCategoryId !== '' && this.listQuery.fristCategoryId > 0) {
        this.listQuery.categoryId = this.listQuery.fristCategoryId
      }
 
      if (this.listQuery.secondCategoryId !== null && this.listQuery.secondCategoryId !== '' && this.listQuery.secondCategoryId > 0) {
        this.listQuery.categoryId = this.listQuery.secondCategoryId
      }
 
      if (this.listQuery.thridCategoryId !== null && this.listQuery.thridCategoryId !== '' && this.listQuery.thridCategoryId > 0) {
        this.listQuery.categoryId = this.listQuery.thridCategoryId
      }
    },
    getPropertyType(propertyType) {
      var rowData = this.propertyTypeList.filter(itmer => {
        if (itmer.value === propertyType) {
          return itmer.label
        }
      })
 
      if (undefined !== rowData[0]) {
        return (rowData[0].label)
      }
    },
 
    getInputType(inputType) {
      var rowData = this.inputTypeList.filter(itmer => {
        if (itmer.value === inputType) {
          return itmer.label
        }
      })
 
      if (undefined !== rowData[0]) {
        return (rowData[0].label)
      }
    }
  }
}
</script>
 
<style scoped>
#attributeLibraryDiv /deep/ .el-dialog__body {
    padding: 0px 20px;
}
</style>

类目的联动实现

考虑到页面的功能较多,我们依然采用先部分再到整体的实现策略,比如三级类目联动的实现。

在实现之前,我们先想一想,联动是怎样一个过程?首先需要显示一级类目吧?二级类目的展示,自然需要一级类目的数据支持,只有当确定了一级类目,才知道需要一级类目下的二级类目数据,同样的,三级类目,也就依赖于二级类目的选择了。

既然是根据数据做再做选择,那么自然离不开el-select组件了。

既然是三个类目列表,自然少不了存储和记录数据。话说在类似VUE这类前端框架下开发,真的比以前省事儿多了,准备好数据,组件帮你渲染就好了。

查询之间也有依赖关系,自然也需要保存每次选择的结果。

接下来,自然是要考虑数据获取的问题了。一级类目选择之后,自然要加载二级类目的数据了。

嗯,似乎在选择类目数据时,是支持搜索的,这一点el-select组件已经帮我们实现好了。加上clearable filterable 两个属性就可以了。

等等,还忘记了一件事情,API在哪里呢?

// 类目
export function fetchCategoryList(query) {
  return request({
    url: '/category/findByPage',
    method: 'post',
    data: query
  })
}

这个不是之前分页的时候用过吗?嗯,可以考虑改变页码大小的方式继续使用嘛,稍微灵活变通下,满足要求就可以了。当然,你也可以开发一个专用的不分页API进行查询。

listCategoryQuery: {
        parentId: null,
        page: 1,
        pageSize: 500
      }
 

需要注意的是这个“不分页返回类目列表”的实现,因为在后续的很多场景中,往往要求获取整个查询条件下的后台类目,此时封装一个区别于分页的接口,算是一种预先考虑的目的。

类目联动后端实现

后端的实现就简单多了,我们只用返回对应的数据就可以了。用到的api也不多,之前已经实现过了,见MallCategoryController。

 /**
     * 分页返回类目列表
     * @param queryMallCategory
     * @return
     */
    @RequestMapping("/findByPage")
    public  Result<List<MallCategory>> findByPage(@RequestBody QueryMallCategory queryMallCategory){
        return mallCategoryService.getMallCategorysByPage(queryMallCategory);
    }

service以及dao层面的实现,搞定代码生成器猿实战05——手把手教你拥有自己的代码生成器,就可以得到你想要的功能了。想要一起实战吗?关注公号即可获取基础代码!