Skip to content

搜索页面

全局过滤器

  1. 创建filter文件夹:src->filters->index.js
js
// 定义一个过滤器
const filters = {
    // 时间过滤器
    date(t){
        const timer = new Date(t);
        return timer.getFullYear() + "-" +
            (timer.getMonth() + 1).toString().padStart(2, 0) + "-" +
            timer.getDate().toString().padStart(2, 0) + " " +
            timer.getHours().toString().padStart(2, 0) + ":" +
            timer.getMinutes().toString().padStart(2, 0) + ":" +
            timer.getSeconds().toString().padStart(2, 0);
    },
    // 金额过滤器
    currency(v, n = 2, type = "$") {
        return type + v.toFixed(n);
    }
}
// 暴漏一个函数
export default function(V){
    for(let key in filters){
        V.filter(key,filters[key]);
    }
}
  1. 在入口文件引入 src->main.js
js
// 导入filters
import filters from "@/filters"

Vue.config.productionTip = false
Vue.use(filters);
  1. 应用过滤器 src->pages->Home->RankList->index.vue
vue
<template lang="">
    <div>
        ...
         <div class="tab-info">
             <div class="info-title">
                <a href="#">{{item.name}}</a>
    		</div>
            <p class="info-price">定金:{{info.price|currency(2,"¥")}}</p>
    	</div>
        ...
    </div>
</template>
<script>
export default {
    
}
</script>
<style lang="">
    
</style>

node实现后端

  1. 安装express,编辑server.js
js
npm install express
js
const express = require("express");
const likeData = require("./data/likeList.json");
const app = express();
// 请求方式:get
// 请求地址:http://zhangpeiyue.com:8082/likeList
// 请求参数:query
//     pageSize:每页显示的条数
//     pageNo:页码
// 响应结果:
// {
//      ok:1,
//      msg:"success",
//      likeList:[],// 喜欢列表
//      pageSum:1// 总页数
// }
app.get("/likeList",(req,res)=>{
	/*
	* 1- 接收参数
	* 2- 根据参数获取数据
	* 3- 响应数据*/
	let {pageSize=6,pageNo=1} = req.query;
	pageNo = pageNo/1;
	pageSize = pageSize/1;
	// pageNo===>1
	res.json({
		ok:1,
		msg:"success",
		likeList:likeData.slice((pageNo-1)*pageSize,pageSize*pageNo),
		pageSum:Math.ceil(likeData.length/pageSize)
		// 1
		// likeList:likeData.slice(0,6)
		// likeList:likeData.slice(6,12)
	})
})
app.listen(9090,()=>{
	console.log("success");
})
  1. 配置data数据:api->data->likeList.json
json
[
    {
        "id": 1,
        "title": "大希地牛排 母后恩点儿童牛排 整切牛排 果蔬腌制宝宝",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/192352/27/35184/246085/64b8f357Fba2db77b/18c1c62aa5a6ff1d.jpg",
        "price": 89
    },
    {
        "id": 2,
        "title": "大希地牛排 母后恩点儿童牛排 整切牛排 果蔬腌制宝宝",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/192352/27/35184/246085/64b8f357Fba2db77b/18c1c62aa5a6ff1d.jpg",
        "price": 89
    },
    {
        "id": 3,
        "title": "大希地牛排 母后恩点儿童牛排 整切牛排 果蔬腌制宝宝",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/192352/27/35184/246085/64b8f357Fba2db77b/18c1c62aa5a6ff1d.jpg",
        "price": 89
    },
    {
        "id": 4,
        "title": "大希地牛排 母后恩点儿童牛排 整切牛排 果蔬腌制宝宝",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/192352/27/35184/246085/64b8f357Fba2db77b/18c1c62aa5a6ff1d.jpg",
        "price": 89
    },
    {
        "id": 5,
        "title": "大希地牛排 母后恩点儿童牛排 整切牛排 果蔬腌制宝宝",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/192352/27/35184/246085/64b8f357Fba2db77b/18c1c62aa5a6ff1d.jpg",
        "price": 89
    },
    {
        "id": 6,
        "title": "大希地牛排 母后恩点儿童牛排 整切牛排 果蔬腌制宝宝",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/192352/27/35184/246085/64b8f357Fba2db77b/18c1c62aa5a6ff1d.jpg",
        "price": 89
    },
    {
        "id": 7,
        "title": "食者道鸡排 鸡胸肉鸡扒 低脂高蛋白 奥尔良黑椒孜然3",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/91472/39/33627/443660/64bf3963F60ccd12b/42cb86098908ec3c.jpg",
        "price": 98
    },
    {
        "id": 8,
        "title": "食者道鸡排 鸡胸肉鸡扒 低脂高蛋白 奥尔良黑椒孜然3",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/91472/39/33627/443660/64bf3963F60ccd12b/42cb86098908ec3c.jpg",
        "price": 98
    },
    {
        "id": 9,
        "title": "食者道鸡排 鸡胸肉鸡扒 低脂高蛋白 奥尔良黑椒孜然3",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/91472/39/33627/443660/64bf3963F60ccd12b/42cb86098908ec3c.jpg",
        "price": 98
    },
    {
        "id": 10,
        "title": "食者道鸡排 鸡胸肉鸡扒 低脂高蛋白 奥尔良黑椒孜然3",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/91472/39/33627/443660/64bf3963F60ccd12b/42cb86098908ec3c.jpg",
        "price": 98
    },
    {
        "id": 11,
        "title": "食者道鸡排 鸡胸肉鸡扒 低脂高蛋白 奥尔良黑椒孜然3",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/91472/39/33627/443660/64bf3963F60ccd12b/42cb86098908ec3c.jpg",
        "price": 98
    },
    {
        "id": 12,
        "title": "食者道鸡排 鸡胸肉鸡扒 低脂高蛋白 奥尔良黑椒孜然3",
        "imgUrl": "https://img10.360buyimg.com/n3/jfs/t1/91472/39/33627/443660/64bf3963F60ccd12b/42cb86098908ec3c.jpg",
        "price": 98
    }
]
  1. 启动node
js
node start server.js
// 测试访问
http://127.0.0.1:9090/likeList?pageNo=1&pageSize=5

猜你喜欢

  1. 配置代理,解决跨域问题。src->vue.config.js
js
proxy: {
            // 请求地址:http://sph-h5-api.atguigu.cn/api/product/getBaseCategoryList
            // 访问"/api"的路径,统一拼接为"http://sph-h5-api.atguigu.cn/api"
            "/api": {
                target: "http://sph-h5-api.atguigu.cn",
                changeOrigin: true
            },
            // 访问"/hanser"的路径,统一拼接为"http://127.0.0.1:9090/hanser"
            "/hanser":{
                target: "http://127.0.0.1:9090",
                changeOrigin:true,
                pathRewrite:{
                    // http://127.0.0.1:9090 == http://127.0.0.1:9090/hanser
                    "^/hanser":""
                }
            }
        }
  1. 封装request,nodeRequest。src->request->nodeRequest.js
js
import axios from "axios";
import nprogress from "nprogress";
import "nprogress/nprogress.css";

// 创建一个实例
const nodeRequest = axios.create({
    baseURL: "/hanser",
    timeout: 5000
});
// 请求拦截器
nodeRequest.interceptors.request.use(config => {
    nprogress.start();// 开启进度条
    return config;
});
// 响应拦截器
nodeRequest.interceptors.response.use(response => {
    nprogress.done();// 结束进度条
    return response.data;// 返回响应体
}, err => {
    nprogress.done();// 结束进度条
    alert(err);//提示错误信息
    return new Promise(() => { });//中断Promise
})
// 暴漏数据
export default nodeRequest;
  1. 封装api getLikeList接口 src->api->product.js
