一.首页的实现
由于路由重定向,首页的实现实际上是对在Layout嵌套下的二级路由home.vue的动态渲染
在utils/vant-ui.js中按需引入用到的vant组件,过程略

1.静态页面布局
<template>
<div class="wrapper">
<!-- 导航条 -->
<van-nav-bar class='navbar' title-class='custom-title' title="智慧商城" />
<!-- 搜索框 -->
<van-search
readonly
snape='round'
placeholder='请输入搜索关键词'
@click="$router.push('/search')"
></van-search>
<!-- 轮拨图 -->
<van-swipe class="my-swipe" :autoplay="3000" fndicator-color="white">
<van-swipe-item><img class='swiper-img' src="https://img2.baidu.com/it/u=2410402552,916832724&fm=253&fmt=auto&app=120&f=JPEG?w=640&h=400" alt=""></van-swipe-item>
<van-swipe-item><img class='swiper-img' src="https://img2.baidu.com/it/u=97817724,2352783115&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500" alt=""></van-swipe-item>
<van-swipe-item><img class='swiper-img' src="https://img2.baidu.com/it/u=3232769293,874533028&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500" alt=""></van-swipe-item>
<van-swipe-item><img class='swiper-img' src="https://img0.baidu.com/it/u=2188800640,3265288344&fm=253&fmt=auto&app=138&f=JPEG?w=513&h=912" alt=""></van-swipe-item>
</van-swipe>
<!-- 导航 -->
<van-grid column-num="5" icon-size="40">
<van-grid-item v-for="item in 10" :key="item" icon="" text="新品首发"></van-grid-item>
</van-grid>
<!-- 主会场 -->
<div class="main">
<img src="@/assets.png" alt="">
</div>
<!-- 猜你喜欢 -->
<div class="guess">
<p class="guess-list">--猜你喜欢--</p>
<div class="goods-list">
<!-- 后续封装成GoodItem -->
<div class="goods-item" v-for="item in 10" :key="item" @click="$router.push('/productdetail')">
<div class="left"><img src="https://img2.baidu.com/it/u=1927218297,407093841&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1069" alt=""></div>
<div class="right">
<p class="tit text-ellspsis-2">
三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景
5G手机 游戏拍照旗舰机S23
</p>
<p class="count">已售104件</p>
<p class="price">
<span class="new">¥3999.00</span>
<span class="old">¥6659.00</span>
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'homePage'
}
</script>
<style lang="less" scoped>
/* 页面容器 */
.wrapper {
background-color: #f7f8fa;
min-height: 100vh;
width:100%;
overflow-x: hidden;
}
/* 导航条 */
.van-nav-bar {
background: #c21401;
}
::v-deep .van-nav-bar__title {
color: white;
}
/* 搜索框 */
.van-search {
padding: 10px 12px;
}
.van-search__content {
border-radius: 20px;
}
/* 轮播图 */
.my-swipe {
height: 180px;
margin: 10px;
border-radius: 8px;
overflow: hidden;
}
.swiper-img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 导航网格 */
.van-grid {
margin: 15px 0;
background-color: white;
padding: 10px 0;
}
.van-grid-item {
font-size: 12px;
color: #333;
}
/* 主会场 */
.main {
background-color: white;
border-radius: 8px;
text-align: center;
font-weight: bold;
color: #333;
}
.main img {
width: 100%;
margin-top: 10px;
border-radius: 4px;
}
/* 猜你喜欢 */
.guess {
margin: 15px;
background-color: white;
border-radius: 8px;
padding: 12px;
}
.guess-list {
text-align: center;
font-size: 16px;
color: #333;
margin-bottom: 15px;
font-weight: bold;
}
/* 商品列表 */
.goods-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.goods-item {
display: flex;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid #f5f5f5;
}
.goods-item:last-child {
border-bottom: none;
}
.left {
width: 120px;
height: 120px;
flex-shrink: 0;
}
.left img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.tit {
font-size: 14px;
color: #333;
line-height: 1.4;
display: -webkit-box;
// -webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.count {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.price {
margin-top: 8px;
}
.new {
font-size: 16px;
color: #f44;
font-weight: bold;
}
.old {
font-size: 12px;
color: #999;
text-decoration: line-through;
margin-left: 8px;
}
</style>
2.封装请求首页数据的接口
- 查看接口文档:确认请求方式,请求路径和请求参数

- 新建
api/home.js封装请求
import request from '@/utils/request.js'
export const getHomeData = () => {
return request.get('/page/detail')
}
3.页面调用

