乐优商城(07)--搜索服务

乐优商城(07)–搜索服务一、索引库数据导入1.1、创建搜索微服务创建modulepom文件<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.
摘要由CSDN通过智能技术生成

乐优商城(07)–搜索服务

一、索引库数据导入

1.1、创建搜索微服务

创建module

在这里插入图片描述

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>myLeyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.search</groupId>
    <artifactId>leyou-search</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--Nacos服务注册-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- web启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- elasticsearch -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-elasticsearch</artifactId>
        </dependency>

        <!-- feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

</project>

application.yaml配置文件

server:
  port: 8083
spring:
  application:
    name: search-service
  elasticsearch:
    rest:
      uris: ip地址:9200
  cloud:
    nacos:
      discovery:
        server-addr: ip地址6:8848
        username: nacos
        password: nacos

启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouSearchApplication {

    public static void main(String[] args) {
        SpringApplication.run(LeyouSearchApplication.class,args);
    }
}

1.2、索引库数据格式分析

目前已有的数据是SKU和SPU,那么如何保存在索引库中?

1.2.1.以结果为导向

先看下搜索页面:

在这里插入图片描述

可以看到,每一个搜索结果都有至少1个商品,当选择大图下方的小图,商品会跟着变化。

因此,搜索的结果是SPU,即多个SKU的集合

既然搜索的结果是SPU,那么索引库中存储的应该也是SPU,但是却需要包含SKU的信息。

1.2.2、需要的数据

在这里插入图片描述

直观能看到的:图片、价格、标题、副标题

暗藏的数据:spu的id,sku的id

另外,页面还有过滤条件:

在这里插入图片描述

这些过滤条件也都需要存储到索引库中,包括:

商品分类、品牌、可用来搜索的规格参数等

综上所述,需要的数据格式有:

spuId、skuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数

1.2.3、最终的数据结构
@Document(indexName = "goods", shards = 1, replicas = 0)
public class Goods {
    @Id
    private Long id; // spuId
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌
    @Field(type = FieldType.Keyword, index = false)
    private String subTitle;// 卖点
    private Long brandId;// 品牌id
    private Long cid1;// 1级分类id
    private Long cid2;// 2级分类id
    private Long cid3;// 3级分类id
    private Date createTime;// 创建时间
    private List<Long> price;// 价格
    @Field(type = FieldType.Keyword, index = false)
    private String skus;// List<sku>信息的json结构
    private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值
    //get和set
}

一些特殊字段解释:

  • all:用来进行全文检索的字段,里面包含标题、商品分类信息

  • price:价格数组,是所有sku的价格集合。方便根据价格进行筛选过滤

  • skus:用于页面展示的sku信息,不索引,不搜索。包含skuId、image、price、title字段

  • specs:所有规格参数的集合。key是参数名,值是参数值。

    例如:在specs中存储 内存:4G,6G,颜色为红色,转为json就是:

    {
        "specs":{
            "内存":[4G,6G],
            "颜色":"红色"
        }
    }
    

    当存储到索引库时,elasticsearch会处理为两个字段:

    • specs.内存:[4G,6G]
    • specs.颜色:红色

    另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合。

    • specs.颜色.keyword:红色

1.3、商品微服务提供接口

索引库中的数据来自于数据库,不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以只能调用商品微服务提供的接口服务。

先确定需要的数据:

  • SPU信息
  • SKU信息
  • SPU的详情
  • 商品分类名称(拼接all字段)

再确定需要哪些服务:

  • 第一:分批查询spu的服务,已有。
  • 第二:根据spuId查询sku的服务,已有
  • 第三:根据spuId查询SpuDetail的服务,已有
  • 第四:根据商品分类id,查询商品分类名称,没有
  • 第五:根据商品品牌id,查询商品的品牌,没有

因此需要额外提供一个查询商品分类名称的接口。

1.3.1.商品分类名称查询

在CategoryController中添加接口:

/**
 * 根据分类id查询分类名称
 * @param ids
 * @return
 */