js
import { sphRequest, mockRequest ,nodeRequest} from "@/request";
...
// 获取猜你喜欢数据
const getLikeList = ()=>{
    return nodeRequest("/hanser/likeList")
}
...
  1. 创建store likeList数据仓库 src->store->product->index.js
js
// 定义商品的数据状态
const state = {
    // 首页分类列表
    categoryList:[],
    // 楼层数据状态
    floorList:[],
    // rank的数据仓库
    rankList:[],
    // 猜你喜欢
    likeList:[]
}
// 定义mutations
const mutations = {
    // 修改state中的首页分类列表
    UP_CATEGORY_LIST(state,categoryList){
        state.categoryList = categoryList
    },
    // 修改state中的楼层列表
    SAVE_FLOOR_LIST(state, floorList) {
        state.floorList = floorList;
    },
    // 修改rank中的数据
    SAVE_RANK_LIST(state, rankList) {
        state.rankList = rankList;
    },
    // 修改like中的数据
    SAVE_LIKE_LIST(state, likeList) {
        state.likeList = likeList;
    },
    // 修改pageSum的数据
    SAVE_SUM(state, pageSum){
        state.pageSum = pageSum;
    }
}
// 定义actions
const actions = {
    // 使用api接口获取数据
    async getBaseCategoryListAsync({commit},num=1){
        const { data } = await getBaseCategoryList();
        commit("UP_CATEGORY_LIST",data.splice(0,num));
    },
    // FloorList获取数据
    async getFloorListAsync({ commit }) {
        const { data } = await getFloorList();
        commit("SAVE_FLOOR_LIST", data);
    },
    // 使用api获取rankList说几句
    async getRankListAsync({ commit }) {
        const { data } = await getRankList();
        commit("SAVE_RANK_LIST", data);
    },
    // 使用api获取likeList说几句
    async getLikeListAsync({ commit },pageNo,pageSize=6) {
        try {
            const { data , pageSum} = await getLikeList(pageNo,pageSize);
            commit("SAVE_LIKE_LIST", data);
            commit("SAVE_SUM", pageSum);
        } catch (error) {
            console.error("Failed to fetch like list:", error);
        }
    }
}
  1. 抽离出猜你喜欢 组件 src->pages->Home->like->index.vue
vue
<template lang="">
    <div class="like">
        <div class="py-container">
            <div class="title">
                <h3 class="fl">猜你喜欢</h3>
                <a href="javascript:;" @click="changeLike" class="fr tip changeBnt">换一换</a>
            </div>
            <div class="bd">
                <ul class="favourate">
                    <li v-for="(item,index) in likeList" :key="index">
                        <img :src="item.imgUrl" alt="" />
                        <div class="like-text">
                            <p>{{item.title}}</p>
                            <h3>{{item.price | currency(2,"¥")}}</h3>
                        </div>
                    </li>
                </ul>
            </div>
        </div>
    </div>
</template>
<script>
import {mapState,mapActions} from "vuex";
export default {
    name:"Like",
    computed:{
        ...mapState("product", ["likeList","pageSum"])
    },
    data(){
        return{
            pageSize:3,
            pageNo:1,
        }
    },
    mounted(){
        this.getLikeListAsync(this.pageNo,this.pageSize);
    },
    methods: {
        ...mapActions("product", ["getLikeListAsync"]),
        // changeLike方法
        changeLike(){
            this.pageNo++;
            if (this.pageNo > this.pageSum){
                this.pageNo = 1;
            }
            this.getLikeListAsync(this.pageNo, this.pageSize);
        }
    },
}
</script>

获取分类名称和ID

  1. 根据后端接口信息获取分类名称和ID
js
// categoryName:分类的名称
// category1Id:一级分类ID
// category2Id:二级分类ID
// category3Id:三级分类ID
  1. 修改导航组件 src->components->Header->TypeNav->index.vue
vue
通过在标签上设置自定义属性 data-level 保存并记录 点击的元素是 哪个分类级别
<template>
<div @click="goSearch" class="all-sort-list2">
    <div v-for="c1 in categoryList" :key="c1.categoryId" class="item">
        <h3>
            <a data-level="1" > {{ c1.categoryName }}</a>
        </h3>
        <div class="item-list clearfix">
            <div v-for="c2 in c1.categoryChild" :key="c2.categoryId" class="subitem">
                <dl class="fore">
                    <dt>
                        <a data-level="2" href="">{{ c2.categoryName }}</a>
                    </dt>
                    <dd>
                        <em v-for="c3 in c2.categoryChild" :key="c3.categoryId">
                            <a data-level="3" href="">{{ c3.categoryName }}</a>
                        </em>
                    </dd>
                </dl>
            </div>
        </div>
    </div>
</div>
</template>
<script>
export default {
    // 点击分类跳转
    goSearch(e){
        const {id,level} = e.target.dataset;
        // 如果id存在,点击后路由到/search并且拼接查询字符串
        if(id){
            this.$router.push({
                path:"/search",
                query:{
                    // category2Id:12
                    ["category"+level+"Id"]:id,
                    categoryName:e.target.innerText.trim()
                }
            })
            // 点击后 分类页隐藏
            this.isShowCategory = false;
        }
    },
}
</script>
  1. Search页面接收参数:src->pages->Search->index.vue
vue
<template>
    <div>
        <h3>搜索界面,接收的参数:{{$route.query}}</h3>
    </div>
</template>
<script>
export default {
    name: "Search",
}
</script>
<style lang="less" scoped>
h3 {
    padding: 50px;
    background: yellow;
}
</style>

获取搜索框中内容

  1. 在Header组件中修改:src->components->Header->index.vue
js
<template lang="">
    <div>
		...
		<div class="searchArea">
            <form action="###" class="searchForm">
                <input placeholder="请输词" ref="keyword" type="text"class="input-error "/>
                <button @click="goSearch" class="sui-btn" type="button">搜索</button>
             </form>
         </div>	
		...
    </div>
</template>
<script>
export default {
    methods: {
        // 搜索跳转方法
        goSearch() {
            const keyword = this.$refs.keyword.value.trim();
            // 判断关键字是否有值,
            if(keyword){
                this.$router.push({
                    path:"/search",
                    query:{
                        keyword,
                    }
                })
            }
        }
    },
    mounted(){
        // 判断查询字符串中是否有keyword,如果有将其值设置为搜索框的内容。
        if (this.$route.query.keyword)
            this.$refs.keyword.value = this.$route.query.keyword;
    }
}
</script>
<style lang="less">
    
</style>

合并类别和关键词搜索

  1. 在搜索页面把 查询字符串拼接上 src->components->Header->index.vue
js
goSearch(){
    // 收集输入的关键词
    const keyword = this.$refs.keyword.value.trim();
    // 判断关键词是否有值,如果有值跳转至搜索界面,并将关键词进行传递
    if(keyword)
        this.$router.push({
            path:"/search",
            query:{
                ...this.$route.query,
                keyword
            }
        });
}
  1. 在头部组件中的分类页面中把搜索的关键字拼接上 src->components->Header->TypeNav->index.vue
js
// 点击分类跳转
goSearch(e){
    const {id,level} = e.target.dataset;
    // 如果id存在,点击后路由到/search并且拼接查询字符串
    if(id){
        this.$router.push({
            path:"/search",
            query:{
                // category2Id:12
                ...this.$route.query,
                ["category"+level+"Id"]:id,
                categoryName:e.target.innerText.trim()
            }
        })
        // 点击后 分类页隐藏
        this.isShowCategory = false;
    }
},

调用接口获取搜索数据

  1. 调用商品接口获取数据 src->api->product.js