//Layout/home.vue
import { getHomeData } from '@/api/home.js'
import GoodsItem from '@/components/GoodsItem.vue'
export default {
name: 'homePage',
components: {
GoodsItem
},
data () {
return {
bannerList: [], // 轮拨
navList: [], // 导航
productList: []// 商品
}
}
methods: {
async getHomeDataMethods () {
// const res = await getHomeData()
const { data: { pageData: { items } } } = await getHomeData()
console.log(items)
this.bannerList = items[1].data
this.navList = items[3].data
this.productList = items[6].data
// console.log(this.bannerList, this.navList, this.productList)
}
},
created () {
this.getHomeDataMethods()
},
4.动态渲染+父传子
把"猜你喜欢"中的可复用代码片段提取成单独的子组件GoodsItem.vue
- 父组件
home.vue
<template>
<div class="home">
......
<!-- 轮拨图 -->
<van-swipe class="my-swipe" :autoplay="3000" fndicator-color="white" >
<van-swipe-item v-for="item in bannerList" :key="item.imgUrl"><img :src="item.imgUrl" alt=""></van-swipe-item>
</van-swipe>
<!-- 导航 -->
<van-grid column-num="5" icon-size="40">
<van-grid-item v-for="item in navList" :key="item.imgUrl" :icon="item.imgUrl" :text="item.text"></van-grid-item>
</van-grid>
<!-- 主会场 -->
<div class="main">
<img src="@/assets/main.png" alt="" style="width:375px">
</div>
<!-- 猜你喜欢 -->
<div class="guess">
<p class="guess-list">--猜你喜欢--</p>
<div class="goods-list">
<!-- 后续封装成GoodItem子组件 -->
<!-- 父传子:自定义属性item -->
<GoodsItem v-for="item in productList" :key="item.id" :item="item"></GoodsItem>
</div>
</div>
</div>
</template>
<script>
import { getHomeData } from '@/api/home.js'
import GoodsItem from '@/components/GoodsItem.vue'
export default {
name: 'homePage',
components: {
GoodsItem
},
data () {
return {
bannerList: [], // 轮拨
navList: [], // 导航
productList: []// 商品
}
},
watch: {},
computed: {},
methods: {
async getHomeDataMethods () {
// const res = await getHomeData()
const { data: { pageData: { items } } } = await getHomeData()
console.log(items)
this.bannerList = items[1].data
this.navList = items[3].data
this.productList = items[6].data
// console.log(this.bannerList, this.navList, this.productList)
}
},
created () {
this.getHomeDataMethods()
}
}
</script>
踩坑: 动态渲染时v-for要设置在静态布局时多个标签名(li,van-swipe-item等)上,而不是其父级
- 子组件
/components/GoodsItem.vue
<template>
<div>
<div
class="goods-item"
@click="$router.push('/productdetail')"
>
<div class="left"><img :src="item.goods_image" alt="" /></div>
<div class="right">
<p class="tit text-ellspsis-2">{{ item.goods_name }}</p>
<p class="count">已售{{ item.goods_sales }}件</p>
<p class="price">
<span class="new">¥{{ item.goods_price_min }}</span>
<span class="old">¥{{ item.goods_price_max }}</span>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
components: {},
props: {
item: {
type: Object,
default: () => {
// 给item一个默认值:返回值对象
return {}
}
}
}
}
</script>
<style lang='less' scoped>
.goods-item {
height: 148px;
margin-bottom: 6px;
padding: 10px;
background-color: #fff;
display: flex;
.left {
width: 127px;
img {
display: block;
width: 100%;
}
}
.right {
flex: 1;
font-size: 14px;
line-height: 1.3;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
.count {
color: #999;
font-size: 12px;
}
.price {
color: #999;
font-size: 16px;
.new {
color: #f03c3c;
margin-right: 10px;
}
.old {
text-decoration: line-through;
font-size: 12px;
}
}
}
}
</style>
5.GoodsItem路由传参跳转商品详情页
//GoodsItem.vue
<div
class="goods-item"
@click="$router.push(`/productdetail?detail=${item.goods_id}`)"
v-if="item.goods_id"
></div>
//ProductDetail/index.vue==>后续用来决定动态渲染哪件商品的详情页
created () {
console.log('从GoodsITem中获取到了数据:', this.$route.query.detail)
},
6.优化:解决无法更改NavBar组件标题颜色问题
<!-- 导航条 -->
<van-nav-bar title="智慧商城" class="navbar" titleTextColor='white'/>
.home /deep/ .van-nav-bar__title {
color: #ffffff;//白字
}
.navbar{
background:#c21401;//红底
}
7.优化:新增滚动广播公告栏
使用到了vant库的van-notice-bar组件