@GetMapping("/names")
public ResponseEntity<List<String>> queryNamesByIds(@RequestParam("ids") List<Long> ids){
    List<String> names = this.categoryService.queryNamesByIds(ids);
    if (CollectionUtils.isEmpty(names)){
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(names);
}
1.3.2、提供接口

第一步:服务的提供方在leyou-item-interface中提供API接口,即创建api包在里面编写接口声明:

在这里插入图片描述

品牌的接口:

@RequestMapping("/brand")
public interface BrandApi {

    /**
     * 根据品牌id查询品牌
     * @param id
     * @return
     */
    @GetMapping("{id}")
    Brand queryBrandById(@PathVariable("id") Long id);
}

分类的接口:

@RequestMapping("/category")
public interface CategoryApi {

    /**
     * 根据分类id查询分类名称
     * @param ids
     * @return
     */
    @GetMapping("/names")
    List<String> queryNamesByIds(@RequestParam("ids") List<Long> ids);
}

商品接口:

public interface GoodsApi {

    /**
     * 根据查询条件分页并排序查询商品信息
     * @param key
     * @param saleable
     * @param page
     * @param rows
     * @return
     */
    @GetMapping("/spu/page")
    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,
            @RequestParam(value = "sortBy",required = false) String sortBy,
            @RequestParam(value = "desc",required = false) Boolean desc);

    /**
     * 通过 spuId 查询 spuDetail
     * @param spuId
     * @return
     */
    @GetMapping("/spu/detail/{spuId}")
    SpuDetail querySpuDetailBySpuId(@PathVariable("spuId") Long spuId);

    /**
     * 通过 spuId 查询 Sku 集合
     * @param spuId
     * @return
     */
    @GetMapping("/sku/list")
    List<Sku> querySkusBySpuId(@RequestParam("id") Long spuId);
}

商品参数接口:

@RequestMapping("/spec")
public interface SpecificationApi {

    /**
     * 根据 多参数查询具体 SpecParam
     * @param gid
     * @param cid
     * @param generic
     * @param searching
     * @return
     */
    @GetMapping("/params")
    List<SpecParam> queryParams(
            @RequestParam(value = "gid",required = false)Long gid,
            @RequestParam(value = "cid",required = false)Long cid,
            @RequestParam(value = "generic",required = false)Boolean generic,
            @RequestParam(value = "searching",required = false)Boolean searching);
}

需要引入springMVC及leyou-common的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>leyou-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

第二步:在调用方leyou-search中编写FeignClient,但不要写方法声明了,直接继承leyou-item-interface提供的api接口:

在这里插入图片描述

品牌的FeignClient:

@FeignClient(value = "item-service")
public interface BrandClient extends BrandApi {
}

商品分类的FeignClient:

@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {
}

商品的FeignClient:

@FeignClient(value = "item-service")
public interface GoodsClient extends GoodsApi {
}

规格参数的FeignClient:

@FeignClient("item-service")
public interface SpecificationClient extends SpecificationApi {
}

测试

在leyou-search中引入springtest依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

创建测试类:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouSearchApplication.class)
public class CategoryClientTest {

    @Autowired
    private CategoryClient categoryClient;

    @Test
    public void testQueryCategories(){
        List<String> names = this.categoryClient.queryNamesByIds(Arrays.asList(1L, 2L, 3L));
        names.forEach(System.out::println);
    }
}

启动时会报一个错误,说说明bean不等被注册,已经存在

解决方法:

leyou-searchapplication.yaml中添加以下内容:

spring:
  main:
    allow-bean-definition-overriding: true

原因:
springCloud 的2.1.0以上版本的,将不再默认支持 FeignClient 的name属性 的相同名字。
即 :多个接口上的@FeignClient(“相同服务名”)会报错,overriding is disabled(覆盖 是 禁止的/关闭的)。

1.3.3、导入数据

创建GoodsRepository

leyou-searchrepository包下创建GoodsRepository接口

public interface GoodsRepository extends ElasticsearchRepository<Goods,Long> {
}

创建索引

新建一个测试类,在里面进行数据的操作:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouSearchApplication.class)
public class ElasticsearchTest {

    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Test
    public void createIndex(){
        //设置索引信息(绑定实体类)  返回IndexOperations
        IndexOperations indexOperations = this.elasticsearchRestTemplate.indexOps(Goods.class);
        //创建索引库,这里必须得注释掉,原因在于Goods实体类中有注解指明了要创建的索引,若这里再创建会报错
        //indexOperations.create();
        Document mapping = indexOperations.createMapping();
        indexOperations.putMapping(mapping);
    }
}

将数据转换

导入数据其实就是查询数据,然后把查询到的Spu转变为Goods来保存,因此得编写一个SearchService,然后在里面定义一个方法, 把Spu转为Goods

public interface SearchService {