js
// 搜索获取数据
const postProductList = function(body){
    return sphRequest.post("/list",body);
}
// 暴漏数据
export { 
    getBaseCategoryList,
    getFloorList,
    getRankList,
    getLikeList,
    postProductList
}
  1. 封装store仓库 src->store->product.js
js
// 导入api
import { getBaseCategoryList, getFloorList, getRankList, getLikeList, postProductList } from '@/api/product';

// 定义商品的数据状态
const state = {
    // 首页分类列表
    categoryList:[],
    // 楼层数据状态
    floorList:[],
    // rank的数据仓库
    rankList:[],
    // 猜你喜欢
    likeList:[],
    pageSum:0,
    // 搜索数据
    searchResult:{},
}
// 定义mutations
const mutations = {
    // 修改state中的首页分类列表
    UP_CATEGORY_LIST(state,categoryList){
        state.categoryList = categoryList
    },
    // 修改state中的楼层列表
    SAVE_FLOOR_LIST(state, floorList) {
        state.floorList = floorList;
    },
    // 修改rank中的数据
    SAVE_RANK_LIST(state, rankList) {
        state.rankList = rankList;
    },
    // 修改like中的数据
    SAVE_LIKE_LIST(state, likeList) {
        state.likeList = likeList;
    },
    // 修改pageSum的数据
    SAVE_SUM(state, pageSum){
        state.pageSum = pageSum;
    },
    // 保存搜索结果
    SAVE_SEARCH_RESULT(state, result) {
        state.searchResult = result;
    },
}
// 定义actions
const actions = {
    // 使用api接口获取数据
    async getBaseCategoryListAsync({commit},num=1){
        const { data } = await getBaseCategoryList();
        commit("UP_CATEGORY_LIST",data.splice(0,num));
    },
    // FloorList获取数据
    async getFloorListAsync({ commit }) {
        const { data } = await getFloorList();
        commit("SAVE_FLOOR_LIST", data);
    },
    // 使用api获取rankList说几句
    async getRankListAsync({ commit }) {
        const { data } = await getRankList();
        commit("SAVE_RANK_LIST", data);
    },
    // 使用api获取likeList说几句
    async getLikeListAsync({ commit },pageNo,pageSize=6) {
        try {
            const { data , pageSum} = await getLikeList(pageNo,pageSize);
            commit("SAVE_LIKE_LIST", data);
            commit("SAVE_SUM", pageSum);
        } catch (error) {
            console.error("Failed to fetch like list:", error);
        }
    },
    // 使用api获取搜索的数据
    async postProductListAsync({ commit }, body) {
        const { data } = await postProductList(body);
        commit("SAVE_SEARCH_RESULT", data);
    }
}
...
  1. 抽离出搜索页面,并且获取商品数据 src->pages->Search->index.vue
vue
<template lang="">
...
</template>
<script>
import {mapState} from "vuex";
export default {
    name: "Search",
    // 使用侦听器
    watch:{
        "$route.query":{
            handler(query){
                this.$store.dispatch("product/postProductListAsync",query)
            },
            immediate:true
        }
    },
    computed:{
        ...mapState("product", ["searchResult"])
    },
    mounted(){
        
    }
}
</script>
<style lang="less" scoped>
h3 {
    padding: 50px;
    background: yellow;
}

</style>

渲染搜索商品数据

  1. 渲染商品列表数据
vue
<template lang="">
    <div>
<div class="goods-list">
    <ul class="yui3-g">
        <li  v-for="(item,index) in searchResult.goodsList" :key="index" class="yui3-u-1-5">
            <div class="list-wrap">
                <div class="p-img">
                    <a href="item.html"  target="_blank"><img :src="item.defaultImg" /></a>
                </div>
                <div class="price">
                    <strong>
                        <em>¥</em>
                        <i>{{item.price}}</i>
                    </strong>
                </div>
                <div class="attr">
                    <a target="_blank" href=""  :title="item.title">{{item.title}}</a>
                </div>
                <div class="commit">
                    <i class="command">已有<span>{{item.hotScore}}</span>人评价</i>
                </div>
                <div class="operate">
                    <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
                    <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
                </div>
            </div>
        </li>
    </ul>
</div>
    </div>
</template>
  1. 解决搜索时 1级分类和3级分类冲突的情况 src->components->Header->TypeNav->index.vue

由于之前的程序会与this.$route.query进行合并,会导致将上一次搜索的类型保留,导致类型混乱,所以对程序进行更新。

问题:比如上一次搜索的是:category1Id=100

如果我点击的是三级分类id为3,那么实际传递过云的数据category1Id=100&category3Id=3

解决问题后:category3Id=3

js
goSearch(e){
    const {id,level} = e.target.dataset;
    if(id){
        this.$router.push({
            path:"/search",
            query:{
                ["category"+level+"Id"]:id,
                categoryName:e.target.innerText.trim(),
                // 修改此处
                keyword:this.$route.query.keyword
            }
        });
        this.isShow = false;
    }

}

SearchSeletor渲染

  1. 把搜索选择框抽离出来: src->pages->Search->SearchSelector->index.vue
vue
<template lang="">
    <div>

    </div>
</template>
<script>
export default {
    
}
</script>
<style lang="">
    
</style>
  1. 同时在父组件中引入:src->pages->Search->index.vue
vue
<template lang="">
    <div>

    </div>
</template>
<script>
import SearchSelector from "@/pages/Search/SearchSelector"    
export default {
    computed:{
        ...mapState("product", ["searchResult"])
    },
}
</script>
<style lang="">
    
</style>
  1. 在searchSelector组件中完成数据获取,并渲染页面 src->pages->Search->SearchSelector->index.vue
vue
<template lang="">
    <div>
        <div class="clearfix selector">
            <div class="type-wrap logo">
                <div class="fl key brand">品牌</div>
                <div class="value logos">
                    <ul class="logo-list">
                        <li v-for="item in trademarkList" :key="item.tmId">{{item.tmName}}</li>
                    </ul>
                </div>
            </div>
            <div class="type-wrap" v-for="item in attrsList" :key="item.attrId">
                <div class="fl key">{{item.attrName}}</div>
                <div class="fl value">
                    <ul class="type-list">
                        <li v-for="(info,index) in item.attrValueList" :key="index">
                            <a>{{info}}</a>
                        </li>
                    </ul>
                </div>
                <div class="fl ext"></div>
            </div>
        </div>
    </div>
</template>
<script>
import {mapState} from "vuex";
export default {
    name:"SearchSelector",
    data(){
        return{

        }
    },
    computed:{
        ...mapState("product",{
            trademarkList:function(state) {
                // 过滤一次,过滤没有名字的元素
                return state.searchResult.trademarkList;
            },
            attrsList(state){
                return state.searchResult.attrsList;
            }
        })
    },

}
</script>
<style lang="less" scoped>
.selector {
    
}
</style>

面包屑导航

类别添加以及移除

  1. 修改搜索页面 : src->pages->Search->index.vue
vue
<template lang="">
    <div>
		<!--bread-->
            <div class="bread">
                <ul class="fl sui-breadcrumb">
                    <li>
                        <a href="#">全部结果</a>
                    </li>
                </ul>
                <ul class="fl sui-tag">
                    <!-- 面包屑 类别添加以及移除 -->
                    <li v-if="$route.query.categoryName" class="with-x">
                        {{$route.query.categoryName}}<i @click="moveCategoryName">×</i>
                    </li>
                </ul>
            </div>
    </div>
</template>
<script>
export default {
    methods:{
        moveCategoryName(){
            // category3Id=249&categoryName=台灯
            // 获取搜索的关键词
            const { keyword } = this.$route.query;
            this.$router.push({
                path:"/search",
                query:{
                    keyword
                }
            })
        }
    }
}
</script>
<style lang="">
    
</style>