<!-- 滚动广播公告 -->
<div class="notice-container">
<van-notice-bar left-icon="volume-o" :text="noticeText" :speed="50" scrollable color="white"
background="#303133" style="font-size: 20px;" />
</div>
......
// 滚动广播公告
.notice-container {
margin: 10px;
border-radius: 4px;
overflow: hidden;
}
8.首页完整代码
home.vue
<template>
<div class="wrapper">
<!-- 导航条 -->
<van-nav-bar class='navbar' title-class='custom-title' title="智慧商城" />
<!-- 搜索框 -->
<van-search readonly snape='round' placeholder='请输入搜索关键词' @click="$router.push('/search')"></van-search>
<!-- 轮拨图 -->
<van-swipe class="my-swipe" :autoplay="3000" fndicator-color="white">
<van-swipe-item v-for="item in bannerList" :key="item.imgUrl"><img class='swiper-img' :src="item.imgUrl"
alt=""></van-swipe-item>
</van-swipe>
<!-- 滚动广播公告 -->
<div class="notice-container">
<van-notice-bar left-icon="volume-o" :text="noticeText" :speed="50" scrollable color="white"
background="#303133" style="font-size: 20px;" />
</div>
<!-- 导航 -->
<van-grid column-num="5" icon-size="40">
<van-grid-item v-for="item in navList" :key="item.imgUrl" :icon="item.imgUrl" :text="item.text"></van-grid-item>
</van-grid>
<!-- 主会场 -->
<div class="main">
<img src="@/assets/main.png" alt="">
</div>
<!-- 猜你喜欢 -->
<div class="guess">
<p class="guess-list">--猜你喜欢--</p>
<div class="goods-list">
<!-- 后续封装成GoodsItem -->
<GoodsItem v-for="item in productList" :key="item.goods_id" :item="item"></GoodsItem>
</div>
</div>
</div>
</template>
<script>
import GoodsItem from '@/components/GoodsItem.vue'
import { getHomeDataAPI } from '@/api/home'
export default {
name: 'homePage',
components: {
GoodsItem
},
data () {
return {
bannerList: [],
navList: [],
productList: [],
noticeText: '智慧商城2.0全新上线,更多新品等你来选~ '
}
},
created () {
this.getHomeData()
},
methods: {
// 获取首页数据
async getHomeData () {
const { data: { pageData: { items } } } = await getHomeDataAPI()
this.bannerList = items[1].data
this.navList = items[3].data
this.productList = items[6].data
console.log(this.bannerList, this.navList, this.productList)
}
}
}
</script>
<style lang="less" scoped>
/* 页面容器 */
.wrapper {
background-color: #f7f8fa;
min-height: 100vh;
width: 100%;
overflow-x: hidden;
}
/* 导航条 */
.van-nav-bar {
background: #c21401;
}
::v-deep .van-nav-bar__title {
color: white;
}
/* 搜索框 */
.van-search {
padding: 10px 12px;
}
.van-search__content {
border-radius: 20px;
}
/* 轮播图 */
.my-swipe {
height: 180px;
margin: 10px;
border-radius: 8px;
overflow: hidden;
}
.swiper-img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 导航网格 */
.van-grid {
margin: 15px 0;
background-color: white;
padding: 10px 0;
}
.van-grid-item {
font-size: 12px;
color: #333;
}
/* 主会场 */
.main {
background-color: white;
border-radius: 8px;
text-align: center;
font-weight: bold;
color: #333;
}
.main img {
width: 100%;
margin-top: 10px;
border-radius: 4px;
}
/* 猜你喜欢 */
.guess {
margin: 15px;
background-color: white;
border-radius: 8px;
padding: 12px;
}
.guess-list {
text-align: center;
font-size: 16px;
color: #333;
margin-bottom: 15px;
font-weight: bold;
}
/* 商品列表 */
.goods-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.goods-item {
display: flex;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid #f5f5f5;
}
.goods-item:last-child {
border-bottom: none;
}
.left {
width: 120px;
height: 120px;
flex-shrink: 0;
}
.left img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.tit {
font-size: 14px;
color: #333;
line-height: 1.4;
display: -webkit-box;
// -webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.count {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.price {
margin-top: 8px;
}
.new {
font-size: 16px;
color: #f44;
font-weight: bold;
}
.old {
font-size: 12px;
color: #999;
text-decoration: line-through;
margin-left: 8px;
}
// 滚动广播公告
.notice-container {
margin: 10px;
border-radius: 4px;
overflow: hidden;
}
</style>
二.搜索页和历史记录的实现
0.需求
- 在首页的假搜索框点击,跳转真正的搜索页
- 搜索历史的基本渲染
- 点击搜索,追加历史
点击搜索按钮或历史记录,都能搜索
若之前没有相同搜索关键字,直接追加一个历史记录在最前面
若之前有相同的搜索关键字,把该历史记录挪到最前面(原有关键字移除,再重新追加)
- 持久化:保证刷新时,搜索历史不丢失
1.静态页面布局
搜索页Search/ndex.vue
<template>
<div class="search">
<van-nav-bar title="商品搜索" left-arrow @click-left="$router.go(-1)" />
<van-search v-model="search" show-action placeholder="请输入搜索关键词" clearable>
<template #action>
<div @click="goSearch(search)">搜索</div>
</template>
</van-search>
<!-- 搜索历史 -->
<div class="search-history" v-if="history">
<div class="title">
<span>最近搜索</span>
<!-- 按需导入Icon组件 -->
<van-icon name="delete-o" size="16"></van-icon>
</div>
<div class="list">
<span class="list-item" v-for="item in history" :key="item" :item="item">{{item}}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'searchIndex',
components: {},
props: {},
data () {
return {
history: [],
search: ''
}
},
watch: {},
computed: {},
methods: {},
created () {},
mounted () {}
}
</script>
<style lang="less" scoped>
.search {
.searchBtn {
background-color: #fa2209;
color: #fff;
}
::v-deep .van-search__action {
background-color: #c21401;
color: #fff;
padding: 0 20px;
border-radius: 0 5px 5px 0;
margin-right: 10px;
}
::v-deep .van-icon-arrow-left {
color: #333;
}
.title {
height: 40px;
line-height: 40px;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15px;
}
.list {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
padding: 0 10px;
gap: 5%;
}
.list-item {
width: 30%;
text-align: center;
padding: 7px;
line-height: 15px;
border-radius: 50px;
background: #fff;
font-size: 13px;
border: 1px solid #efefef;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-bottom: 10px;
}
}
</style>
效果

