Vue2项目之移动端购物商城(三) 首页的实现,搜索页和搜索历史记录的实现,搜索列表页的实现,分类页的实现

文章目录

一.首页的实现

由于路由重定向,首页的实现实际上是对在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-iconarrow-uparrow-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)
  }
},
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端OnTheRun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值