1.1.页面布局
1.1.1.整体布局
打开规格参数页面,看到如下内容:
商品分类树我们之前已经做过,所以这里可以直接展示出来。
因为规格是跟商品分类绑定的,因此首先会展现商品分类树,并且提示你要选择商品分类,才能看到规格参数的模板。一起了解下页面的实现:
页面结构:
这里使用了v-layout
来完成页面布局,并且添加了row属性,代表接下来的内容是行布局(左右)。
可以看出页面分成1个部分:
-
<v-flex xs3>
:左侧,内部又分上下两部分:商品分类树及标题-
v-card-title
:标题部分,这里是提示信息,告诉用户要先选择分类,才能看到模板 -
v-tree
:这里用到的是我们之前讲过的树组件,展示商品分类树,
-
-
<v-flex xs9 class="px-1">
:右侧:内部是规格参数展示
1.1.2.右侧规格
当我们点击一个分类时,最终要达到的效果:
可以看到右侧分为上下两部分:
-
上部:面包屑,显示当前选中的分类
-
下部:table,显示规格参数信息
页面实现:
可以看到右侧并不是我们熟悉的 v-data-table
,而是一个spec-group
组件(规格组)和spec-param
组件(规格参数),这是我们定义的独立组件:
在SpecGroup中定义了表格:
1.2.规格组的查询
1.2.1.树节点的点击事件
当我们点击树节点时,要将v-dialog
打开,因此必须绑定一个点击事件:(Specification.vue)
我们来看下handleClick
方法:(Specification.vue)
点击事件发生时,发生了两件事:
-
记录当前选中的节点,选中的就是商品分类
-
showGroup
被置为true,则规格组就会显示了。
同时,我们把被选中的节点(商品分类)的id传递给了SpecGroup
组件:(Specification.vue)
1.2.2.页面查询规格组
来看下SpecGroup.vue
中的实现:
我们查看页面控制台,可以看到请求已经发出:
1.2.3.后端代码
实体类
在leyou-item-interface
中添加实体类:
内容:
@Table(name = "tb_spec_group") public class SpecGroup { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long cid; private String name; @Transient private List<SpecParam> params; // getter和setter省略 }
@Table(name = "tb_spec_param") public class SpecParam { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long cid; private Long groupId; private String name; @Column(name = "`numeric`") private Boolean numeric; private String unit; private Boolean generic; private Boolean searching; private String segments; // getter和setter ... }
在leyou-item-service
中编写业务:
mapper
public interface SpecGroupMapper extends Mapper<SpecGroup> { }
controller
先分析下需要的东西,在页面的ajax请求中可以看出:
-
请求方式:get
-
请求路径:/spec/groups/{cid} ,这里通过路径占位符传递商品分类的id
-
请求参数:商品分类id
-
返回结果:页面是直接把
resp.data
赋值给了groups:
那么我们返回的应该是规格组SpecGroup的集合
代码:
@RestController @RequestMapping("spec") public class SpecificationController { @Autowired private SpecificationService specificationService; /** * 根据分类id查询分组 * @param cid * @return */ @GetMapping("groups/{cid}") public ResponseEntity<List<SpecGroup>> queryGroupsByCid(@PathVariable("cid")Long cid){ List<SpecGroup> groups = this.specificationService.queryGroupsByCid(cid); if (CollectionUtils.isEmpty(groups)){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(groups); } }
service
@Service public class SpecificationService { @Autowired private SpecGroupMapper groupMapper; /** * 根据分类id查询分组 * @param cid * @return */ public List<SpecGroup> queryGroupsByCid(Long cid) { SpecGroup specGroup = new SpecGroup(); specGroup.setCid(cid); return this.groupMapper.select(specGroup); } }
页面访问测试:
目前,我们数据库只为手机分类(76)提供了规格组:
我们访问:http://api.leyou.com/api/item/spec/groups/76
然后在后台系统中测试:
1.3.规格参数查询
1.3.1.表格切换
当我们点击规格组,会切换到规格参数显示,肯定是在规格组中绑定了点击事件:
我们看下事件处理:
可以看到这里是使用了父子通信,子组件触发了select事件
再来看下父组件的事件绑定:
事件处理:
这里我们记录了选中的分组,并且把标记设置为false,这样规格组就不显示了,而是显示:SpecParam
并且,我们把group也传递到spec-param
组件:
1.3.2.页面查询规格参数
我们来看SpecParam.vue
的实现:
查看页面控制台,发现请求已经发出:
报404,因为我们还没有实现后台逻辑,接下来就去实现。
1.3.3.后台实现
SpecificationController
分析:
-
请求方式:GET
-
请求路径:/spec/params
-
请求参数:gid,分组id
-
返回结果:该分组下的规格参数集合
List<SpecParam>
代码:
/** * 根据条件查询规格参数 * @param gid * @return */ @GetMapping("params") public ResponseEntity<List<SpecParam>> queryParams(@RequestParam("gid")Long gid){ List<SpecParam> params = this.specificationService.queryParams(gid); if (CollectionUtils.isEmpty(params)){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(params); }
SpecificationService
@Autowired private SpecParamMapper paramMapper; /** * 根据条件查询规格参数 * @param gid * @return */ public List<SpecParam> queryParams(Long gid) { SpecParam param = new SpecParam(); param.setGroupId(gid); return this.paramMapper.select(param); }
SpecParamMapper
public interface SpecParamMapper extends Mapper<SpecParam> { }
测试:
2. spu sku
sku:标准产品单元,商品集,spuDetail(generic_spec:通用规格参数(id,value) special_spec:特殊规格参数(id,[value]))
spu:标准库存单元,具体的商品(indexes:0_0_0 own_spec:具体的值) stock
3. 商品查询
- 添加查询条件
- 添加过滤条件
- 添加分页
- 执行查询
- List<spu> --> List<spuBo>
- new PageResult<>(total,spuBos)
可以看出整体是一个table,然后有新增按钮。
并且在Vue实例挂载后就会发起查询(mounted调用getDataFromServer方法初始化数据):
我们刷新页面,可以看到浏览器发起已经发起了查询商品数据的请求:
因此接下来,我们编写接口即可。
@Table(name = "tb_spu") public class Spu { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long brandId; private Long cid1;// 1级类目 private Long cid2;// 2级类目 private Long cid3;// 3级类目 private String title;// 标题 private String subTitle;// 子标题 private Boolean saleable;// 是否上架 private Boolean valid;// 是否有效,逻辑删除用 private Date createTime;// 创建时间 private Date lastUpdateTime;// 最后修改时间 // 省略getter和setter }
SPU详情
@Table(name="tb_spu_detail") public class SpuDetail { @Id private Long spuId;// 对应的SPU的id private String description;// 商品描述 private String specialSpec;// 商品特殊规格的名称及可选值模板 private String genericSpec;// 商品的全局规格属性 private String packingList;// 包装清单 private String afterService;// 售后服务 // 省略getter和setter }
public interface SpuMapper extends Mapper<Spu> { }
先分析:
-
请求方式:GET
-
请求路径:/spu/page
-
请求参数:
-
page:当前页
-
rows:每页大小
-
key:过滤条件
-
saleable:上架或下架
-
-
返回结果:商品SPU的分页信息。
-
要注意,页面展示的是商品分类和品牌名称,而数据库中保存的是id,怎么办?
我们可以新建一个类,继承SPU,并且拓展cname和bname属性,写到
leyou-item-interface
-
public class SpuBo extends Spu { String cname;// 商品分类名称 String bname;// 品牌名称 // 略 。。 }
编写controller代码:
我们把与商品相关的一切业务接口都放到一起,起名为GoodsController,业务层也是这样
@Controller public class GoodsController { @Autowired private GoodsService goodsService; @GetMapping("spu/page") public ResponseEntity<PageResult<SpuBo>> querySpuBoByPage( @RequestParam(value = "key", required = false)String key, @RequestParam(value = "saleable", required = false)Boolean saleable, @RequestParam(value = "page", defaultValue = "1")Integer page, @RequestParam(value = "rows", defaultValue = "5")Integer rows ){ PageResult<SpuBo> pageResult = this.goodsService.querySpuBoByPage(key, saleable, page, rows); if(CollectionUtils.isEmpty(pageResult.getItems())){ return ResponseEntity.notFound().build(); } return ResponseEntity.ok(pageResult); } }
@Service public class GoodsService { @Autowired private SpuMapper spuMapper; @Autowired private CategoryService categoryService; @Autowired private BrandMapper brandMapper; public PageResult<SpuBo> querySpuBoByPage(String key, Boolean saleable, Integer page, Integer rows) { Example example = new Example(Spu.class); Example.Criteria criteria = example.createCriteria(); // 搜索条件 if (StringUtils.isNotBlank(key)) { criteria.andLike("title", "%" + key + "%"); } if (saleable != null) { criteria.andEqualTo("saleable", saleable); } // 分页条件 PageHelper.startPage(page, rows); // 执行查询 List<Spu> spus = this.spuMapper.selectByExample(example); PageInfo<Spu> pageInfo = new PageInfo<>(spus); List<SpuBo> spuBos = new ArrayList<>(); spus.forEach(spu->{ SpuBo spuBo = new SpuBo(); // copy共同属性的值到新的对象 BeanUtils.copyProperties(spu, spuBo); // 查询分类名称 List<String> names = this.categoryService.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3())); spuBo.setCname(StringUtils.join(names, "/")); // 查询品牌的名称 spuBo.setBname(this.brandMapper.selectByPrimaryKey(spu.getBrandId()).getName()); spuBos.add(spuBo); }); return new PageResult<>(pageInfo.getTotal(), spuBos); } }
页面需要商品的分类名称需要在这里查询,因此要额外提供查询分类名称的功能,
在CategoryService中添加功能:
public List<String> queryNamesByIds(List<Long> ids) { List<Category> list = this.categoryMapper.selectByIdList(ids); List<String> names = new ArrayList<>(); for (Category category : list) { names.add(category.getName()); } return names; // return list.stream().map(category -> category.getName()).collect(Collectors.toList()); }
mapper的selectByIdList方法是来自于通用mapper。不过需要我们在mapper上继承一个通用mapper接口:
public interface CategoryMapper extends Mapper<Category>, SelectByIdListMapper<Category, Long> { }