2.基础功能的业务实现
2.1.点击搜索按钮或历史关键字触发点击事件并传参
- data定义变量
data(){
search:"",//搜索框的内容(v-model双向绑定)
history:['手机','电脑','冰箱']//历史记录(测试用)
}
- 在点击搜索和遍历历史记录的地方绑定,并在methods中定义goSearch点击事件
<!-- 绑定点击事件goSearch:形参search,它是input框输入的数据 -->
<div @click="goSearch(search)">搜索</div>
...
<!-- 绑定点击事件goSearch:形参item -->
<span class="list-item" @click="goSearch(item)" v-for="item in history" :key="item">{{item}}</span>
2.2.追加历史记录(在点击事件中实现)
methods: {
// 如何往历史记录里面追加?
goSearch (key) {
console.log(key)// key用于判断是否已经存在该关键词
const index = this.history.indexOf(key)
if (index !== -1) { // 该关键词存在:添加到最前面(即先删除再unshift进去)
this.history.splice(index, 1)
}
this.history.unshift(key)// 该关键词不存在:直接unshift进去
this.search = ''//清空文本框内容
},
2.3.清空历史记录
<!-- 按需导入Icon组件 -->
<van-icon name="delete-o" size="16" @click='handleClear'></van-icon>
...
handleClear(){
this.history=[]
}
3.历史记录的持久化保存
step1:封装历史记录的localStorage存取功能
//utils/storage.js
// 实现历史记录的本地化存储的增删改查封装---搜索历史记录
const HISTORY_KEY = 'my-shoping-historylist'
// 存
export const setHistory = (arr) => {
localStorage.setItem(HISTORY_KEY, JSON.stringify(arr))
}
// 取
export const getHistory = () => {
const result = localStorage.getItem(HISTORY_KEY)
return result ? JSON.parse(result) : []
}
step2:按需导入并在合适的地方调用
import { getHistory, setHistory } from '@/utils/storage.js'
...
//data
// history: ['手机', '电脑', '平板']
history: getHistory()
//goSearch
this.history.unshift(key)
setHistory(this.history)//存入本地
this.search = ''
//handleClear
this.history = []//清空历史记录
setHistory([])//本地也清空

三.对搜索页的优化
1.从首页点击假搜索框跳转搜索页时,input框自动获得焦点
- 首页home.vue的
<van-search></van-search>绑定了跳转搜索页的点击事件 - 注释掉搜索页的
van-search,使用原生input
//Layout/home.vue=>点击跳转搜索页
<van-search @click="$router.push('/search')></van-search>
//search./index.vue==>自动获取焦点
//注释掉van-search
<!-- 搜索框 -->
<!-- <van-search
v-model="search"
show-action
placeholder="请输入搜索关键词"
clearable
v-if="showSearch"
:autofocus="true"
ref="searchInput"
>
<template #action>
<div @click="goSearch(search)">搜索</div>
</template>
</van-search> -->
//改用input
<!-- 搜索框 -->
<div class="custom-search">
<div class="search-box">
<span class="search-icon">⌕</span>
<input
type="text"
v-model="search"
placeholder="请输入搜索关键词"
class="search-input"
ref="searchInput"
@keyup.enter="goSearch(search)"
/>
</div>
<div class="search-btn" @click="goSearch(search)">搜索</div>
</div>
- 使用ref和$ref实现自动获取焦点
//input
ref="searchInput"
//mounted
this.$nextTick(() => {
this.$refs.searchInput.focus()
})
- 最后再补上样式
// 原生HTML搜索框
/* 容器层 */
.custom-search {
display: flex;
align-items: center;
padding: 8px 16px;
background: #f7f8fa;
border-radius: 16px;
box-sizing: border-box;
width: 100%;
max-width: 750px; /* 适配移动端最大宽度 */
}
/* 搜索区域 */
.search-box {
flex: 1;
display: flex;
align-items: center;
background: #f1f2f3;
border-radius: 4px;
padding: 6px 5px;
transition: border-color 0.2s;
color: #dadae4;
font-size: 14px;
line-height: 24px;
border: 1px solid #f1f2f3;
}
.search-box:focus-within {
border-color: #1989fa; /* Vant主色 */
}
/* 输入框 */
.search-input {
flex: 1;
outline: none;
font-size: 14px;
line-height: 1.5;
background: transparent;
padding: 0 8px;
color: #333;
border: none;
box-sizing: border-box;
width: 100%;
line-height: inherit;
text-align: left;
background-color: transparent;
resize: none;
}
.search-input::placeholder {
color: #b9b9c2;
font-size: 14px;
}
/* 搜索图标 */
.search-icon {
color: #b9b9c2;
font-size: 20px;
margin-right: 8px;
}
/* 搜索按钮 */
.search-btn {
margin-left: 12px;
border: none;
background: transparent;
font-size: 14px;
line-height: 28px;
background-color: #c21401;
color: #fff;
padding: 0 20px;
border-radius: 0 5px 5px 0;
margin-right: 10px;
cursor: pointer;
transition: opacity 0.2s;
}
.search-btn:active {
opacity: 0.6;
}

2.用户输入完搜索词不用点击搜索按钮,回车也能实现相同效果
//input
@keyup.enter="goSearch(search)"
//van-search(如果保留van-seatch,可以用它封装的@search)
@search="goSearch(search)"
四.搜索列表页的实现
搜索列表页是用户输入完搜索词后匹配到的商品列表页面,
需要从搜索页路由传参跳转
0.路由跳转
搜索页通过查询参数传参给搜索列表页,
列表页要将获取到的传参同步显示到自己的input框上
//Search/index.vue
goSearch(key){
...
this.$router.push(`/list?search=${key}`)
}
//Search/list.vue
//调用
<van-search :value="querySearch||'搜索商品'"></van-search>
...
computed:{
querySearch(){
return this.$route.query.search
}
}

1.静态页面布局
Search/list.vue
<template>
<div class="search">
<van-nav-bar
fixed
title="商品列表"
left-arrow
@click-left="$router.go(-1)"
/>
<van-search
readonly
shape="round"
background="#ffffff"
value="手机"
show-action
@click="$router.push('/search')"
>
<template #action>
<van-icon class="tool" name="apps-o" />
</template>
</van-search>
<!-- 排序选项按钮 -->
<div class="sort-btns">
<div class="sort-item">综合</div>
<div class="sort-item">销量</div>
<div class="sort-item">价格</div>
</div>
<div class="goods-list">
<GoodsItem v-for="item in 10" :key="item"></GoodsItem>
</div>
</div>
</template>
<script>
export default {
name: 'listIndex',
components: {},
props: {},
data () {
return {}
},
watch: {},
computed: {},
methods: {},
created () {},
mounted () {}
}
</script>
<style lang="less" scoped>
.search {
padding-top: 46px;
::v-deep .van-icon-arrow-left {
color: #333;
}
.tool {
font-size: 24px;
height: 40px;
line-height: 40px;
}
.sort-btns {
display: flex;
height: 36px;
line-height: 36px;
.sort-item {
text-align: center;
flex: 1;
font-size: 16px;
}
}
}
// 商品样式
.goods-list {
background-color: #f6f6f6;
}
</style>
2.查看文档和封装接口


//新建api/product.js
import request from '@/utils/request'
export const getProductList = (obj) => {
const { categoryId, goodsName, page } = obj
return request.get('/goods/list', {
params: {
categoryId,
goodsName,
page
}
})
}
3.调用接口
data () {
return {
page: 1,
produList: []
}
},
computed: {
querySearch () {
return this.$route.query.search
}
},
methods: {
async getProductListMethods () {
const { data: { list } } = await getProductList({
goodsName: this.querySearch,
page: this.page
})
this.produList = list.data
console.log('获取到的数据:', list.data)
}
},
created () {
this.getProductListMethods()
},
4.用获取到的productList动态渲染页面
引入和调用GoodsItem子组件并通过父子组件通信传参
<div class="goods-list">
<GoodsItem
v-for="item in productList"
:key="item.goods_id"
:item="item"
></GoodsItem>
</div>
import GoodsItem from '@/components/GoodsItem.vue'
...
components: {
GoodsItem
},

(别再屏蔽俺的图片了…)
五.对列表页的优化
1.搜索词querySearch匹配商品列表的商品名productList[0].goods_name
若匹配到结果则正常显示,若没有匹配到任何商品名,显示v-else
- 判断是否有匹配的商品
computed: {
// 获取搜索词
querySearch () {
return this.$route.query.search
},
// 过滤匹配搜索词的商品
filteredProductList () {
const keyword = this.querySearch.toLowerCase()
return this.productList.filter(product =>
product.goods_name.toLowerCase().includes(keyword)
)
}
},
- 新增一个
v-else逻辑并提供样式
<!-- 若传过来的数据是空的,则显示"亲,暂无相关数据~" -->
<div class="goods-list" v-if="hasMatchedProductList.length">
<GoodsItem v-for="item in productList" :key="item.goods_id" :item="item"></GoodsItem>
</div>
<div class="goods-list empty-state" v-else>
<img src="http://smart-shop.itheima.net/static/empty.png" alt="">
<p>亲,暂无相关数据~</p>
</div>
// 空数据样式
.empty-state {
text-align: center;
padding: 20px;
}
.empty-state img {
width: 150px;
height: 150px;
opacity: 0.6;
}
- 最终效果

2.实现排序选项按钮的点击高亮
//html
<!-- 排序选项按钮 -->
<div class="sort-btns">
<div class="sort-item" :class="{active:sortType==='default'}" @click="sortType='default'">综合</div>
<div class="sort-item" :class="{active:sortType==='sales'}" @click="sortType='sales'">销量</div>
<div class="sort-item" :class="{active:sortType==='price'}" @click="sortType='price'">价格</div>
</div>
//js
data () {
return {
......
sortType: 'default'
}
},
//css
.sort-item.active {
color: #e49a3d;
}

3.实现商品列表按销量和价格排序
//html
<!-- 排序选项按钮 -->
<div class="sort-btns">
<!-- step1:绑定点击排序和高亮的事件 -->
<div class="sort-item" :class="{active:sortType==='default'}" @click="changeSort('default')">综合</div>
<div class="sort-item" :class="{active:sortType==='sales'}" @click="changeSort('sales')">销量</div>
<div class="sort-item" :class="{active:sortType==='price'}" @click="changeSort('price')">价格</div>
</div>
......
<!-- step5:用排序后的商品列表渲染页面 -->
<div class="goods-list" v-if="filteredProductList.length">
<GoodsItem v-for="item in sortedProductList" :key="item.goods_id" :item="item"></GoodsItem>
</div>
//js
data () {
return {
......
sortType: 'default',
// step2:声明用于记录当前排序类型的变量
sortOrder: 'asc'// 排序顺序,默认为升序
}
}.
methods: {
......
// step3:点击时切换排序类型和顺序
changeSort (type) {
if (this.sortType === type) {
// 再次点击同一排序类型:切换排序(升/降)
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'
} else {
// 点击不同排序类型(价格/销量):重置为升序
this.sortType = type
this.sortOrder = 'asc'
}
}
},
computed: {
......
// step4:返回经过排序后的商品列表
sortedProductList () {
const list = [...this.filteredProductList]
switch (this.sortType) {//是什么排序类型?
case 'sales'://是对价格排序
this.sortOrder === 'asc' ? list.sort((a, b) => a.sales - b.sales) : list.sort((a, b) => b.sales - a.sales)//是升序还是降序?
break
case 'price':
this.sortOrder === 'asc' ? list.sort((a, b) => a.price - b.price) : list.sort((a, b) => b.price - a.price)
break
}
// 再次点击同一排序类型时切换顺序
if (this.sortOrder === 'desc') {
list.reverse()
}
return list
}
}
效果:按销量降序排列

4.优化:新增代表升降序的上下箭头小图标并高亮
使用到了vant库中van-icon的arrow-up和arrow-down图标
<div class="sort-item" :class="{ active: sortType === 'sales' }" @click="changeSort('sales')">
销量
<!-- 新增指示升降序高亮的上下箭头图标 -->
<van-icon v-if="sortType === 'sales'" :name="sortOrder === 'asc' ? 'arrow-up' : 'arrow-down'" />
</div>
<div class="sort-item" :class="{ active: sortType === 'price' }" @click="changeSort('price')">
价格
<!-- 新增指示升降序高亮的上下箭头图标 -->
<van-icon v-if="sortType === 'price'" :name="sortOrder === 'asc' ? 'arrow-up' : 'arrow-down'" />
</div>
六.分类页的实现
分类页是嵌套在LayOut下的二级路由category.vue,
在底部导航栏中显示,以及在列表页的右上角图标中点击跳转
1.静态页面布局
<template>
<div class="category">
<!-- 分类 -->
<van-nav-bar title="全部分类" fixed />
<!-- 搜索框 -->
<van-search
readonly
shape="round"
background="#f1f1f2"
placeholder="请输入搜索关键词"
@click="$router.push('/search')"
/>
<!-- 分类列表 -->
<div class="list-box">
<div class="left">
<ul>
<li v-for="(item) in 10" :key="item">
<a>{{ item.name }}</a>
</li>
</ul>
</div>
<div class="right">
<div
@click="$router.push(`/searchlist`)"
v-for="item in 10"
:key="item"
class="cate-goods"
>
<img src="" alt="" />
<p>手机</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CategoryPage',
components: {},
props: {},
data () {
return {
}
},
watch: {},
computed: {},
methods: {},
created () {},
mounted () {}
}
</script>
<style lang="less" scoped>
.category {
padding-top: 100px;
padding-bottom: 50px;
height: 100vh;
.list-box {
height: 100%;
display: flex;
.left {
width: 85px;
height: 100%;
background-color: #f3f3f3;
overflow: auto;
a {
display: block;
height: 45px;
line-height: 45px;
text-align: center;
color: #444444;
font-size: 12px;
&.active {
color: #fb442f;
background-color: #fff;
}
}
}
.right {
flex: 1;
height: 100%;
background-color: #ffffff;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
padding: 10px 0;
overflow: auto;
.cate-goods {
width: 33.3%;
margin-bottom: 10px;
img {
width: 70px;
height: 70px;
display: block;
margin: 5px auto;
}
p {
text-align: center;
font-size: 12px;
}
}
}
}
}
// 导航条样式定制
.van-nav-bar {
z-index: 999;
}
// 搜索框样式定制
.van-search {
position: fixed;
width: 100%;
top: 46px;
z-index: 999;
}
</style>
2.查看接口并封装

