4.商品查询
4.1.效果预览
接下来,我们实现商品管理的页面,先看下我们要实现的效果:
可以看出整体是一个table,然后有新增按钮。是不是跟昨天写品牌管理很像?
模板代码在分别在Goods.vue
4.2.从0开始
接下来,我们自己来实现一下,新建两个组件:MyGoods.vue和MyGoodsForm.vue
内容先随意:
代码语言:javascript复制<template>
<v-card>
MyGoods
</v-card>
</template>
<script>
export default {
name: "my-goods",
data() {
return {
}
}
}
</script>
<style scoped>
</style>
然后修改menu.js,新建一个菜单:
修改router/index.js,添加一个路由:
预览一下:
4.3.页面实现
4.3.1.页面基本表格
商品列表页与品牌列表页几乎一样,我们可以直接去复制一份过来,然后进行一些修改。
首先,字段不一样,商品列表也展示的SPU信息,包含以下字段:
代码语言:javascript复制id:
title:标题
cname:商品分类名称
bname:品牌名称
完整代码:
代码语言:javascript复制<template>
<v-card>
<v-card-title>
<v-btn color="primary" @click="addGoods">新增商品</v-btn>
<!--搜索框,与search属性关联-->
<v-spacer/>
<v-text-field label="输入关键字搜索" v-model.lazy="search" append-icon="search" hide-details/>
</v-card-title>
<v-divider/>
<v-data-table
:headers="headers"
:items="goodsList"
:search="search"
:pagination.sync="pagination"
:total-items="totalGoods"
:loading="loading"
class="elevation-1"
>
<template slot="items" slot-scope="props">
<td>{{ props.item.id }}</td>
<td class="text-xs-center">{{ props.item.title }}</td>
<td class="text-xs-center">{{props.item.cname}}</td>
<td class="text-xs-center">{{ props.item.bname }}</td>
<td class="justify-center layout">
<v-btn color="info" @click="editGoods(props.item)">编辑</v-btn>
<v-btn color="warning">删除</v-btn>
<v-btn >下架</v-btn>
</td>
</template>
</v-data-table>
<!--弹出的对话框-->
<v-dialog max-width="500" v-model="show" persistent>
<v-card>
<!--对话框的标题-->
<v-toolbar dense dark color="primary">
<v-toolbar-title>{{isEdit ? '修改' : '新增'}}商品</v-toolbar-title>
<v-spacer/>
<!--关闭窗口的按钮-->
<v-btn icon @click="closeWindow"><v-icon>close</v-icon></v-btn>
</v-toolbar>
<!--对话框的内容,表单-->
<v-card-text class="px-5">
<my-goods-form :oldGoods="oldGoods" />
</v-card-text>
</v-card>
</v-dialog>
</v-card>
</template>
<script>
// 导入自定义的表单组件
import MyGoodsForm from './MyGoodsForm'
export default {
name: "my-goods",
data() {
return {
search: '', // 搜索过滤字段
totalGoods: 0, // 总条数
goodsList: [], // 当前页品牌数据
loading: true, // 是否在加载中
pagination: {}, // 分页信息
headers: [
{text: 'id', align: 'center', value: 'id'},
{text: '标题', align: 'center', sortable: false, value: 'title'},
{text: '商品分类', align: 'center', sortable: false, value: 'cname'},
{text: '品牌', align: 'center', value: 'bname', sortable: false,},
{text: '操作', align: 'center', sortable: false}
],
show: false,// 控制对话框的显示
oldGoods: {}, // 即将被编辑的商品信息
isEdit: false, // 是否是编辑
}
},
mounted() { // 渲染后执行
// 查询数据
this.getDataFromServer();
},
watch: {
pagination: { // 监视pagination属性的变化
deep: true, // deep为true,会监视pagination的属性及属性中的对象属性变化
handler() {
// 变化后的回调函数,这里我们再次调用getDataFromServer即可
this.getDataFromServer();
}
},
search: { // 监视搜索字段
handler() {
this.getDataFromServer();
}
}
},
methods: {
getDataFromServer() { // 从服务的加载数的方法。
// 发起请求
this.$http.get("/item/spu/page", {
params: {
key: this.search, // 搜索条件
page: this.pagination.page,// 当前页
rows: this.pagination.rowsPerPage,// 每页大小
sortBy: this.pagination.sortBy,// 排序字段
desc: this.pagination.descending// 是否降序
}
}).then(resp => { // 这里使用箭头函数
this.goodsList = resp.data.items;
this.totalGoods = resp.data.total;
// 完成赋值后,把加载状态赋值为false
this.loading = false;
})
},
addGoods() {
// 修改标记
this.isEdit = false;
// 控制弹窗可见:
this.show = true;
// 把oldBrand变为null
this.oldBrand = null;
},
editGoods(oldGoods){
// 修改标记
this.isEdit = true;
// 控制弹窗可见:
this.show = true;
// 获取要编辑的brand
this.oldGoods = oldGoods;
},
closeWindow(){
// 重新加载数据
this.getDataFromServer();
// 关闭窗口
this.show = false;
}
},
components:{
MyGoodsForm
}
}
</script>
<style scoped>
</style>
主要的改动点:
- 页面的
v-data-table
中的属性绑定修改。items指向goodsList,totalItems指向totalGoods - 页面渲染的字段名修改:字段改成商品的SPU字段:id、title,cname(商品分类名称),bname(品牌名称)
- data属性修改了以下属性:
- goodsList:当前页商品数据
- totalGoods:商品总数
- headers:头信息,需要修改头显示名称
- oldGoods:准备要修改的商品
- 加载数据的函数:getDataFromServer,请求的路径进行了修改,另外去除了跟排序相关的查询。SPU查询不排序
- 新增商品的事件函数:清除了一些数据查询接口,只保留弹窗
查看效果:
因为没有编写查询功能,表格一直处于loading状态。
接下来看弹窗:
4.3.2.上下架状态按钮
另外,似乎页面少了对上下架商品的过滤,在原始效果图中是有的:
这在Vuetify中是一组按钮,我们查看帮助文档:
查看实例得到以下信息:
v-btn
:一个按钮
v-btn-toggle
:按钮组,内部可以有多个按钮,点击切换,有以下属性:
- multiple:是否支持多选,默认是false
- value:选中的按钮的值,如果是多选,结果是一个数组;单选,结果是点击的v-btn中的value值,因此按钮组的每个btn都需要指定value属性
改造页面:
首先在data中定义一个属性,记录按钮的值。
代码语言:javascript复制filter:{
saleable: false, // 上架还是下架
search: '', // 搜索过滤字段
}
这里我们的做法是定义一个filter属性,内部在定义search来关联过滤字段,saleable来关联上下架情况。
这样watch就必须监听filter,而不是只监听search了:
代码语言:javascript复制filter: {// 监视搜索字段
handler() {
this.getDataFromServer();
},
deep:true
}
另外,页面中与search有关的所有字段都需要修改成filter.search:
代码语言:javascript复制<!--搜索框,与search属性关联-->
<v-text-field label="输入关键字搜索" v-model.lazy="filter.search" append-icon="search" hide-details/>
然后,在页面中添加按钮组:
代码语言:javascript复制 <v-flex xs3>
状态:
<v-btn-toggle v-model="filter.saleable">
<v-btn flat>
全部
</v-btn>
<v-btn flat :value="true">
上架
</v-btn>
<v-btn flat :value="false">
下架
</v-btn>
</v-btn-toggle>
</v-flex>
最后,不要忘了在查询时,将saleable携带上:
代码语言:javascript复制getDataFromServer() { // 从服务的加载数的方法。
// 发起请求
this.$http.get("/item/spu/page", {
params: {
key: this.filter.search, // 搜索条件
saleable: this.filter.saleable, // 上下架
page: this.pagination.page,// 当前页
rows: this.pagination.rowsPerPage,// 每页大小
}
}).then(resp => { // 这里使用箭头函数
this.goodsList = resp.data.items;
this.totalGoods = resp.data.total;
// 完成赋值后,把加载状态赋值为false
this.loading = false;
})
}
4.4.后台提供接口
页面已经准备好,接下来在后台提供分页查询SPU的功能:
4.4.1.实体类
代码语言:javascript复制SPU
@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
}
代码语言:javascript复制SPU详情
@Table(name="tb_spu_detail")
public class SpuDetail {
@Id
private Long spuId;// 对应的SPU的id
private String description;// 商品描述
private String specTemplate;// 商品特殊规格的名称及可选值模板
private String specifications;// 商品的全局规格属性
private String packingList;// 包装清单
private String afterService;// 售后服务
// 省略getter和setter
}
4.4.2.controller
先分析:
- 请求方式:GET
- 请求路径:/spu/page
- 请求参数:
- page:当前页
- rows:每页大小
- key:过滤条件
- saleable:上架或下架
- 返回结果:商品SPU的分页信息。
- 要注意,页面展示的是商品分类和品牌名称,而数据库中保存的是id,怎么办?
我们可以新建一个类,继承SPU,并且拓展cname和bname属性,写到
ly-item-interface
public class SpuBo extends Spu { String cname;// 商品分类名称 String bname;// 品牌名称 // 略 。。 }
- 要注意,页面展示的是商品分类和品牌名称,而数据库中保存的是id,怎么办?
我们可以新建一个类,继承SPU,并且拓展cname和bname属性,写到
编写controller代码:
我们把与商品相关的一切业务接口都放到一起,起名为GoodsController,业务层也是这样
代码语言:javascript复制@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
/**
* 分页查询SPU
* @param page
* @param rows
* @param key
* @return
*/
@GetMapping("/spu/page")
public ResponseEntity<PageResult<SpuBo>> querySpuByPage(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "rows", defaultValue = "5") Integer rows,
@RequestParam(value = "key", required = false) String key) {
// 分页查询spu信息
PageResult<SpuBo> result = this.goodsService.querySpuByPageAndSort(page, rows, key);
if (result == null || result.getItems().size() == 0) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(result);
}
}
4.4.3.service
所有商品相关的业务(包括SPU和SKU)放到一个业务下:GoodsService。
代码语言:javascript复制@Service
public class GoodsService {
@Autowired
private SpuMapper spuMapper;
@Autowired
private CategoryService categoryService;
@Autowired
private BrandMapper brandMapper;
public PageResult<SpuBo> querySpuByPageAndSort(Integer page, Integer rows, Boolean saleable, String key) {
// 1、查询SPU
// 分页,最多允许查100条
PageHelper.startPage(page, Math.min(rows, 100));
// 创建查询条件
Example example = new Example(Spu.class);
Example.Criteria criteria = example.createCriteria();
// 是否过滤上下架
if (saleable != null) {
criteria.orEqualTo("saleable", saleable);
}
// 是否模糊查询
if (StringUtils.isNotBlank(key)) {
criteria.andLike("title", "%" key "%");
}
Page<Spu> pageInfo = (Page<Spu>) this.spuMapper.selectByExample(example);
List<SpuBo> list = pageInfo.getResult().stream().map(spu -> {
// 2、把spu变为 spuBo
SpuBo spuBo = new SpuBo();
// 属性拷贝
BeanUtils.copyProperties(spu, spuBo);
// 3、查询spu的商品分类名称,要查三级分类
List<String> names = this.categoryService.queryNameByIds(
Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
// 将分类名称拼接后存入
spuBo.setCname(StringUtils.join(names, "/"));
// 4、查询spu的品牌名称
Brand brand = this.brandMapper.selectByPrimaryKey(spu.getBrandId());
spuBo.setBname(brand.getName());
return spuBo;
}).collect(Collectors.toList());
return new PageResult<>(pageInfo.getTotal(), list);
}
}
4.4.4.mapper
代码语言:javascript复制public interface SpuMapper extends Mapper<Spu> {
}
4.4.5.Category中拓展查询名称的功能
页面需要商品的分类名称需要在这里查询,因此要额外提供查询分类名称的功能,
在CategoryService中添加功能:
代码语言:javascript复制public List<String> queryNameByIds(List<Long> ids) {
return this.categoryMapper.selectByIdList(ids).stream().map(Category::getName).collect(Collectors.toList());
}
mapper的selectByIDList方法是来自于通用mapper。不过需要我们在mapper上继承一个通用mapper接口:
代码语言:javascript复制public interface CategoryMapper extends Mapper<Category>, SelectByIdListMapper<Category, Long> {
// ...coding
}
4.5.测试
刷新页面,查看效果:
基本与预览的效果一致,OK!