搜索页面
全局过滤器
- 创建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]);
}
}
- 在入口文件引入 src->main.js
js
// 导入filters
import filters from "@/filters"
Vue.config.productionTip = false
Vue.use(filters);
- 应用过滤器 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实现后端
- 安装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");
})
- 配置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
}
]
- 启动node
js
node start server.js
// 测试访问
http://127.0.0.1:9090/likeList?pageNo=1&pageSize=5
猜你喜欢
- 配置代理,解决跨域问题。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":""
}
}
}
- 封装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;
- 封装api getLikeList接口 src->api->product.js
js
import { sphRequest, mockRequest ,nodeRequest} from "@/request";
...
// 获取猜你喜欢数据
const getLikeList = ()=>{
return nodeRequest("/hanser/likeList")
}
...
- 创建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);
}
}
}
- 抽离出猜你喜欢 组件 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
- 根据后端接口信息获取分类名称和ID
js
// categoryName:分类的名称
// category1Id:一级分类ID
// category2Id:二级分类ID
// category3Id:三级分类ID
- 修改导航组件 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>
- 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>
获取搜索框中内容
- 在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>
合并类别和关键词搜索
- 在搜索页面把 查询字符串拼接上 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
}
});
}
- 在头部组件中的分类页面中把搜索的关键字拼接上 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;
}
},
调用接口获取搜索数据
- 调用商品接口获取数据 src->api->product.js
js
// 搜索获取数据
const postProductList = function(body){
return sphRequest.post("/list",body);
}
// 暴漏数据
export {
getBaseCategoryList,
getFloorList,
getRankList,
getLikeList,
postProductList
}
- 封装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);
}
}
...
- 抽离出搜索页面,并且获取商品数据 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>
渲染搜索商品数据
- 渲染商品列表数据
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级分类和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渲染
- 把搜索选择框抽离出来: src->pages->Search->SearchSelector->index.vue
vue
<template lang="">
<div>
</div>
</template>
<script>
export default {
}
</script>
<style lang="">
</style>
- 同时在父组件中引入: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>
- 在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>
面包屑导航
类别添加以及移除
- 修改搜索页面 : 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>
关键字添加以及移除
- 修改搜索页面: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>
通过事件总线清空搜索栏
- 在入口文件生成$bus src->index.js
js
new Vue({
store,
router,
beforeCreate() {
// 设置事件总线
Vue.prototype.$bus = this;
},
render: h => h(App),
}).$mount('#app')
- 在搜索按钮的页面设置监听事件 :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>
- 在离开搜索页面的时候触发监听事件 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");
}
按照品牌进行搜索
- 修改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>
无搜索条件隐藏面包屑导航
- 修改页面: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>
面包屑导航中渲染品牌和移除品牌
- 修改页面: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>
根据商品属性进行过滤搜索
- 后端的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>
- 点击相关属性后,获取后台数据: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>
面包屑渲染属性以及移除属性
- 面包屑渲染过滤属性: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>
引用阿里图标库资源
- 下载阿里图标
- 把图标资源放在:src->assets->iconfont
- 在入口文件引入样式:src->index.js
js
import "@/assets/iconfont/iconfont.css";
- 在对应的页面使用样式: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>
实现商品排序
- 结合后端api数据
js
"order",this.$route.query.order
// 排序方式
// 排序类型(type) 1: 综合 2: 价格
// 排序标识(flag) asc: 升序 desc: 降序
// 示例: "1:desc"
- 修改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>
- 解决增加排序后,面包屑隐藏问题
vue
回车键提交表单
- 修改搜索框: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>
实现页码功能
创建页码组件
- 封装页码组件: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>
- 在入口文件引入,并且挂载为全局组件 : src->index.js
js
import Pagination from "@/components/Pagination";
// 挂载
new Vue({
...
beforeCreate() {
// 全局组件
Vue.component("Pagination",Pagination);
...
},
...
})
向分页组件传递参数
- 创建页码的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);
}
}
- 在父组件中传值:src->pages->Search->index.vue
js
<!-- 分页 -->
...
<Pagination :continue="5" :total="103" :pageNo="16" :pageSize="5"></Pagination>
...
- 在子组件中接收值: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>
计算连接页码的起始与结束位置
- 计算页码的开始位置和结束位置: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>
渲染页码
- 修改页码界面: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>
实现页码功能
- 设置并监听自定义事件,调用事件实现页码跳转: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
}
})
},
}
- 在页码组件上,设置触发事件: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
- 修改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:面包屑为空时,不隐藏
- 修改 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
}
},
使用一个函数优化搜索代码
- 定义一个工具 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,
}
})
}
- 导入 src->route->index.js
js
import '@/utils/gotoSearch';
- 在搜索页面使用: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, })
}
}
创建路由产品详情
- 抽离路由产品页面 src->pages->Detail->index.vue
js
<template lang="">
<div>
页面详情
</div>
</template>
<script>
export default {
name:"Details"
}
</script>
<style lang="less" scoped>
</style>
- 添加路由配置:src->router->index.js
js
{
path:"/detail/:id.html",
component:Detail,
meta:{
isTypeNav: true
}
}
- 在搜索页面配置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>
切换路由并指定位置
- 使用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// 纵向
}
}
}
});
获取详情数据
- 创建api请求数据:src->api->product.js
js
// 根据商品ID获取商品详情
const getProductInfoById = function (skuId){
return sphRequest.get(`/item/${skuId}`)
}
// 暴漏数据
export {
getBaseCategoryList,
getFloorList,
getRankList,
getLikeList,
postProductList,
getProductInfoById
}
- 在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
}
- 在页面中调用:src->Pages->Details->index.vue
js
mounted(){
this.$store.dispatch("product/getProductInfoByIdAsync",this.$route.params.id)
}
渲染详情基本数据
- 在页面中渲染数据: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>
完成放大镜
- 抽离为组件: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>
- 使用第三方模块: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
- 在页面中引入: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>
完成缩略图
- 抽离出缩略图:src->pages->Details->ImagesList->index.vue
vue
<template lang="">
<div>
<div class="specScroll">
<!--左按钮-->
<a class="prev"><</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">></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>
- 实现缩略图切换: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";
}
}
- 在缩略图的组件中修改:src->pages->Detail->Thumbnail->index.vue
vue
<template lang="">
<div>
<div class="specScroll">
<!--左按钮-->
<a class="prev"><</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">></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);
}
}
}
- 通过点击缩略图,放大镜的图片也发生相应的变化
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>