关键字添加以及移除

  1. 修改搜索页面:src->pages->Search->index.vue
vue
<template lang="">
    <div>
		<div class="bread">
                <ul class="fl sui-breadcrumb">
                    <li>
                        <a href="#">全部结果</a>
                    </li>
                </ul>
                <ul class="fl sui-tag">
                    <!-- 面包屑 类别添加以及移除 -->
                    <li v-if="$route.query.categoryName" class="with-x">
                        {{$route.query.categoryName}}<i @click="moveCategoryName">×</i>
                    </li>
                    <!--  关键词内容 -->
                    <li v-if="$route.query.keyword" class="with-x">
                        {{$route.query.keyword}}<i @click="moveKeyword">×</i>
                    </li>
                </ul>
            </div>
    </div>
</template>
<script>
export default {
    methods:{
        moveCategoryName(){
            // category3Id=249&categoryName=台灯
            // 获取搜索的关键词
            const { keyword } = this.$route.query;
            this.$router.push({
                path:"/search",
                query:{
                    keyword
                }
            })
        },
        moveKeyword(){
            // 解构出把 query进行浅拷贝
            const query = {...this.$route.query};
            // 删除query中的keyword属性
            delete query.keyword;
            // 路由到指定地址
            this.$router.push({
                path: "/search",
                query
            })
        }
    }
}
</script>
<style lang="">
    ...
</style>

通过事件总线清空搜索栏

  1. 在入口文件生成$bus src->index.js
js
new Vue({
    store,
    router,
    beforeCreate() {
        // 设置事件总线
        Vue.prototype.$bus = this;
    },
    render: h => h(App),
}).$mount('#app')
  1. 在搜索按钮的页面设置监听事件 :src->components->Header->index.vue
vue
<template lang="">
    <div>

    </div>
</template>
<script>
export default {
    mounted(){
    this.$bus.$on("clearKeyword",()=>{
        this.$refs.keyword.value = null;
    })
}
}
</script>
<style lang="">
    
</style>
  1. 在离开搜索页面的时候触发监听事件 clearKeyword :src->pages->Search->index.vue
js
beforeDestroy() {
    this.$bus.$emit("clearKeyword");
}
// 同时清空关键词标签后 清空搜索框内容
moveKeyword(){
    const query = {...this.$route.query};
    delete query.keyword;
    this.$router.push({
        path:"/search",
        query
    });
    this.$bus.$emit("clearKeyword");
}

按照品牌进行搜索

  1. 修改SearchSelector页面:src->pages->Search->SearchSelector->index.vue
vue
<template>
    <div class="clearfix selector">
        <div class="type-wrap logo">
            <div class="fl key brand">品牌</div>
            <div class="value logos">
                <ul class="logo-list">
                    <li @click="$router.push({
                        path:'/search',
                        query:{
                            ...$route.query,
                            trademark:item.tmId+':'+item.tmName
                        }
                    })" v-for="item in trademarkList" :key="item.tmId">
                        {{item.tmName}}
                    </li>
                </ul>
            </div>
        </div>
        ....
    </div>
</template>

无搜索条件隐藏面包屑导航

  1. 修改页面:src->pages->Search->index.vue
vue
<template>
.....
          <!--bread-->
            <div class="bread" v-show="isSelector">
                .....
            </div>
.....
</template>

<script>
export default {
    .....
    computed:{
        isSelector(){
            return Object.values(this.$route.query).filter(v=>v).length>0;
        }
    }
    ....
}
</script>

面包屑导航中渲染品牌和移除品牌

  1. 修改页面:src->pages->Search->index.vue
vue
<template lang="">
    <div>
		<div class="bread" v-show="isSelector">
                <ul class="fl sui-breadcrumb">
                    <li>
                        <a href="#">全部结果</a>
                    </li>
                </ul>
                <ul class="fl sui-tag">
                    <!-- 面包屑 类别添加以及移除 -->
                    <li v-if="$route.query.categoryName" class="with-x">
                        {{$route.query.categoryName}}<i @click="moveCategoryName">×</i>
                    </li>
                    <!--  关键词内容 -->
                    <li v-if="$route.query.keyword" class="with-x">
                        {{$route.query.keyword}}<i @click="moveKeyword">×</i>
                    </li>
                    <!--  品牌内容 -->
                    <li v-if="$route.query.trademark" class="with-x">
                        {{$route.query.trademark|trademark}}<i @click="moveTrademark">×</i>
                    </li>
                </ul>
         </div>
    </div>
</template>
<script>
export default {
    moveTrademark(){
        const query = { ...this.$route.query };
        // 删除query中的keyword属性
        delete query.trademark;
        // 路由到指定地址
        this.$router.push({
            path: "/search",
            query
        })
	}
}
</script>
<style lang="">
    ...
</style>

根据商品属性进行过滤搜索

  1. 后端的api接口
js
* 请求地址:http://zhangpeiyue.com/search?a=[1,2,3,4]
* 后端接收到的query this.$route.query ===> {a:"[1,2,3,4]"}

* 请求地址:http://zhangpeiyue.com/search?a=1&a=2&a=3&a=4
* 后端接收到的query this.$route.query ===> {a:['1', '2', '3', '4']}

特殊情况:当数组的元素只有一位时,会被浏览器识别为字符串
this.$route.query ===> {a:"1"} http://zhangpeiyue.com/search?a=1

使用router-link以及$router.push传递参数:
请求地址:http://zhangpeiyue.com/search?a=1&a=2&a=3&a=4
<router-link to="/search?a=1&a=2&a=3&a=4" class="register">搜索1</router-link>
// 方式1
<router-link :to="{
                  path:'/search',
                  query:{
                  	a:[1,2,3,4]
                  }
                  }" class="register">搜索2</router-link>
// 方式2
<a @click.prevent="$router.push({
                   path:'/search',
                   query:{
                       a:[1,2,3,4]
                   }
                   })" href="">搜索3</a>
  1. 点击相关属性后,获取后台数据:src->pages->Search->SearchSelector->index.vue
vue
<template lang="">
    <div>
        ...
		<ul class="type-list">
            <li v-for="(info,index) in item.attrValueList" :key="index">
                <a @click="addPropsGoSearch(item.attrId,info,item.attrName)">{{info}}</a>
    		</li>
    	</ul>
        ...
    </div>
</template>
<script>
export default {
    methods:{
        // attrId-->属性ID
        // info---->属性值
        // attrName--->属性名"
        addPropsGoSearch(attrId,info,attrName){
            // 添加的属性值
            const propsValue = attrId+':'+info+':'+attrName;
            // 判断props是否有值,如果无值,那么让其值为[]
            const props = this.$route.query.props || [];
            // 如果拥有该属性值,那么程序停止 。
            if(props.includes(propsValue)) return;
            this.$router.push({
                path:'/search',
                query:{
                    ...this.$route.query,
                    props:[...props,propsValue]
                }
            })
        }
    }
}
</script>
<style lang="">
    
</style>

面包屑渲染属性以及移除属性

  1. 面包屑渲染过滤属性:src->pages->Search->->index.vue
vue
<template lang="">
    ...
    <!--bread-->
    <div class="bread" v-show="isSelector">
        <ul class="fl sui-breadcrumb">
            <li>
                <a href="#">全部结果</a>
            </li>
        </ul>
        <ul class="fl sui-tag">
            <!-- 属性内容显示 -->
            <li v-for="(item,index) in $route.query.props" :key="index" class="with-x">
                {{item.split(":")[1]}}<i @click="moveProps(item)">×</i>
            </li>
        </ul>
    </div>
	...