    /**
     * 将 Spu对象转换为 Good对象
     *
     * @param spu
     * @return
     */
    Goods buildGoods(Spu spu) throws IOException;
}

实现类:

@Service
public class SearchServiceImpl implements SearchService {

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SpecificationClient specificationClient;

    @Autowired
    private GoodsRepository goodsRepository;

    //序列化工具
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 将 Spu对象转换为 Good对象
     *
     * @param spu
     * @return
     */
    @Override
    public Goods buildGoods(Spu spu) throws IOException {
        // 创建goods对象
        Goods goods = new Goods();
        // 查询品牌
        Brand brand = this.brandClient.queryBrandById(spu.getBrandId());
        // 查询分类名称
        List<String> names = this.categoryClient.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
        // 查询spu下的所有sku
        List<Sku> skus = this.goodsClient.querySkusBySpuId(spu.getId());
        List<Long> prices = new ArrayList<>();
        List<Map<String, Object>> skuMapList = new ArrayList<>();
        // 遍历skus,获取价格集合
        skus.forEach(sku ->{
            prices.add(sku.getPrice());
            Map<String, Object> skuMap = new HashMap<>();
            skuMap.put("id", sku.getId());
            skuMap.put("title", sku.getTitle());
            skuMap.put("price", sku.getPrice());
            skuMap.put("image", StringUtils.isNotBlank(sku.getImages()) ? StringUtils.split(sku.getImages(), ",")[0] : "");
            skuMapList.add(skuMap);
        });
        // 查询出所有的搜索规格参数
        List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true);
        // 查询spuDetail。获取规格参数值
        SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spu.getId());
        // 获取通用的规格参数
        Map<Long, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<Long, Object>>() {
        });
        // 获取特殊的规格参数
        Map<Long, List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<Object>>>() {
        });
        // 定义map接收{规格参数名,规格参数值}
        Map<String, Object> paramMap = new HashMap<>();
        params.forEach(param -> {
            // 判断是否通用规格参数
            if (param.getGeneric()) {
                // 获取通用规格参数值
                String value = genericSpecMap.get(param.getId()).toString();
                // 判断是否是数值类型
                if (param.getNumeric()){
                    // 如果是数值的话,判断该数值落在那个区间
                    value = chooseSegment(value, param);
                }
                // 把参数名和值放入结果集中
                paramMap.put(param.getName(), value);
            } else {
                paramMap.put(param.getName(), specialSpecMap.get(param.getId()).toString());
            }
        });
        // 设置参数
        goods.setId(spu.getId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setCreateTime(spu.getCreateTime());
        goods.setSubTitle(spu.getSubTitle());
        goods.setAll(spu.getTitle() + brand.getName() + StringUtils.join(names, " "));
        goods.setPrice(prices);
        goods.setSkus(MAPPER.writeValueAsString(skuMapList));
        goods.setSpecs(paramMap);
        return goods;
    }

    /**
     * 判断数值落在那个区间
     * @param value
     * @param param
     * @return
     */
    private String chooseSegment(String value, SpecParam param) {
        double val = NumberUtils.toDouble(value);
        String result = "其他";
        for (String segment : param.getSegments().split(",")) {
            String[] segs = segment.split("-");
            //获取取值范围
            double start = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2){
                end = NumberUtils.toDouble(segs[1]);
            }
            //判断数值在哪个范围
            if (val >= start && val < end){
                if (segs.length == 1){
                    result = segs[0] + param.getUnit() + "以上";
                }else if (start == 0){
                    result = segs[1] + param.getUnit() + "以下";
                }else {
                    result = segment + param.getUnit();
                }
                break;
            }
        }
        return result;
    }
}

然后编写一个测试类,循环查询Spu,然后调用IndexService中的方法,把SPU变为Goods,然后写入索引库:

@Test
public void loadData(){
    int page = 1; //当前页
    int rows = 100;//当前页数据大小
    do {
        // 分批查询spuBo
        PageResult<SpuBo> pageResult = this.goodsClient.querySpuBoByPage("", true, page, rows);
        // 遍历spubo集合转化为List<Goods>
        List<Goods> goodsList = pageResult.getItems().stream().map(spuBo -> {
            try {
                return this.searchService.buildGoods((Spu) spuBo);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }).collect(Collectors.toList());
        this.goodsRepository.saveAll(goodsList);
        // 获取当前页的数据条数,如果是最后一页,没有100条,循环会退出
        rows = pageResult.getItems().size();
        //每次循环页码+1
        page++;
    }while (rows == 100);
}

去kibana查询:可以看到数据成功导入

在这里插入图片描述

二、实现基本搜索

2.1、页面分析

在首页的顶部,有一个输入框:

在这里插入图片描述

当在搜索栏输入任何文本,点击搜索,就会跳转到搜索页search.html了,并且会将搜索关键字以请求参数携带过来:

在这里插入图片描述

2.2、发起异步请求

要想在页面加载后,就展示出搜索结果。应该在页面加载时,获取地址栏请求参数,并发起异步请求,查询后台数据,然后在页面渲染。

在data中定义一个对象,记录请求的参数:

在这里插入图片描述

然后通过钩子函数created,在页面加载时获取请求参数,并记录下来。

created(){
    // 判断是否有请求参数
    if(!location.search){
        return;
    }
    // 将请求参数转为对象
    const search = ly.parse(location.search.substring(1));
    // 记录在data的search对象中
    this.search = search;
    
    // 发起请求,根据条件搜索
    this.loadData();
}

发起请求,搜索数据:

methods: {
    loadData(){
        ly.http.post("/search/page", this.search).then(resp=>{
            console.log(resp);
        });
    }
}
  • 这里使用ly是common.js中定义的工具对象。
  • 这里使用的是post请求,这样可以携带更多参数,并且以json格式发送

leyou-gateway中的CORS配置类中,添加允许信任域名,解决跨域问题:

在这里插入图片描述

并在leyou-gateway工程的application.yml中添加网关映射:

- id: search-router
  uri: http://127.0.0.1:8083
  predicates:
    - Path=/api/search/**
  filters:
    - StripPrefix=1

刷新页面试试:

在这里插入图片描述

2.3、后端实现

SearchController

  • 请求方式:Post

  • 请求路径:/search/page,代表分页查询

  • 请求参数:json格式,目前只有一个属性:key-搜索关键字,但是搜索结果页一定是带有分页查询的,所以将来肯定会有page属性,因此可以用一个对象来接收请求的json数据:

    package com.leyou.search.bo;
    
    public class SearchRequest {
        private String key;// 搜索条件
    
        private Integer page;// 当前页
    
        private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
        private static final Integer DEFAULT_PAGE = 1;// 默认页
    
        public String getKey() {
            return key;
        }
    
        public void setKey(String key) {
            this.key = key;
        }
    
        public Integer getPage() {
            if(page == null){
                return DEFAULT_PAGE;
            }
            // 获取页码时做一些校验,不能小于1
            return Math.max(DEFAULT_PAGE, page);
        }
    
        public void setPage(Integer page) {
            this.page = page;
        }
    
        public Integer getSize() {
            return DEFAULT_SIZE;
        }
    }
    
  • 返回结果:作为分页结果,一般都两个属性:当前页数据、总条数信息,可以使用之前定义的PageResult类

@RestController
@RequestMapping("/search")
public class SearchController {

    @Autowired
    private SearchService searchService;

    /**
     * 搜索商品
     *
     * @param request
     * @return
     */
    @PostMapping("/page")
    public ResponseEntity<PageResult<Goods>> search(@RequestBody SearchRequest request){
        PageResult<Goods> result = this.searchService.search(request);
        if (null == result){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(result);
    }
}

SearchService

/**
 * 搜索商品
 *
 * @param request
 * @return
 */
PageResult<Goods> search(SearchRequest request);

实现类:

/**
 * 搜索商品
 *
 * @param request
 * @return
 */
@Override
public PageResult<Goods> search(SearchRequest request) {
    String key = request.getKey();
    // 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
    if (StringUtils.isBlank(key)) return null;
    // 构建查询条件
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 1、对key进行全文检索查询
    queryBuilder.withQuery(QueryBuilders.matchQuery("all",key).operator(Operator.AND));
    // 2、通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"},null));

    // 3、分页
    // 准备分页参数
    Integer page = request.getPage();
    Integer size = request.getSize();
    queryBuilder.withPageable(PageRequest.of(page - 1,size));
    //Page<Goods> goodsPage = this.goodsRepository.search(queryBuilder.build());
    // 4、查询,获取结果
    SearchHits<Goods> searchHits = this.elasticsearchRestTemplate.search(queryBuilder.build(), Goods.class);
    Page<Goods> goodsPage = this.goodsRepository.search(queryBuilder.build());
    // 封装结果并返回
    return new PageResult<>(goodsPage.getTotalElements(),goodsPage.getTotalPages(),goodsPage.getContent());
}

注意点:要设置SourceFilter,来选择要返回的结果,否则返回一堆没用的数据,影响查询效率。

测试

注意:查询返回的数据是带有空值的,将这些空值去除。

leyou-search的application.yml中添加一行配置,json处理时忽略空值:

spring:
  jackson:
    default-property-inclusion: non_null # 配置json处理时忽略空值

在这里插入图片描述

2.4、页面渲染

保存搜索结果

首先,在data中定义属性,保存搜索的结果:

在这里插入图片描述

loadData的异步查询中,将结果赋值给goodsList

查询出的结果集skus是一个json类型的字符串,不是js对象,在查询成功的回调函数中,对goods进行遍历,把skus转化成对象,并添加一个selected属性保存被选中的sku:

在这里插入图片描述

循环展示商品

在search.html的中部,有一个div,用来展示所有搜索到的商品:

在这里插入图片描述

保留其中一个<li>元素,然后利用vue的循环来展示搜索到的结果:

多sku展示

在这里插入图片描述

这里可以发现,一个商品位置,是多个sku的信息集合。**当用户鼠标选择某个sku,对应的图片、价格、标题会随之改变!**先来实现sku的选择,才能去展示不同sku的数据。

可以看到,在列表中默认第一个是被选中的,那就需要做两件事情:

  • 在搜索到数据时,先默认把第一个sku作为被选中的,记录下来
  • 记录当前被选中的是哪一个sku,记录在哪里比较合适呢?显然是遍历到的goods对象自己内部,因为每一个goods都会有自己的sku信息。

多sku图片列表

在这里插入图片描述

注意:

  • class样式通过 goods.selected的id是否与当前sku的id一致来判断
  • 绑定了鼠标事件,鼠标进入后把当前sku赋值到goods.selected

展示sku其它属性

已经可以通过goods.selected获取用户选中的sku,那么就可以在页面展示了:

在这里插入图片描述

这里在common.js页面的ly对象中添加一个函数:

/**
 * 字符串截取
 * @param val
 * @returns {string}
 */
formatStr(val){
  return val.substr(0,18)+"....";
},

三、页面分页效果

3.1.如何生成分页条

分页数据应该是根据总页数当前页总条数等信息来计算得出。

  • 当前页:肯定是由页面来决定的,点击按钮会切换到对应的页
  • 总页数:需要后台传递给我们
  • 总条数:需要后台传递给我们

首先在data中记录下这几个值:page-当前页,total-总条数,totalPage-总页数

data: {
    ly,
    search:{
        key: "",
        page: 1
    },
    goodsList:[], // 接收搜索得到的结果
    total: 0, // 总条数
    totalPage: 0 // 总页数
}

因为page是搜索条件之一,所以记录在search中。

要注意:在created钩子函数中,会读取url路径的参数,然后赋值给search。如果是第一次请求页面,page是不存在的。因此为了避免page被覆盖,需要做以下修改:

在这里插入图片描述

3.2、页面计算分页条

在这里插入图片描述

思路分析:

  • 最多有5个按钮,因此可以用v-for循环从1到5即可

  • 但是分页条不一定是从1开始:

    • 如果当前页值小于等于3的时候,分页条位置从1开始到5结束
    • 如果总页数小于等于5的时候,分页条位置从1开始到5结束
    • 如果当前页码大于3,应该从page-3开始
    • 但是如果当前页码大于totalPage-3,应该从totalPage-5开始

所以,页面这样来做:

在这里插入图片描述

需要注意的是,如果总页数不足5页,就不应该遍历15,而是1总页数 :Math.min(5,totalPage)

a标签中的分页数字通过index函数来计算,需要把i传递过去:

在这里插入图片描述

分页总代码:

在这里插入图片描述

3.3、翻页事件

接下来编写翻页事件:

prev() {
    if (this.search.page > 1) {
        this.search.page--;
    }
},
next() {
    if (this.search.page < this.totalPage) {
        this.search.page++;
    }
},

当page发生变化,就应该去后台重新查询数据。

不过,如果直接发起ajax请求,那么浏览器的地址栏中是不会有变化的,没有记录下分页信息。如果用户刷新页面,那么就会回到第一页。

这样不太友好,应该把搜索条件记录在地址栏的查询参数中。

因此,需要监听search的变化,然后把search的过滤字段拼接在url路径后:

watch: {
    search: {
        deep: true,
        handler(newVal, oldVal) {
            // 如果旧的search值为空,或者search中的key为空,证明是第一次
            if (!oldVal || !oldVal.key) {
                return;
            }
            // 把search对象变成请求参数,拼接在url路径
            location = "http://www.leyou.com/search.html?" + ly.stringify(this.search);
        }
    }
},

若没有if (!oldVal || !oldVal.key) {return;}这行代码会发生什么?

页面无限刷新!

因为Vue实例初始化的钩子函数中,读取请求参数,赋值给search的时候,会触发了watch监视!也就是说,每次页面创建完成,都会触发watch,然后就会去修改window.location路径,然后页面被刷新,再次触发created钩子,又触发watch,周而复始,无限循环。

页面顶部分页条

在这里插入图片描述

直接在代码中加上点击事件即可:

在这里插入图片描述

四、排序

4.1、页面搜索排序条件

在搜索商品列表的顶部,有以下内容:

在这里插入图片描述

这是用来做排序的,默认按照综合排序。点击新品,应该按照商品创建时间排序,点击价格应该按照价格排序。

排序需要知道两个内容:

  • 排序的字段
  • 排序的方式

因此,首先在search中记录这两个信息,因为created钩子函数会对search进行覆盖,因此在钩子函数中对这两个信息进行初始化即可:
在这里插入图片描述

然后,在页面上给按钮绑定点击事件,修改sortBy和descending的值:

<ul class="sui-nav">
    <li :class="{active:!search.sortBy}" @click="search.sortBy=''">
        <a href="#">综合</a>
    </li>
    <li>
        <a href="#">销量</a>
    </li>
    <li :class="{active: search.sortBy==='createTime'}" @click="search.sortBy='createTime'">
        <a href="#">新品</a>
    </li>
    <li>
        <a href="#">评价</a>
    </li>
    <li :class="{active: search.sortBy==='price'}"
        @click="search.sortBy='price';search.descending = !search.descending ">
        <a href="#">价格
            <v-icon v-show="search.descending">arrow_drop_down</v-icon>
            <v-icon v-show="!search.descending">arrow_drop_up</v-icon>
        </a>
    </li>
</ul>

可以看到,页面请求参数中已经有了排序字段了:

在这里插入图片描述

4.2、后端实现

后台需要接收请求参数中的排序信息,然后在搜索中加入排序的逻辑。

现在的请求参数对象SearchRequest中,只有page、key两个字段。需要进行扩展:(记得添加get和set方法)

在这里插入图片描述

然后在搜索业务逻辑中,添加排序条件:

在这里插入图片描述

注意,因为存储在索引库中的的价格是一个数组:

在这里插入图片描述

因此在按照价格排序时,会进行智能处理:

  • 如果是价格降序,则会把数组中的最大值拿来排序
  • 如果是价格升序,则会把数组中的最小值拿来排序

存在一个问题

比方选中价格降序,显示的商品中有sku的价格是符合降序的,但是默认显示的sku图片却不是符合要求的商品

解决方法:

修改loadData函数,在给goods.selected赋值时不在是把skus中第一个商品赋给它,而是查询符合要求的商品。

loadData() {
    ly.http.post("/search/page", this.search).then(({data}) => {
        data.items.forEach(goods => {
            let max = 0;
            let min = 0;
            //转换skus:把字符串转变为对象
            goods.skus = JSON.parse(goods.skus);
            //添加默认选中项,如果按价格排序则选出skus中价格最低的或者最高的,否则选skus中的第一个
            if (this.search.sortBy === "price"){
                if (this.search.descending === true){
                    //降序,则skus中价格选最高的
                    goods.skus.forEach(sku => {
                        if (sku.price > max){
                            max = sku.price;
                        }
                    });
                    goods.skus.forEach(sku => {
                        if (sku.price === max){
                            goods.selected = sku;
                        }
                    });
                } else {
                    //升序,则skus中价格选最低的
                    min = goods.skus[0].price;
                    goods.skus.forEach(sku => {
                        if (sku.price < min){
                            min = sku.price;
                        }
                    });
                    goods.skus.forEach(sku => {
                        if (sku.price === min){
                            goods.selected = sku;
                        }
                    });
                }
            } else {
                goods.selected = goods.skus[0];
            }
        });
        this.goodsList = data.items;
        this.totalPage = data.totalPage;
        this.total = data.total;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值