//新建utils/category.js
import request from '@/utils/request'
export const getCategoryData = () => {
return request.get('/category/list')
}
3.页面中调用
import { getCategoryData } from '@/api/category'
...
data () {
return {
productList: [],
activeIndex: 0
}
},
created () {
this.getCategoryList()
},
methods: {
async getCategoryList () {
const {
data: { list }
} = await getCategoryData()
this.productList = list
}
}
4.动态渲染页面
用productList渲染页面,
在data声明activeIndex,用于存放点击左边分类元素的索引,并在渲染右边当前分类下的商品时使用它
<!-- 分类列表 -->
<div class="list-box">
<div class="left">
<ul>
<!-- 左边实现功能:
1.动态渲染页面
2.当前分类后把当前索引传给变量activeIndex,
3.被点击的分类赋给类名active,获得选中样式 -->
<li v-for="(item,index) in productList" :key="item.category_id">
<a
href="javascript:;"
:class="{active:index===activeIndex}"
@click="activeIndex=index"
>{{ item.name }}</a
>
</li>
</ul>
</div>
<div class="right">
<!-- 右边实现的功能:
1.动态渲染页面
2.不是每个productList中的对象元素都有children属性的,此处使用可选链"?."代替"."
3.如果有children属性,就遍历当前点击分类的序号对应的第index个productList数组元素中的children数组
动态渲染children数组下的name属性和image属性下的external_url(可选)
-->
<div v-for="item in productList[activeIndex]?.children"
:key="item.category_id"
class="cate-goods"
>
<img :src="item.image?.external_url"/>
<p>{{item.name}}</p>
</div>
</div>
</div>
productList下对象元素的结构:


