文章目录
- 一、·首页搜索功能
- 1. 搜索页面
- 2. 历史记录和热门搜索组件
- 3. 搜索框提示列表组件
- 4. 综合-价格-分类
- 5. 搜索出的产品展示
- 6. 异常修复
- 7. 路由拦截/路由守卫
- 二、详情页
- 2.1. 效果图
- 2.2. 详情api
- 2.3. 配置路由
- 2.4. 详情页面
- 2.5. 详情页源码
技术选型
组件 | 版本 | 说明 |
---|---|---|
vue | ^2.6.11 | 数据处理框架 |
vue-router | ^3.5.3 | 动态路由 |
vant | ^2.12.37 | 移动端UI |
axios | ^0.24.0 | 前后端交互 |
amfe-flexible | ^2.2.1 | Rem 布局适配 |
postcss-pxtorem | ^5.1.1 | Rem 布局适配 |
less | ^4.1.2 | css编译 |
less-loader | ^5.0.0 | css编译 |
vue/cli | ~4.5.0 | 项目脚手架 |
vue-cli + vant+ less +axios 开发
一、·首页搜索功能
1. 搜索页面
{path: '/home',//首页name: 'Home',component: () => import('@/views/home/Home'),meta: { // 用来判断该组件对应的页面是否显示底部tabbarisShowTabbar: true},children: [{path: 'searchPopup',name: 'SearchPopup',component: () => import('@/views/home/search/SearchPopup')}]},
1.在http.js文件中定义接口请求
// 2.搜索页 SearchPopup
// 历史记录列表和热门搜索列表
export function GetPopupData(params) {return instance({url: '/search/index',method: 'get',params})
}
//删除历史记录
export function Clearhistory(params) {return instance({url: '/search/clearhistory',method: 'post',data: params})
}//搜索提示列表
export function GetSearchTipsListData(params) {return instance({url: '/search/helper',method: 'get',params})
}
//根据关键字搜索商品
export function GetSearchData(params) {return instance({url: '/goods/list',method: 'get',params})
}
2.views /SearchPopup.vue
在views 目录下创建SearchPopup.vue 页面,作为点击搜索后的页面
<template><div class="search-popup-box"><!--搜索框 --><van-searchshape="round"v-model="value"show-action:placeholder="placeholderVal"@search="onSearch"@cancel="onCancel"@input="onInput"/><!-- 历史记录和热门搜索 组件 --><!-- 接受子组件传来的数据 --><HistoryHotv-if="blockShow == 1":historyKeywordList="historyKeywordList":hotKeywordList="hotKeywordList"@goSearch="setValue"></HistoryHot><!-- 搜索提示 组件 --><SearchTipsListv-else-if="blockShow == 2":dataList="dataList"@setValue="setValue"></SearchTipsList><!-- 在父组件中绑定自定义事件 priceChange1 categoryChange1 --><!-- 下拉菜单 --><searchProducts:filterCategory="filterCategory":goodsList="goodsList"@priceChange1="priceChange1"@categoryChange1="categoryChange1"v-else></searchProducts></div>
</template><script>
import HistoryHot from "@/views/home/search/HistoryHot";
import searchProducts from "@/views/home/search/searchProducts";
import SearchTipsList from "@/views/home/search/SearchTipsList";
// 引入请求接口
import { GetSearchData, GetPopupData, GetSearchTipsListData } from "@/https/http";
export default {name: "search-popup",data() {return {value: "", // 搜索值page: 1, //页数size: 20, // 每页数据条数order: "desc", // 价格由高到底categoryId: 0, // 商品类别id 全家、居家、餐饮。。。sort: "id", // 排序字段 price 和id 2种情况goodsList: [], // 搜索出来的商品数据filterCategory: [], // 商品类别参数historyKeywordList: [], //历史记录数据hotKeywordList: [], // 热门搜索 数据blockShow: 1, //控制显示哪个?( 历史记录和热门搜索组件 、 搜索提示组件、下拉菜单组件)dataList: [], //搜索提示数据placeholderVal: '' // 搜索框的默认提示词};},methods: {priceChange1(value) {// value为子组件searchProducts传来的价格排序参数asc,descconsole.log("父组件:价格", value);this.order = value;this.sort = "price";this.onSearch();// this.$router.go(0);},categoryChange1(value) {// value为子组件searchProducts传来的种类参数console.log("父组件:类别id", value);this.sort = "id";this.categoryId = value;this.onSearch();},// 搜索框搜索功能onSearch() {this.blockShow = 3// 关键字搜索接口数据let obj = {keyword: this.value,page: 1,size: this.size, // 每页数据条数order: this.order, // 价格由高到底categoryId: this.categoryId, // 商品类别id 全家、居家、餐饮。。。sort: this.sort, // 排序字段};// 发送数据请求// 搜索框商品搜索功能接口,获取对应的商品列表GetSearchData(obj).then((res) => {// console.log(22, res);this.filterCategory = res.data.filterCategory;// 将商品的类别中的字段替换一下this.filterCategory = JSON.parse(JSON.stringify(this.filterCategory).replace(/id/g, "value").replace(/name/g, "text"));this.goodsList = res.data.goodsList;});},// 搜索取消onCancel() {this.$router.push("/home");},// 获得历史记录gethistoryHotData() {GetPopupData().then((res) => {console.log(555, res);this.historyKeywordList = res.data.historyKeywordList;this.hotKeywordList = res.data.hotKeywordList;this.placeholderVal = res.data.defaultKeyword.keyword;});},// 获取搜索提示getSearchHelperData(value) {this.blockShow = 2 // 展示搜索提示列表组件// 输入触发// 搜索提示数据请求GetSearchTipsListData({ keyword: value }).then((res) => {console.log(333333, res);this.dataList = res.data})},setValue(m) {console.log(6666, m);this.value = mthis.onSearch()},onInput(value) {this.getSearchHelperData(value)}},created() {this.gethistoryHotData();},components: {HistoryHot,searchProducts,SearchTipsList,},
};
</script><style></style>
2. 历史记录和热门搜索组件
components/HistoryHot.vue
在components目录下 创建 HistoryHot.vue(历史记录和热门搜索) 组件,引入到SearchPopup.vue中
<template><div class="box"><div class="history-hot" v-if="isShowHistory"><h4>历史记录</h4><van-icon name="delete" class="delete-icon" @click="clearFn" /><van-tagplaintype="primary"v-for="(item, index) in historyKeywordList":key="index"v-if="item"@click="goSearch(item)">{{ item }}</van-tag></div><div class="hot-box"><h4>热门搜索</h4><van-tagplain:type="item.is_hot ? 'danger' : 'primary'"v-for="(item, index) in hotKeywordList":key="index"v-if="item.keyword"@click="goSearch(item.keyword)">{{ item.keyword }}</van-tag></div></div>
</template><script>
import { Clearhistory } from '@/https/http'
export default {name: "history-hot",data() {return {isShowHistory: 1}},props: ["historyKeywordList", "hotKeywordList"],methods: {clearFn() {Clearhistory().then((res) => {console.log(res);this.isShowHistory = 0})},goSearch(value) {this.$emit('goSearch', value)}}
};
</script>
<style lang="less" scoped>
.box {font-size: 16px;span {margin-right: 3px;}.history-hot {margin-bottom: 10px;position: relative;.delete-icon {position: absolute;top: 10px;right: 10px;}}
}
</style>
3. 搜索框提示列表组件
components/SearchTipsList.vue
在components目录下 创建 SearchTipsList.vue(搜索框提示列表组件) 组件,引入到SearchPopup.vue中
<template><div class="search-tips-list-box"><van-listv-model="loading":finished="finished"finished-text="没有更多了"@load="onLoad"><van-cellv-for="item in dataList":key="item":title="item"@click="geiValue(item)"/></van-list></div>
</template><script>
export default {name: "search-tips-list",data() {return {list: [],loading: false, // //是否处于加载状态finished: false, // 是否加载完成};},props: ["dataList"], 父传子数组methods: {onLoad() {},geiValue(value){this.$emit('setValue',value)}},
};
</script><style lang="less" scoped>
</style>
4. 综合-价格-分类
components/SearchProducts.vue
在components 下 新建SearchProducts.vue (综合-价格-分类组件)组件,引入到SearchPopup.vue
<template><div class="search-popup-box"><!--搜索框 --><van-searchshape="round"v-model="value"show-action:placeholder="placeholderVal"@search="onSearch"@cancel="onCancel"@input="onInput"/><!-- 历史记录和热门搜索 组件 --><!-- 接受子组件传来的数据 --><HistoryHotv-if="blockShow == 1":historyKeywordList="historyKeywordList":hotKeywordList="hotKeywordList"@goSearch="setValue"></HistoryHot><!-- 搜索提示 组件 --><SearchTipsListv-else-if="blockShow == 2":dataList="dataList"@setValue="setValue"></SearchTipsList><!-- 在父组件中绑定自定义事件 priceChange1 categoryChange1 --><!-- 下拉菜单 --><searchProducts:filterCategory="filterCategory":goodsList="goodsList"@priceChange1="priceChange1"@categoryChange1="categoryChange1"v-else></searchProducts></div>
</template><script>
import HistoryHot from "@/views/home/search/HistoryHot";
import searchProducts from "@/views/home/search/searchProducts";
import SearchTipsList from "@/views/home/search/SearchTipsList";
// 引入请求接口
import { GetSearchData, GetPopupData, GetSearchTipsListData } from "@/https/http";
export default {name: "search-popup",data() {return {value: "", // 搜索值page: 1, //页数size: 20, // 每页数据条数order: "desc", // 价格由高到底categoryId: 0, // 商品类别id 全家、居家、餐饮。。。sort: "id", // 排序字段 price 和id 2种情况goodsList: [], // 搜索出来的商品数据filterCategory: [], // 商品类别参数historyKeywordList: [], //历史记录数据hotKeywordList: [], // 热门搜索 数据blockShow: 1, //控制显示哪个?( 历史记录和热门搜索组件 、 搜索提示组件、下拉菜单组件)dataList: [], //搜索提示数据placeholderVal: '' // 搜索框的默认提示词};},methods: {priceChange1(value) {// value为子组件searchProducts传来的价格排序参数asc,descconsole.log("父组件:价格", value);this.order = value;this.sort = "price";this.onSearch();// this.$router.go(0);},categoryChange1(value) {// value为子组件searchProducts传来的种类参数console.log("父组件:类别id", value);this.sort = "id";this.categoryId = value;this.onSearch();},// 搜索框搜索功能onSearch() {this.blockShow = 3// 关键字搜索接口数据let obj = {keyword: this.value,page: 1,size: this.size, // 每页数据条数order: this.order, // 价格由高到底categoryId: this.categoryId, // 商品类别id 全家、居家、餐饮。。。sort: this.sort, // 排序字段};// 发送数据请求// 搜索框商品搜索功能接口,获取对应的商品列表GetSearchData(obj).then((res) => {// console.log(22, res);this.filterCategory = res.data.filterCategory;// 将商品的类别中的字段替换一下this.filterCategory = JSON.parse(JSON.stringify(this.filterCategory).replace(/id/g, "value").replace(/name/g, "text"));this.goodsList = res.data.goodsList;});},// 搜索取消onCancel() {this.$router.push("/home");},// 获得历史记录gethistoryHotData() {GetPopupData().then((res) => {console.log(555, res);this.historyKeywordList = res.data.historyKeywordList;this.hotKeywordList = res.data.hotKeywordList;this.placeholderVal = res.data.defaultKeyword.keyword;});},// 获取搜索提示getSearchHelperData(value) {this.blockShow = 2 // 展示搜索提示列表组件// 输入触发// 搜索提示数据请求GetSearchTipsListData({ keyword: value }).then((res) => {console.log(333333, res);this.dataList = res.data})},setValue(m) {console.log(6666, m);this.value = mthis.onSearch()},onInput(value) {this.getSearchHelperData(value)}},created() {this.gethistoryHotData();},components: {HistoryHot,searchProducts,SearchTipsList,},
};
</script><style></style>
5. 搜索出的产品展示
components/Products.vue
在components 下 新建 Products.vue(搜索出的产品展示组件),引入到SearchProducts.vue,作为其子组件使用。
<template><!-- 数据列表渲染 --><ul class="goods_list"><li v-for="item in goodsList" :key="item.id" @click="gotodetail(item.id)"><img v-lazy="item.list_pic_url" alt="" /><p>{{ item.name }}</p><p>{{ item.retail_price | moneyFlrmat }}</p></li></ul>
</template><script>
export default {name:'products',props:['goodsList'],data(){return{}},methods: {gotodetail(id_){this.$router.push({path:'/productDetail',query:{id:id_}})}}
}
</script><style lang="less" scoped>.goods_list {display: flex;flex-wrap: wrap;justify-content: space-between;font-size: 16px;line-height: 20px;text-align: center;li {width: 48%;img {width: 100%;}}}
</style>
6. 异常修复
关于重复点击同一个路由出现的报错问题解决
在新版本的vue-router中,重复点击同一个路由会出现以下报错:
方案1、vue-router降级处理(但不推荐)
npm i vue-router@3.0.7
方案2、直接在push方法最后添加异常捕获,例如:
<van-search v-model="SearchVal" shape="round" placeholder="请输入搜索关键词" disabled @click="$router.push('/home/searchPopup').catch(err=>{})"/>
方案3、直接修改原型方法push(推荐)
// 把这段代码直接粘贴到router/index.js中的Vue.use(VueRouter)之前
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function(location) {return originalPush.call(this, location).catch(err => {})
};
7. 路由拦截/路由守卫
vue-router文档地址
路由拦截(导航守卫:前置导航守卫和后置导航守卫)
前置导航守卫有三个参数
to: 表示即将进入的路由
from: 表示即将离开的路由
next() :表示执行进入这个路由
// 路由前置守卫
router.beforeEach((to, from, next) => {// 有token就表示已经登录// 想要进入购物车页面,必须有登录标识token// console.log('to:', to)// console.log('from:', from)let token = localStorage.getItem('token')if (to.path == '/cart') {// 此时必须要有tokenif (token) {next(); // next()去到to所对应的路由界面} else {Vue.prototype.$toast('请先登录');// 定时器setTimeout(() => {next("/user"); // 强制去到"/user"所对应的路由界面}, 1000);}} else {// 如果不是去往购物车的路由,则直接通过守卫,去到to所对应的路由界面next()}
})
二、详情页
2.1. 效果图
2.2. 详情api
1.在http.js 文件中定义详情页请求接口
//3.详情页 ProductDetail
// 产品详情
export function GoodsDetailApi(params) {return instance({url: '/goods/detail',method: 'get',params})
}
//详情页相关产品
export function GetGoodsRelatedData(params) {return instance({url: '/goods/related',method: 'get',params})
}
//获取商品数量
export function GetCartNum(params) {return instance({url: '/cart/goodscount',method: 'get',params})
}
// 添加到购物车
export function AddToCart(params) {return instance({url: '/cart/add',method: 'post',data: params})
}
2.3. 配置路由
router/index.js
在components 目录下创建ProductDetail.vue (详情页组件),并在路由中配置( 一级路由)
{path: '/productDetail', //产品详情name: 'ProductDetail',component: () => import('@/views/productdetail/ProductDetail')},
2.4. 详情页面
components /ProductDetail.vue
2.5. 详情页源码
<template><div class="product-detail-box"><van-swipe :autoplay="3000"><van-swipe-item v-for="item in gallery" :key="item.id"><img :src="item.img_url" /></van-swipe-item></van-swipe><div class="info" v-if="info.name"><p class="info-name">{{ info.name }}</p><p class="info-brief">{{ info.goods_brief }}</p><p class="info-price">{{ info.retail_price | moneyFlrmat }}</p></div><div class="attribute"><div class="mytitle"><span></span><h3>商品参数</h3></div><ul><li v-for="item in attribute" :key="item.id"><span class="attribute-name">{{ item.name }}</span><span class="attribute-value">{{ item.value }}</span></li></ul></div><!-- 产品详情 --><div class="mytitle"><span></span><h3>产品详情</h3></div><!-- 产品描述信息 --><div class="goods_desc" v-html="info.goods_desc"></div><div class="mytitle"><span></span><h3>常见问题</h3></div><!-- 常见问题 --><ul class="issue"><li v-for="item in issue" :key="item.id"><h3>{{ item.question }}</h3><p>{{ item.answer }}</p></li></ul><!-- 产品列表 --><Weekproduct :newGoodsList="goodsList" title="相关产品"> </Weekproduct><!-- 添加购物车面板 --><van-skuv-model="show"ref="sku":sku="sku":goods="goods":hide-stock="sku.hide_stock"@add-cart="onAddCartClicked"/><!-- 下方购物车 --><van-goods-action><van-goods-action-icon:icon="star_flag ? 'star' : 'star-o'":text="star_flag ? '已收藏' : '未收藏'":color="star_flag ? '#ff5000' : '#323233'"@click="clickFn"/><van-goods-action-iconicon="cart-o"text="购物车":badge="badge"@click="$router.push('/cart')"/><van-goods-action-buttontype="warning"text="加入购物车"@click="addCar"/><van-goods-action-button type="danger" text="立即购买" /></van-goods-action></div>
</template><script>
import { getDetailData, AddToCart, GetGoodsRelatedData,GetCartCountData } from "@/https/http";
import Weekproduct from "@/views/home/Weekproduct";export default {name: "product-detail",data() {return {gallery: [],info: {},attribute: [], //参数show: false,sku: {tree: [], //规格类目 颜色 尺寸 。。。price: "", // 默认价格(单位元)stock_num: 227, // 商品总库存// 数据结构见下方文档hide_stock: false, //是否隐藏剩余库存},goods: {// 默认商品 sku 缩略图picture: ''},productList: [], // 当前产品信息issue: [],goodsList: [],star_flag: false,badge:0,};},created() {this.GetDetailData()this.getRelatedData()this.getCartData()},methods: {addCar() {this.show = true},// 加入购物车onAddCartClicked() {console.log(666,this.$refs.sku.getSkuData());let obj = {}obj.goodsId = this.$route.query.idobj.productId = this.productList[0].idobj.number = this.$refs.sku.getSkuData().selectedNumAddToCart(obj).then((res) => {console.log(res);// 显示添加成功this.$toast.success("添加成功");this.getCartData()})// 隐藏 商品规格面板this.show = false;},// 获取产品明细数据列表GetDetailData() {getDetailData({ id: this.$route.query.id }).then((res) => {console.log(33, res);this.gallery = res.data.gallery;this.info = res.data.info;this.productList = res.data.productList;this.attribute = res.data.attribute;this.issue = res.data.issue;this.goods.picture = res.data.info.list_pic_urlthis.sku.price = res.data.info.retail_pricethis.sku.stock_num = res.data.info.goods_number});},// 获取相关产品数据列表getRelatedData() {GetGoodsRelatedData({ id: this.$route.query.id }).then((res) => {console.log(3366, res);this.goodsList = res.data.goodsList});},// 获取购物车商品数量getCartData(){GetCartCountData().then((res)=>{console.log(7778,res);this.badge = res.data.cartTotal.goodsCount})},clickFn() {this.star_flag = !this.star_flagif (this.star_flag) {this.$toast('收藏成功')} else {this.$toast('取消宝贝收藏成功')}}},components: {Weekproduct}
};
</script><style lang="less" scoped>
.product-detail-box {font-size: 14px;line-height: 30px;padding-bottom: 100px;img {width: 100%;}.info {text-align: center;.info-brief {color: #666;}.info-price {color: red;}}.attribute {ul {li {border-bottom: 1px solid #eee;font-size: 12px;display: flex;.attribute-name {width: 15%;}.attribute-value {flex: 1;}}}}.mytitle {text-align: center;font-size: 16px;margin-top: 20px;position: relative;height: 50px;span {width: 50%;height: 2px;background-color: #ccc;display: inline-block;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}h3 {width: 30%;background-color: #fff;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);}}.issue {li {h3 {padding-left: 10px;line-height: 20px;position: relative;&:before {content: "";width: 4px;height: 4px;border-radius: 50%;background-color: red;display: inline-block;position: absolute;left: 2px;top: 50%;margin-top: -2px;}}margin-bottom: 15px;}}/deep/.goods_desc {img {width: 100%;}}
}
</style>