</template>
<script>
export default {
	methods:{
        // 移除属性过滤
        moveProps(item){
            // 跳转到指定路由
            this.$router.push({
                path: "/search",
                query:{
                    // 把之前的query拼接上
                    ...this.$route.query,
                    // 使用过滤器把props过滤后的拼接上
                    // 把不相等的返回流下来,相等的去除。
                    props: this.$route.query.props.filter(value =>{ return value !== item})
                }
            })
        }
	}
}
</script>
<style lang="">
    
</style>

引用阿里图标库资源

  1. 下载阿里图标
  2. 把图标资源放在:src->assets->iconfont
  3. 在入口文件引入样式:src->index.js
js
import "@/assets/iconfont/iconfont.css";
  1. 在对应的页面使用样式:src->pages->Search->index.vue
vue
<template lang="">
<div class="sui-navbar">
    <div class="navbar-inner filter">
        <ul class="sui-nav">
            <li class="active">
                <a href="#">综合<i class="iconfont icon-icon_up"></i></a>
            </li>
            <li>
                <a href="#">价格<i class="iconfont icon-icon_down"></i></a>
            </li>
        </ul>
    </div>
</div>
</template>
<script>
export default {
    
}
</script>
<style lang="">
    
</style>

实现商品排序

  1. 结合后端api数据
js
"order",this.$route.query.order
// 排序方式
// 排序类型(type)   1: 综合     2: 价格
// 排序标识(flag)  	asc: 升序  desc: 降序
// 示例: "1:desc"
  1. 修改src->pages->Search->index.vue
vue
<template lang="">
<div class="sui-navbar">
    <div class="navbar-inner filter">
        <ul class="sui-nav">
            <li @click.prevent="orderSearch(1)" :class="{active:type/1===1}">
                <a href="#">综合<i v-show="type==1" :class="upOrDown"></i></a>
    		</li>
            <li @click.prevent="orderSearch(2)" :class="{active:type/1===2}">
                <a href="#">价格<i v-show="type==2" :class="upOrDown"></i></a>
    		</li>
    	</ul>
    </div>
</div>
</template>
<script>
export default {
    computed:{
        ...mapState("product", ["searchResult"]),
        // 判断隐藏面包屑的方法
        isSelector() {
            // Object.values 返回由对象的属性值组成的数组,把对象的属性值进行过滤,
            // 过滤出 undefined的,如果length>0返回真
            // true 代表存在 有查询
            return Object.values(this.$route.query).filter(v => v).length > 0;
        },
        // 改变排序图标的方法 
        upOrDown() {
            return this.flag === "desc" ? "iconfont icon-paixu" : "iconfont icon-xiangshang"
        },
    },
    methods:{
        ...
        // 排序搜索
        orderSearch(type){
            // 如果选中的类别与当前的类别相同,则改变排序方式
            if(this.type === type){
                // 当是降序排序,则改变为升序排序
                this.flag = this.flag==="desc"?"asc":"desc";

            }else{
                // 如果排序类别不相同,则进行改变 type类型,并设置flag默认排序方式
                this.type = type;
                this.flag = "desc";
            }
            // 最后都要进行路由跳转
            this.$router.push({
                path: "/search",
                query: {
                    // 拼接上之前的query
                    ...this.$route.query,
                    // 拼接上order属性。
                    order: this.type + ":" + this.flag
                }
            })
        }
        // 
    }
}
</script>
<style lang="">
    
</style>
  1. 解决增加排序后,面包屑隐藏问题
vue

回车键提交表单

  1. 修改搜索框:src->components->Header->index.vue
vue
// 通过设置阻止默认样式,和设置name值,通过回车提交表单
<form @submit.prevent="goSearch" autocomplete="off" class="searchForm">
    <input name="keyword" placeholder="请输入搜索的关键词" type="text" ref="keyword"
           id="autocomplete" class="input-error input-xxlarge"/>
    <button class="sui-btn btn-xlarge btn-danger" type="submit">搜索</button>
</form>

实现页码功能

创建页码组件

  1. 封装页码组件:src->components->Pagination->index.vue
js
<template lang="">
    <div>
        <div class="pagination">
            <button>上一页</button>
            <button>1</button>
            <span>···</span>

            <button>14</button>
            <button>15</button>
            <button class="active">16</button>
            <button>17</button>
            <button>18</button>

            <span>···</span>
            <button>21</button>
            <button>下一页</button>

            <span>共 103 条</span>
        </div>
    </div>
</template>
<script>
export default {
    name:"Pagination",

}
</script>
<style lang="less" scoped>
    .pagination {...}
</style>
  1. 在入口文件引入,并且挂载为全局组件 : src->index.js
js
import Pagination from "@/components/Pagination";
// 挂载
new Vue({
    ...
	beforeCreate() {
		// 全局组件
		Vue.component("Pagination",Pagination);
		...
	},
	...
})

向分页组件传递参数

  1. 创建页码的store仓库:src->store->product.js
js
const state = {
    // 搜索数据
    searchResult:{
        // 设置搜索数据的初始值,防止undefined报错
        trademarkList:[],
        pageNo:1,
        pageSize:1,
        total:1,
        totalPages:10,
    },
}
// 定义mutations
const mutations = {
    // 保存搜索结果
    SAVE_SEARCH_RESULT(state, result) {
        state.searchResult = result;
    },
}
// 定义actions
const actions = {
    // 使用api获取搜索的数据
    async postProductListAsync({ commit }, body) {
        const { data } = await postProductList(body);
        commit("SAVE_SEARCH_RESULT", data);
    }
}
  1. 在父组件中传值:src->pages->Search->index.vue
js
<!-- 分页 -->
...
<Pagination :continue="5" :total="103" :pageNo="16" :pageSize="5"></Pagination>
...
  1. 在子组件中接收值:src->components->Pagination->index.vue
vue
<template lang="">
    <div>
        <div class="pagination">
            <button>上一页</button>
            <button>1</button>
            <span>···</span>

            <button>14</button>
            <button>15</button>
            <button class="active">16</button>
            <button>17</button>
            <button>18</button>

            <span>···</span>
            <button>{{pageSum}}</button>
            <button>下一页</button>
            <span>共 {{total}} 条</span>
        </div>
    </div>
</template>
<script>
export default {
    props: ["total", "pageSize","pageNo",],
    name:"Pagination",
    computed: {
        // 计算总页数
        pageSum() {
            return Math.ceil(this.total / this.pageSize)
        }
    }

}
</script>
<style lang="">
    
</style>

计算连接页码的起始与结束位置

  1. 计算页码的开始位置和结束位置:src->pages->components->Pagination.vue
js
<script>
export default {
    props:["total","pageSize","continue","pageNo"],
    name: "index",
    computed:{
        pageSum(){
            return Math.ceil(this.total/this.pageSize)
        },
        // 计算起始与结束位置
        startAndEnd(){
            let start = 0;
            let end = 0;
            // pageNo = 16,start==>14 end=18
            start = this.pageNo-(this.continue-1)/2;
            end = this.pageNo + (this.continue-1)/2
            return {start,end}
        }
    }
}
</script>

渲染页码

  1. 修改页码界面:src->components->Pagination->index.vue
vue
<template lang="">
    <div>
        <div class="pagination">
            <button :disabled="pageNo===1" >上一页</button>
            <button v-show="startAndEnd.start>1">首页</button>
            <span v-show="startAndEnd.start>2">···</span>

            <button v-for="page in (startAndEnd.end - startAndEnd.start+1)" 
                :key="page"
                :class="{active:pageNo===startAndEnd.start+page-1}">{{startAndEnd.start+page-1}}
            </button>

            <span v-show="pageSum>startAndEnd.end+1" >···</span>
            <button v-show="pageSum>startAndEnd.end" >尾页</button>
            <button :disabled="pageNo===pageSum">下一页</button>
            <span>共 {{total}} 条</span>
        </div>
    </div>
