所有代码发布在 [https://github.com/hades0525/leyou]
Day12
2019年2月9日
23:42
商品实现分页翻页
- 分页条
<!--分页条-->
<divclass="fr">
<divclass="sui-paginationpagination-large">
<ul>
<li:class="{prev:true,disabled:search.page===1}">
<ahref="#"@click.prevent="prePage">«上一页</a>
</li>
<li:class="{active:index(i)===search.page}"v-for="iinMath.min(5,totalPage)":key="i"
@click="">
<ahref="#"v-text="index(i)"></a>
</li>
<liclass="dotted"v-show="search.page+2<totalPage&&totalPage>5"><span>...</span></li>
<li:class="{next:true,disabled:search.page===totalPage}">
<ahref="#"@click.prevent="nextPage">下一页»</a>
</li>
</ul>
<div><span>共{{totalPage}}页 </span><span>
- 上端搜索结果
<divclass="top-pagination">
<span>共<istyle="color:#222;">{{total}}</i>商品</span>
<span><istyle="color:red;">{{search.page}}</i>/{{totalPage}}</span>
<aclass="btn-arrow"href="#"style="display:inline-block"@click.prevent="prePage"><</a>
<aclass="btn-arrow"href="#"style="display:inline-block"@click.prevent="nextPage">></a>
</div>
- methods
index(i){
if(this.search.page<=3||this.totalPage<=5){
return i;
}else if(this.search.page>=this.totalPage-2){
return this.totalPage-5+i;
}else{
return i+this.search.page-3;
}
},
prePage(){
if(this.search.page>1){
this.search.page--
}
},
nextPage(){
if(this.search.page<this.totalPage){
this.search.page++
}
},
- watch
watch:{
search:{
deep:true,
handler(val, oldValue) {
if (!oldValue || !oldValue.key) {
// 如果旧的search值为空,或者search中的key为空,证明是第一次
return;
}
// this.loadData(); 这样会使页面刷新把查询参数弄没了
//把请求参数写到url中
location.search = "?" + ly.stringify(this.search);
}
}
},
对分类和品牌聚合
6. 扩展返回结果对象
• 原来我们返回的结果是PageResult对象,里面只有total、totalPage、items3个属性。但是现在要对商品分类和品牌进行聚合,数据显然不够用,我们需要对返回的结果进行扩展,添加分类和品牌的数据。
• 新建一个类,继承PageResult,然后扩展两个新的属性:分类集合和品牌集合
@Data
public class SearchResult extends PageResult<Goods>{
private List<Category> categories;
private List<Brand> brands;
public SearchResult(){
}
public SearchResult(Longtotal,List<Goods> items,IntegertotalPage,List<Category>categories,List<Brand>brands){
super(total,items,totalPage);
this.categories=categories;
this.brands=brands;
}
}
- 提供查询接口
• brandApi
/**
*根据ids查询list
*@paramids
*@return
*/
@GetMapping("brand/list")
List<Brand>queryBrandByIds(@RequestParam("ids")List<Long>ids);
• brandController
/**
*根据ids找到brandlist
*@paramids
*@return
*/
@GetMapping("list")
publicResponseEntity<List<Brand>>queryBrandByIds(@RequestParam("ids")List<Long>ids){
returnResponseEntity.ok(brandService.queryBrandByIds(ids));
}
• brandService
/**
*根据ids找到brandlist
*@paramids
*@return
*/
@GetMapping("list")
publicResponseEntity<List<Brand>>queryBrandByIds(@RequestParam("ids")List<Long>ids){
returnResponseEntity.ok(brandService.queryBrandByIds(ids));
}
• brandMapper
• 继承自写的BaseMapper
• 搜索功能改造
/**
*搜索功能
*@paramsearchRequest
*@return
*/
publicPageResult<Goods>search(SearchRequestsearchRequest){
intpage=searchRequest.getPage()-1;
intsize=searchRequest.getSize();
//创建查询构建器
NativeSearchQueryBuilderqueryBuilder=newNativeSearchQueryBuilder();
//0.结果过滤
queryBuilder.withSourceFilter(newFetchSourceFilter(newString[]{"id","subTitle","skus"},null));
//1.分页,从0开始
queryBuilder.withPageable(PageRequest.of(page,size));
//2.过滤
queryBuilder.withQuery(QueryBuilders.matchQuery("all",searchRequest.getKey()));
//3.聚合分类和品牌
//3.1聚合分类
StringCategoryAggName="category_agg";
queryBuilder.addAggregation(AggregationBuilders.terms(CategoryAggName).field("cid3"));
//3.2聚合品牌
StringBrandAggName="brand_agg";
queryBuilder.addAggregation(AggregationBuilders.terms(BrandAggName).field("brandId"));
//4.查询
//Page<Goods>result=repository.search(queryBuilder.build());
AggregatedPage<Goods>result=template.queryForPage(queryBuilder.build(),Goods.class);
//5.解析结果
//5.1解析分页结果
longtotal=result.getTotalElements();
inttotalPages=result.getTotalPages();
List<Goods>goodsList=result.getContent();
//5.2解析聚合结果
Aggregationsaggs=result.getAggregations();
List<Category>categories=parseCategoryAgg(aggs.get(CategoryAggName));
List<Brand>brands=parseBrandAgg(aggs.get(BrandAggName));
return new SearchResult(total,goodsList,totalPages,categories,brands);
}
/**
*解析分类聚合
*@paramterms
*@return
*/
private List<Category> parseCategoryAgg(LongTermsterms){
try{
List<Long> ids = terms.getBuckets().stream()
.map(b->b.getKeyAsNumber().longValue())
.collect(Collectors.toList());
List<Category> categories = categoryClient.queryCategoryByIds(ids);
return categories;
}catch(Exceptione){
log.error("[搜索服务]查询分类异常:",e);
return null;
}
}
/**
*解析品牌聚合
*@paramterms
*@return
*/
private List<Brand> parseBrandAgg(LongTerms terms){
try{
List<Long> ids = terms.getBuckets().stream()
.map(b->b.getKeyAsNumber().longValue())
.collect(Collectors.toList());
List<Brand> brands = brandClient.queryBrandByIds(ids);
return brands;
}catch(Exceptione){
log.error("[搜索服务]查询品牌异常:",e);
return null;
}
}
-
结果
-
分类品牌页面渲染
• 分类、品牌内容都不太一样,但是结构相似,都是key和value的结构。
• 把所有的过滤条件放入一个数组中,然后在页面利用v-for遍历一次生成
[
{
k:"过滤字段名",
options:[{/*过滤字段值对象*/},{/*过滤字段值对象*/}]
}
]
• 先在data中定义数组,等待组装过滤参数
filters:[]
• 在查询搜索结果的回调函数loadData中,对过滤参数进行封装
//获取聚合结果,形成过滤项
//商品分类
this.filters.push({
k:"cid3",
options:resp.data.categories,
});
//商品品牌
this.filters.push({
k:"brandId",
options:resp.data.brands,
});
• 结果
• 页面渲染数据
<div class="type-wrap" v-for="f in filters" :key="f.k" v-if="f.k !== 'brandId'">
<div class="fl key" v-text="f.k === 'cid3' ? '分类' : f.k"></div>
<div class="fl value">
<ul class="type-list">
<li v-for="(o,i) in f.options" :key="i">
<a v-text="o.name"></a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
<div class="type-wrap logo" v-else>
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list" v-for="(o,i) in f.options" :key="i">
<li v-if="o.image "><img :src="o.image" /></li>
<li v-else=><a href="#" v-text="o.name"></a></li>
</ul>
</div>
<div class="fl ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
</div>
</div>
规格参数聚合
• 分析步骤
• 1)用户搜索得到商品,并聚合出商品分类
• 2)判断分类数量是否等于1,如果是则进行规格参数聚合
• 3)先根据分类,查找可以用来搜索的规格
• 4)对规格参数进行聚合
• 5)将规格参数聚合结果整理后返回
• 扩展返回结果
• 返回结果中需要增加新数据,用来保存规格参数过滤条件。
[
{
"k":"规格参数名",
"options":["规格参数值","规格参数值"]
}
]
• 在java中我们用List<Map<String,Object>>来表示
@Data
publicclassSearchResultextendsPageResult<Goods>{
//分类待选项
privateList<Category>categories;
//品牌待选项
privateList<Brand>brands;
//规格参数key及待选项
private List<Map<String,Object>> specs;
publicSearchResult(){
}
public SearchResult(Longtotal,List<Goods>items,IntegertotalPage,List<Category>categories,List<Brand>brands,List<Map<String,Object>>specs){
super(total,items,totalPage);
this.categories=categories;
this.brands=brands;
this.specs=specs;
}
}
• 搜索功能改造
/**
*搜索功能
*@paramsearchRequest
*@return
*/
publicPageResult<Goods>search(SearchRequestsearchRequest){
intpage=searchRequest.getPage()-1;
intsize=searchRequest.getSize();
//创建查询构建器
NativeSearchQueryBuilderqueryBuilder=newNativeSearchQueryBuilder();
//0.结果过滤
queryBuilder.withSourceFilter(newFetchSourceFilter(newString[]{"id","subTitle","skus"},null));
//1.分页,从0开始
queryBuilder.withPageable(PageRequest.of(page,size));
//2.过滤
//查询条件
MatchQueryBuilderbasicQuery=QueryBuilders.matchQuery("all",searchRequest.getKey());
queryBuilder.withQuery(basicQuery);
//3.聚合分类和品牌
//3.1聚合分类
StringCategoryAggName="category_agg";
queryBuilder.addAggregation(AggregationBuilders.terms(CategoryAggName).field("cid3"));
//3.2聚合品牌
StringBrandAggName="brand_agg";
queryBuilder.addAggregation(AggregationBuilders.terms(BrandAggName).field("brandId"));
//4.查询
//Page<Goods>result=repository.search(queryBuilder.build());
AggregatedPage<Goods>result=template.queryForPage(queryBuilder.build(),Goods.class);
//5.解析结果
//5.1解析分页结果
longtotal=result.getTotalElements();
inttotalPages=result.getTotalPages();
List<Goods>goodsList=result.getContent();
//5.2解析聚合结果
Aggregationsaggs=result.getAggregations();
List<Category>categories=parseCategoryAgg(aggs.get(CategoryAggName));
List<Brand>brands=parseBrandAgg(aggs.get(BrandAggName));
//6.完成规格参数聚合
List<Map<String,Object>> specs = null;
if(categories!=null&&categories.size()==1){
//商品分类存在并且数量为1,可以聚合规格参数
specs = buildSpecifictionAgg(categories.get(0).getId(),basicQuery);
}
return new SearchResult(total,goodsList,totalPages,categories,brands,specs);
}
/**
*聚合规格参数
*@paramcid
*@parambasicQuery
*@return
*/
private List<Map<String,Object>> buildSpecifictionAgg(Longcid,MatchQueryBuilderbasicQuery){
List<Map<String,Object>> specs = new ArrayList<>();
//1.查询需要聚合的规格参数
List<SpecParam> params = specifictionClient.queryParamByList(null,cid,true);
//2.聚合
NativeSearchQueryBuilder queryBuilder =new NativeSearchQueryBuilder();
//2.1带上查询条件
queryBuilder.withQuery(basicQuery);
//2.2聚合
for(SpecParamparam:params){
String name = param.getName();
queryBuilder.addAggregation(
AggregationBuilders.terms(name).field("specs."+name+".keyword"));
}
//3.获取结果
AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(),Goods.class);
//4.解析结果
Aggregations aggs= result.getAggregations();
for(SpecParamparam:params){
//规格参数名称
String name = param.getName();
StringTerms terms = aggs.get(name);
//待选项
List<String> options = terms.getBuckets().stream().map(
b->b.getKeyAsString()).collect(Collectors.toList());
//准备map
Map<String,Object> map = new HashMap<>();
map.put("k",name);
map.put("options",options);
specs.add(map);
}
returnspecs;
}
-
结果
-
规格参数页面渲染
• loadData中
//其他规格
resp.data.specs.forEach(spec=>this.filters.push(spec));
• 展示 v-if:有些属性为空,o不为空才展示;options里面直接是属性,所以直接为o
<ulclass="type-list">
<li v-for="(o,i)inf.options" :key="i" v-if="o">
<a v-text="o.name || o"></a>
</li>
</ul>
• 过滤条件内容展示得过多,通过按钮点击来展开和隐藏部分内容
//在data中定义变量
showMore:false,
• 添加绑定事件
<div class="type-wrap" style="text-align:center">
<v-btn smallflat v-show="!showMore" @click="showMore=true">
更多<v-icon>arrow_drop_down</v-icon>
</v-btn>
<v-btn small=""flat v-show="showMore" @click="showMore=false">
收起<v-icon>arrow_drop_up</v-icon>
</v-btn>
</div>
• 对showMore进行判断
<div class="type-wrap" v-for="(f,j) in filters" v-show="j<=4 || showMore" :key="f.k" v-if="f.k!=='brandId'">
<div class="flkey" v-text="f.k==='cid3'?'分类':f.k"></div>
过滤条件筛选
• JavaScript
search.filter是一个对象,结构:
{
"过滤项名":"过滤项值"
}
• created
search.filter=search.filter||{}; //初始化过滤条件
• 绑定点击事件 加在过滤的条件的li上
点击事件传2个参数:
• k:过滤项的key
• option:当前过滤项对象
@click="selectFilter(f.k,o.id||o)"
• methods
selectFilter(key,option){
//this.search.filter[key]=option;
const{...obj}=this.search.filter;
obj[key]=option;
this.search.filter=obj;
},
• search对象中嵌套了filter对象,请求参数格式化时需要进行特殊处理,
修改common.js中的一段代码,把allowDots改为true
var defaults={
allowDots:true,
• 结果
• 后台
• searchRequest ,添加属性
private Map<String,String> filter;
• 要把页面传递的过滤条件也进入进去。
因此不能在使用普通的查询,而是要用到BooleanQuery,基本结构是这样的:
GET /heima/_search
{
"query":{
"bool":{
"must":{ "match": { "title": "小米手机",operator:"and"}},
"filter":{
"range":{"price":{"gt":2000.00,"lt":3800.00}}
}
}
}
}
• searchService 添加过滤条件
//search方法中
QueryBuilder basicQuery = buildBasicQuery(searchRequest);
queryBuilder.withQuery(basicQuery);
• buildBasicQuery
/**
*过滤条件筛选
*@paramsearchRequest
*@return
*/
private QueryBuilderbuild BasicQuery(SearchRequest searchRequest){
//创建bool查询
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
//查询条件
queryBuilder.must(QueryBuilders.matchQuery("all",searchRequest.getKey()));
//过滤条件
Map<String,String>map=searchRequest.getFilter();
for(Map.Entry<String,String>entry:map.entrySet()){
Stringkey=entry.getKey();
//key有两种,分类品牌和规格参数
if(!"cid3".equals(key)&&!"brandId".equals(key)){
key="specs."+key+".keyword";
}
Stringvalue=entry.getValue();
queryBuilder.filter(QueryBuilders.termQuery(key,value));
}
returnqueryBuilder;
}
其他过滤项
• 已选择的过滤项
• 添加事件
<!--已选择过滤项-->
<ul class="tags-choose">
<li class="tag" v-for="(v,k) in search.filter" :key="k">
{{k === 'brandId' ? '品牌' : k}}:<span style="color: red" v-text="findValue(k,v)"></span>
<i class="sui-icon icon-tb-close" @click="deleteFilter(k)"></i>
</li>
</ul>
• JavaScript
findValue(k,v) {
if (!this.filters){
return;
}
if (k !== 'brandId') return v;
return this.filters.find(f => f.k === 'brandId').options[0].name;
},
deleteFilter(k) {
const {... obj} = this.search.filter;
delete obj[k];
this.search.filter = obj;
}
• 隐藏已经选择的过滤项
• 编写一个计算属性,把filters中的 已经被选择的key过滤掉
computed:{
remainFilter() {
//获取已选择的项的key
const keys = Object.keys(this.search.filter);
//完成对已选择的过滤项的过滤
return this.filters.filter(f => !keys.includes(f.k) && f.options.length>1);
}
},
• 页面不再直接遍历filters,而是遍历remainFilter
<div class="type-wrap" v-for="(f,j) in remainFilter"
• 商品分类面包屑
• 用户选择的商品分类就存放在search.filter中,但是里面只有第三级分类的id:cid3
我们需要根据它查询出所有三级分类的id及名称
• controller
/**
*根据3级分类id,查询1~3级的分类
*@paramid
*@return
*/
@GetMapping("all/level")
publicResponseEntity<List<Category>>queryAllByCid3(@RequestParam("id")Longid){
returnResponseEntity.ok(categoryService.queryAllByCid3(id));
}
• service
/**
*根据3级分类id,查询1~3级的分类
*@paramid
*@return
*/
publicList<Category>queryAllByCid3(Longid){
Categoryc3=this.categoryMapper.selectByPrimaryKey(id);
Categoryc2=this.categoryMapper.selectByPrimaryKey(c3.getParentId());
Categoryc1=this.categoryMapper.selectByPrimaryKey(c2.getParentId());
List<Category>list=Arrays.asList(c1,c2,c3);
if(CollectionUtils.isEmpty(list)){
thrownewLyException(ExceptionEnum.CATEGORY_NOT_FIND);
}
returnlist;
}
• 前端
<!--面包屑-->
<ul class="fl sui-breadcrumb">
<li><span>全部结果:</span></li>
<li v-for="(c,i) in breads" :key="i">
<a href="#" v-if="i < 2">{{c.name}}</a>
<span v-else>{{c.name}}</span>
</li>
</ul>
• 在发送到后台的post请求里加
//初始化商品分类过滤参数
if(resp.data.categories.length===1){
//如果只有1个,那么久查询三级商品分类,展示到面包屑
ly.http.get("/item/category/all/level?id="+resp.data.categories[0].id)
.then(resp=>this.breads=resp.data);
}