乐优商城day12(商品搜索页面完全实现)

所有代码发布在 [https://github.com/hades0525/leyou]

Day12
2019年2月9日
23:42
商品实现分页翻页

  1. 分页条
<!--分页条-->
<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}}页&nbsp;</span><span>
  1. 上端搜索结果
<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">&lt;</a>
<aclass="btn-arrow"href="#"style="display:inline-block"@click.prevent="nextPage">&gt;</a>
</div>
  1. 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++
}
},
  1. 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;
}
}
  1. 提供查询接口
•	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;
}
}
  1. 结果

  2. 分类品牌页面渲染
    • 分类、品牌内容都不太一样,但是结构相似,都是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;
}
  1. 结果

  2. 规格参数页面渲染
    • 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);
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值