</template>
<script>
export default {
    props: ["total", "pageSize", "pageNo", "continue"],
    name:"Pagination",
    data(){
        return{
            
        }
    },
    computed: {
        // 计算总页数
        pageSum() {
            return Math.ceil(this.total / this.pageSize)
        },
        // 计算起始与结束位置
        startAndEnd() {
            // 定义初始的开始位置和结束位置
            let start = 0;
            let end = 0;
            
            // pageNo当前页 = 16,continue页长为5,start==>14 end=18
            // 实际页码长度不足continue
            if (this.continue > this.pageSum){
                start = 1;
                end = this.pageSum;
            }else{
                start = this.pageNo - (this.continue - 1) / 2;
                end = this.pageNo + (this.continue - 1) / 2;
                // 左边界问题
                if (start < 1) {
                    start = 1;
                    end = start + this.continue - 1;
                }
                // 右边界问题
                if (end > this.pageSum) {
                    end = this.pageSum;
                    start = this.pageSum - this.continue + 1;
                }
            }
            // console.log(start,end);
            return { start, end }
        }
    }
}
</script>

实现页码功能

  1. 设置并监听自定义事件,调用事件实现页码跳转:src->pages->Search->index.vue
js
<Pagination 
    :continue="5" 
    :total="23" 
    :pageNo="2" 
    :pageSize="15" 
	@change-page-no="changePageNo"
></Pagination>

methods:{
    // 改变当前页码
    changePageNo(pageNo){
        console.log("changePageNo",pageNo);
        this.$router.push({
            path: "/search",
            query: {
                ...this.$route.query,
                pageNo
            }
        })
    },
}
  1. 在页码组件上,设置触发事件:src->components->Pagination->index.vue
vue
<div>
  <div class="pagination">
    <button @click="$emit('change-page-no',pageNo-1)" :disabled="pageNo===1" >上一页</button>
    <button @click="$emit('change-page-no',1) "v-show="startAndEnd.start>1">首页</button>
    <span v-show="startAndEnd.start>2">···</span>

    <button 
            v-for="page in (startAndEnd.end - startAndEnd.start+1)" 
            :key="page"
            :class="{active:pageNo===startAndEnd.start+page-1}"
            @click="$emit('change-page-no',startAndEnd.start+page-1)"
            >{{startAndEnd.start+page-1}}
    </button>

    <span v-show="pageSum>startAndEnd.end+1" >···</span>
    <button @click="$emit('change-page-no',pageSum)" v-show="pageSum>startAndEnd.end" >尾页</button>
    <button @click="$emit('change-page-no',pageNo-1)" :disabled="pageNo===pageSum">下一
    </button>
    <span>共 {{total}} 条</span>
  </div>
</div>

解决bug

bug1:移除类别后只保留了关键字,并且设置 分类 品牌 属性 搜索 排序 移除后页码重置为1

  1. 修改src->pages->Search->index.vue
js
methods:{
        // 改变当前页码
        changePageNo(pageNo){
            console.log("changePageNo",pageNo);
            // 路由跳转到地址
            this.$router.push({
                path: "/search",
                query: {
                    ...this.$route.query,
                    pageNo
                }
            })
        },
        // 移除分类名字
        moveCategoryName(){
            // category3Id=249&categoryName=台灯
            // 获取搜索的关键词
            const query = { ...this.$route.query };
            // 删除分类信息
            delete query.category3Id;
            delete query.category2Id;
            delete query.category1Id;
            delete query.categoryName;
            this.$router.push({
                path:"/search",
                query,
                pageNo: 1
            })
        },
        moveKeyword(){
            // 解构出把 query进行浅拷贝
            const query = {...this.$route.query};
            // 删除query中的keyword属性
            delete query.keyword;
            // 路由到指定地址
            this.$router.push({
                path: "/search",
                query,
                pageNo: 1,
            })
        },
        // 删除商标信息
        moveTrademark(){
            const query = { ...this.$route.query };
            // 删除query中的trademark属性
            delete query.trademark;
            // 路由到指定地址
            this.$router.push({
                path: "/search",
                query,
                pageNo:1,
            })
        },
        // 移除属性过滤
        moveProps(item){
            // 跳转到指定路由
            this.$router.push({
                path: "/search",
                query:{
                    // 把之前的query拼接上
                    ...this.$route.query,
                    // 使用过滤器把props过滤后的拼接上
                    // 把不相等的返回流下来,相等的去除。
                    props: this.$route.query.props.filter(value =>{ return value !== item}),
                    pageNo:1
                }
            })
        },
        // 排序搜索
        orderSearch(type){
            // 如果选中的类别与当前的类别相同,则改变排序方式
            if(this.type === type){
                // 当是降序排序,则改变为升序排序
                this.flag = this.flag==="desc"?"asc":"desc";

            }else{
                // 如果排序类别不相同,则进行改变 type类型,并设置flag默认排序方式
                this.type = type;
                this.flag = "desc";
            }
            // 最后都要进行路由跳转
            this.$router.push({
                path: "/search",
                query: {
                    // 拼接上之前的query
                    ...this.$route.query,
                    // 拼接上order属性。
                    order: this.type + ":" + this.flag,
                    pageNo:1
                }
            })
        }
    }

bug2:面包屑为空时,不隐藏

  1. 修改 src->pages->Search-index.vue
js
原因:在删除完面包屑之后,会存在props一个空数组和pageNo=1这两个元素,造成判断隐藏 isSelector 函数中的数组中一直存在两个元素,返回为真,不隐藏
解决方法:修改 isSelector 函数,添加一个判断
computed:{
        ...mapState("product", ["searchResult"]),
        // 判断隐藏面包屑的方法
        isSelector() {
            // Object.values 返回由对象的属性值组成的数组,把对象的属性值进行过滤,过滤出 undefined 的,如果length>0返回真
            // true 代表存在 有查询
            // 判断是否是空数组
            // 浅拷贝一份:为了与watch侦听器区别开
            const query = { ...this.$route.query };
            // 删除pageNo属性
            delete query.pageNo;
            // 判断是否存在props,并且判断长度是否为0,为0则删除props属性
            if (query.props){
                if (query.props.length === 0)
                    delete query.props;
            }
            // 把query对象的属性值转为数组
            const arr = Object.values(query);
            // 遍历数组,把有属性值的元素返回
            const newArr = arr.filter(item => {
                return item;
            });
            // 当数组中的长度为0时,返回false,隐藏框
            return newArr.length>0;
        },
        // 改变排序图标的方法 
        upOrDown() {
            return this.flag === "desc" ? "iconfont icon-paixu" : "iconfont icon-xiangshang"
        },
    },

bug3:当搜索属性props只有一个时,刷新页面会出现"xxx"

js
原因:props在只有一个属性时,向后端发送的是 字符串形式 ,后端无响应,应该发送数组形式。
解决办法:在src->pages->Search-index.vue页面的watch侦听器中添加一个判断

  // 使用侦听器
    watch:{
        "$route.query":{
            handler(query){
                // 判断query的props属性是否是字符串,并转为数组
                if(query.props && (typeof query.props === "string")){
                    query.props = [query.props];
                }
                // 发送请求
                this.$store.dispatch("product/postProductListAsync",query)
            },
            immediate:true
        }
    },

使用一个函数优化搜索代码

  1. 定义一个工具 src->utils->gotoSearch.js
js
// 导入vueRouter
import VueRouter from "vue-router";

// 接收参数对象
VueRouter.prototype.gotoSearch = function (query) {
    console.log(this)
    this.push({
        path: "/search",
        query: {
            // 拼接之前的 query
            ...this.history.current.query,
            // 默认pageNo为1
            pageNo: 1,
            // 拼接新的 query
            ...query,
        }
    })
}
  1. 导入 src->route->index.js