渲染效果:

六.对分类页的优化
1.列表页list.vue点击小图标跳转分类页category.vue
list.vue
<template #action>
<van-icon class="tool" name="apps-o" @click="$router.push('/category')" />
</template>
2.category分类页中点击商品,路由传参跳转list页
当前点击分类页/Layout/category.vue下的商品跳转搜索列表页/Search/list.vue时没有传参,跳转时需要带上参数categoryId,优化如下:
-
观察分类页中获得的productList数组中,每一个对象元素都有一个category_Id属性

-
分类页:查询参数传参
<div v-for="item in productList[activeIndex]?.children"
:key="item.category_id"
class="cate-goods"
@click="$router.push(`/list?categoryId=${item.category_id}`)"
>
- 列表页:接收参数
//名字太长了在computed中封装一下
computed: {
querySearch () {
return this.$route.query.search
},
queryCategoryId () {
return this.$route.query.categoryId
}
},
//接收参数
methods: {
async getProductListMethods () {
const {
data: { list }
} = await getProductList({
categoryId: this.queryCategoryId,
goodsName: this.querySearch,
page: this.page
})
this.productList = list.data
}
},
created () {
this.getProductListMethods()
},
效果:分类页点击某个分类的商品,跳转进入列表页不再渲染全部商品,只渲染该分类的商品
3.list页分别对search页和category页跳转而来的传参做条件渲染
要求:
- 搜索页携带
搜索词search跳转,list页对商品列表数据条件渲染作为搜索结果,若无数据显示空状态 - 分类页携带
categoryId跳转,list页对商品列表数据条件渲染若无数据显示空状态 - 在list页获得后台数据之前,先加载骨架屏
实现代码:
- html
<!-- 1.骨架屏加载状态 -->
<van-skeleton v-if="loading" title avatar :row="3" :row-width="['40%', '80%', '60%']" style="margin-top: 10px;" />
<!-- 2.有数据时,渲染商品列表 -->
<!-- 分类商品列表 -->
<div class="goods-list" v-else-if="hasData">
<!-- 分类商品列表 -->
<template v-if="queryCategoryId">
<GoodsItem v-for="item in productList" :key="item.goods_id" :item="item" />
</template>
<!-- 搜索结果列表 -->
<template v-else-if="querySearch">
<GoodsItem v-for="item in filteredProductList" :key="item.goods_id" :item="item" />
</template>
</div>
<!-- 3.无数据时,显示空状态 -->
<div class="goods-list empty-state" v-else>
<img src="http://smart-shop.itheima.net/static/empty.png" alt="">
<p>{{ querySearch ? '未找到相关商品' : '暂无商品数据' }}</p>
</div>
- js
data () {
return {
......
loading: false,
hasData: false
}
},
computed: {
// 获取搜索词
querySearch () {
return this.$route.query.search
},
// 获取分类Id
queryCategoryId () {
return this.$route.query.categoryId
},
// 返回过滤匹配搜索词后的商品列表
filteredProductList () {
const keyword = this.querySearch.toLowerCase()
return this.productList.filter(product =>
product.goods_name.toLowerCase().includes(keyword)
)
}
},
methods: {
// 获取商品列表
async getProductList () {
this.loading = true
const { data: { list } } = await getProductListAPI({
page: this.page,
goodsName: this.querySearch,
categoryId: this.queryCategoryId
})
this.productList = list.data
this.hasData = list.data.length > 0
this.loading = false
console.log('获取到的商品列表:', this.productList)
}
},
1066

被折叠的 条评论
为什么被折叠?