js
import '@/utils/gotoSearch';
  1. 在搜索页面使用:src->pages->Search->index.vue
js
methods:{
        // 改变当前页码
        changePageNo(pageNo){
            // console.log("changePageNo",pageNo);
            // 路由跳转到地址
            // this.$router.push({
            //     path: "/search",
            //     query: {
            //         ...this.$route.query,
            //         pageNo
            //     }
            // })
            this.$router.gotoSearch({pageNo});
        },
        // 移除分类名字
        moveCategoryName(){
            // category3Id=249&categoryName=台灯
            this.$router.gotoSearch({
                categoryName: undefined,
                category3Id: undefined,
                category2Id: undefined,
                category1Id: undefined
            })
        },
        // 移除关键字
        moveKeyword(){
            // 解构出把 query进行浅拷贝
            // const query = {...this.$route.query};
            // 删除query中的keyword属性
            // delete query.keyword;
            // 路由到指定地址
            // this.$router.push({
            //     path: "/search",
            //     query,
            //     pageNo: 1,
            // })
            this.$router.gotoSearch({
                keyword: undefined,
            });
            // 使用事件总线清空搜索框内的内容
            this.$bus.$emit("clearKeyword");
        },
        // 删除商标信息
        moveTrademark(){
            // const query = { ...this.$route.query };
            // 删除query中的trademark属性
            // delete query.trademark;
            // 路由到指定地址
            // this.$router.push({
            //     path: "/search",
            //     query,
            //     pageNo:1,
            // })
            this.$router.gotoSearch({
                trademark: undefined,
            });
        },
        // 移除属性过滤
        moveProps(item){
            // 跳转到指定路由
            // this.$router.push({
            //     path: "/search",
            //     query:{
            //         // 把之前的query拼接上
            //         ...this.$route.query,
            //         // 使用过滤器把props过滤后的拼接上
            //         // 把不相等的返回流下来,相等的去除。
            //         props: this.$route.query.props.filter(value =>{ return value !== item}),
            //         pageNo:1
            //     }
            // })
            this.$router.gotoSearch({ props: this.$route.query.props.filter(value => { return value !== item }) });
        },
        // 排序搜索
        orderSearch(type){
            // 如果选中的类别与当前的类别相同,则改变排序方式
            if(this.type === type){
                // 当是降序排序,则改变为升序排序
                this.flag = this.flag==="desc"?"asc":"desc";

            }else{
                // 如果排序类别不相同,则进行改变 type类型,并设置flag默认排序方式
                this.type = type;
                this.flag = "desc";
            }
            // 最后都要进行路由跳转
            // this.$router.push({
            //     path: "/search",
            //     query: {
            //         // 拼接上之前的query
            //         ...this.$route.query,
            //         // 拼接上order属性。
            //         order: this.type + ":" + this.flag,
            //         pageNo:1
            //     }
            // })
            this.$router.gotoSearch({ order: this.type + ":" + this.flag, })
        }
    }

创建路由产品详情

  1. 抽离路由产品页面 src->pages->Detail->index.vue
js
<template lang="">
    <div>
        页面详情
    </div>
</template>
<script>
export default {
    name:"Details"
}
</script>
<style lang="less" scoped>
    
</style>
  1. 添加路由配置:src->router->index.js
js
{
    path:"/detail/:id.html",
        component:Detail,
            meta:{
                isTypeNav: true
            }
}
  1. 在搜索页面配置router-link:src->pages->Search->index.vue
vue
<div class="goods-list">
    <ul class="yui3-g">
        <li  v-for="(item,index) in searchResult.goodsList" :key="index" class="yui3-u-1-5">
            <div class="list-wrap">
                <div class="p-img">
                    <router-link :to="'/detail/'+item.id+'.html'"><img :src="item.defaultImg" /></router-link>
                </div>
                <div class="price">
                    <strong>
                        <em>¥</em>
                        <i>{{item.price}}</i>
                    </strong>
                </div>
                <div class="attr">
                    <router-link :to="'/detail/'+item.id+'.html'" :title="item.title" >{{item.title}}</router-link>
                    <!-- <a target="_blank" href=""  :title="item.title">{{item.title}}</a> -->
                </div>
                <div class="commit">
                    <i class="command">已有<span>{{item.hotScore}}</span>人评价</i>
                </div>
                <div class="operate">
                    <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
                    <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
                </div>
            </div>
        </li>
    </ul>
</div>

切换路由并指定位置

  1. 使用vue-router的滚动事件(scrollBehavior):src->router->index.js
js
// 设置meta的值
// 搜索详情路由
    {
        path: "/detail/:id.html",
        component: Details,
        meta: {
            isTypeNav: true,
            ScrollToHeader:true,
        }
    }
// 创建路由对象
const router = new VueRouter({
	mode:"history",
	routes,
	// 1- 什么时候执行?答:切换路由时执行。
	// 2- 接收什么参数?答:to:去哪个路由,from:来自哪个路由
	// 3- 返回的结果是什么?答:一个对象,对象可以决定滚动条的位置。
	scrollBehavior(to,from){
		if(!to.meta.noScroll){
			return {
				x:0,// 横向
				y:0// 纵向
			}
		}
	}
});

获取详情数据

  1. 创建api请求数据:src->api->product.js
js
// 根据商品ID获取商品详情
const getProductInfoById = function (skuId){
    return sphRequest.get(`/item/${skuId}`)
}
// 暴漏数据
export { 
    getBaseCategoryList,
    getFloorList,
    getRankList,
    getLikeList,
    postProductList,
    getProductInfoById
}
  1. 在store中存储商品的数据:src->store->product.js
js
import {
	getProductInfoById
} from "@/api/product";

const state = {
	// 商品信息
	productInfo:{
	
	}
}

const mutations = {
	// 保存商品信息
	SAVE_PRODUCT_INFO(state,payload){
		state.productInfo = payload;
	}
}

const actions = {
	async getProductInfoByIdAsync ({commit},id){
		const {data} = await getProductInfoById(id);
		commit("SAVE_PRODUCT_INFO",data);
	}
}

export default {
	namespaced:true,
	state,
	mutations,
	actions
}
  1. 在页面中调用:src->Pages->Details->index.vue
js
mounted(){
    this.$store.dispatch("product/getProductInfoByIdAsync",this.$route.params.id)
}

渲染详情基本数据

  1. 在页面中渲染数据:src->pages->Details->index.vue
vue
<template lang="">
...
<!-- 导航路径区域 -->
<div class="conPoin">
    <div class="conPoin">
        <!-- 一级分类 -->
        <router-link :to="{
            path:'/search',
            query:{
                category1Id:categoryView.category1Id,
                categoryName:categoryView.category1Name,
            }
        }" >{{categoryView.category1Name}}</router-link>
        <!-- 二级分类 -->
        <router-link :to="{
            path:'/search',
            query:{
                category2Id:categoryView.category2Id,
                categoryName:categoryView.category2Name,
            }
        }" >{{categoryView.category2Name}}</router-link>
        <!-- 三级分类 -->
        <router-link :to="{
            path:'/search',
            query:{
                category3Id:categoryView.category3Id,
                categoryName:categoryView.category3Name,
            }
        }" >{{categoryView.category3Name}}</router-link>
    </div>
</div>
...
<!-- 商品配置信息选择 -->
<div class="choose">
    <div class="chooseArea">
        <div class="choosed"></div>
        <dl v-for="item in spuSaleAttrList" :key="item.id">
            <dt class="title">选择{{item.saleAttrName}}</dt>
            <dd changepirce="0" 
                :class="{active:info.isChecked/1===1}"
                v-for="info in item.spuSaleAttrValueList"
                :key="info.id"
                >{{info.saleAttrValueName}}
            </dd>
        </dl>
    </div>
    <div class="cartWrap">
        <div class="controls">
            <input autocomplete="off" value="1" class="itxt">
            <a href="###" class="plus">+</a>
            <a href="###" class="mins">-</a>
        </div>
        <div class="add">
            <a href="###" target="_blank">加入购物车</a>
        </div>
    </div>
</div>
....
</template>
<script>
export default {
    <script>
import { mapState } from "vuex";
export default {
    name:"Details",
    data(){
        return{

        }
    },
    computed:{
        ...mapState("product",{
            // 类别,面包屑导航
            categoryView(state) {
                return state.productInfo.categoryView || {};
            },
            // 商品的详情配置
            spuSaleAttrList(state) {
                return state.productInfo.spuSaleAttrList || {};
            },
            // 商品的详情描述信息
            skuInfo(state) {
                return state.productInfo.skuInfo || {};
            },
        })
    },
    mounted(){
        this.$store.dispatch("product/getProductInfoByIdAsync",this.$route.params.id)
    }
}
}
</script>
<style lang="">
    ...
</style>

完成放大镜

  1. 抽离为组件:src->Details->Zoom->index.vue
vue
<template lang="">
    <div>
        <div class="preview">
            <div class="jqzoom">
                <img src="../../../assets/images/s1.png">
            </div>
        </div>
    </div>
</template>
<script>
export default {
    name:"Zoom",

}
</script>
  1. 使用第三方模块:vue-photo-zoom-pro
js
https://mater1996.github.io/vue-photo-zoom-pro/guide/
// 使用2.2.1版本
npm install vue-photo-zoom-pro@2.2.1
  1. 在页面中引入:src->pages->Detail->Zoom->index.vue
vue
<template>
    <div class="preview">
        <vue-photo-zoom-pro
                :out-zoomer="true"
                :width="200"
                :height="200"
                :high-url="$store.state.product.productInfo.skuInfo.skuDefaultImg">
            <img :src="$store.state.product.productInfo.skuInfo.skuDefaultImg" />
        </vue-photo-zoom-pro>
    </div>
</template>

<script>
import VuePhotoZoomPro from 'vue-photo-zoom-pro'
import 'vue-photo-zoom-pro/dist/style/vue-photo-zoom-pro.css'
export default {
    name: "Zoom",
    components: {
        VuePhotoZoomPro,
    }
}
</script>
<style lang="less">
.preview {
    position: relative;
    width: 400px;
    height: 400px;
    border: 1px solid #DFDFDF;
    img{
        width:100%;
        height:100%;
    }
    .zoomer{
        z-index: 999;
        top:0!important;
        left:10px!important;
    }
    .selector{
        background-color: rgba(255,0,0,0.3);
    }
}
</style>

完成缩略图

  1. 抽离出缩略图:src->pages->Details->ImagesList->index.vue
vue
<template lang="">
    <div>
        <div class="specScroll">
            <!--左按钮-->
            <a class="prev">&lt;</a>
            <!-- 中间可滑动区域 使用swiper实现 第三方-->
            <swiper class="swiper" :options="swiperOption">
                <swiper-slide v-for="item in skuImageList" :key="item.id" >
                    <img :src="item.imgUrl" />
                </swiper-slide>
            </swiper>
            <!--右按钮-->
            <a class="next">&gt;</a>
        </div>
    </div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper';
import 'swiper/css/swiper.css';
import { mapState } from 'vuex';
export default {
    name:"Thumbnail",
    components: {
        Swiper,
        SwiperSlide
    },
    data() {
        return {
            swiperOption: {
                slidesPerView: 5,
                spaceBetween: 10,
                navigation: {
                    nextEl: '.next',
                    prevEl: '.prev'
                }
            }
        }
    },
    computed:{
        ...mapState("product",{
            skuImageList(state){
                return state.productInfo.skuInfo.skuImageList;
            }
        })
    }
}
</script>
<style lang="less" scoped>
.specScroll {
    margin-top: 5px;
    width: 400px;
    overflow: hidden;
    display: flex;
    .prev,.next {
        text-align: center;
        width: 10px;
        height: 54px;
        line-height: 54px;
        border: 1px solid #CCC;
        background: #EBEBEB;
        cursor: pointer;
    }
    img {
        text-align: center;
        border: 1px solid #CCC;
        padding: 2px 6px;
        width: 50px;
        height: 50px;
        margin-right: 20px;
    }
    .swiper {
        width: 100%;
        margin: 0 10px;
    }
}
        
</style>
  1. 实现缩略图切换:src->store->product.js
js
const state = {
	// 商品信息
	productInfo:{
		// 商品详情
		skuInfo:{
            // 方式出现undefined 报错
			skuImageList:[]
		}
	}
}
const mutations = {
	// 根据ID修改选中项
	SAVE_SKUIMAGE_DEFAULT(state,id){
		// 1- 将之前的选中项移除,isDefault标记修改为0
		state.productInfo.skuInfo.skuImageList.find(v=>v.isDefault==="1").isDefault = "0";
		// 2- 根据id,将isDefault修改为1
		state.productInfo.skuInfo.skuImageList.find(v=>v.id === id).isDefault = "1";
	}
}
  1. 在缩略图的组件中修改:src->pages->Detail->Thumbnail->index.vue
vue
<template lang="">
    <div>
        <div class="specScroll">
            <!--左按钮-->
            <a class="prev">&lt;</a>
            <!-- 中间可滑动区域 使用swiper实现 第三方-->
            <swiper class="swiper" :options="swiperOption">
                <swiper-slide v-for="item in skuImageList" :key="item.id" >
                    <img @click="changeImg(item.id)" :src="item.imgUrl" />
                </swiper-slide>
            </swiper>
            <!--右按钮-->
            <a class="next">&gt;</a>
        </div>
    </div>
</template>


<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper';
import 'swiper/css/swiper.css';
import { mapState } from 'vuex';
export default {
    name:"Thumbnail",
    components: {
        Swiper,
        SwiperSlide
    },
    data() {
        return {
            swiperOption: {
                slidesPerView: 5,
                spaceBetween: 10,
                navigation: {
                    nextEl: '.next',
                    prevEl: '.prev'
                }
            }
        }
    },
    computed:{
        ...mapState("product",{
            skuImageList(state){
                return state.productInfo.skuInfo.skuImageList;
            }
        })
    },
    // 增加方法
    methods:{
        changeImg(id) {
            this.$store.commit("product/SAVE_SKUIMAGE_DEFAULT", id);
        }
    }
}
  1. 通过点击缩略图,放大镜的图片也发生相应的变化
vue
<template lang="">
    <div>
        <div class="preview">
        <vue-photo-zoom-pro
                :out-zoomer="true"
                :width="200"
                :height="200"
                :high-url="imgUrl">
            <img :src="imgUrl" />
        </vue-photo-zoom-pro>
    </div>
    </div>
</template>
<script>
import VuePhotoZoomPro from 'vue-photo-zoom-pro';
import 'vue-photo-zoom-pro/dist/style/vue-photo-zoom-pro.css';
import { mapState } from 'vuex';

export default {
    name:"Zoom",
    data(){
        return{
            
        }
    },
    components:{
        VuePhotoZoomPro,
    },
    computed:{
        ...mapState("product",{
            skuImageList(state){
                return state.productInfo.skuInfo.skuImageList;
            }
        }),
        imgUrl(){
            // 获取isDefault === "1"的缩略图对象
            const imgItem = this.skuImageList.find(v => v.isDefault === "1");
            // 判断 imgItem 存在,返回缩略图的地址
            if (imgItem) return imgItem.imgUrl;
            return imgItem;
        }
    }
}
</script>