Vue3.2 电商前台项目(三)—— Home页面和Search页面(axios的封装、分页器的封装)

码云仓库地址

一、 Home组件

开发流程

  • 拆分静态组件 | 页面
  • 配置 api 请求
  • vuex 配置
  • 组件获取数据,动态展示数据

1. 全局组件:三级联动组件的实现

  • 三级联动组件在多个位置(home、search、detail)使用到了,所以将其注册为全局组件(只需要注册一次,就可以在任意需要的地方使用)
  1. 新建 src / components / TypeNav / index.vue
  2. main.js 中进行全局注册
    全局注册时接收两个参数:第一个参数为全局组件的名字,后续使用用到的就是这个名字;第二个参数为要注册的组价
// main.js

// 导入三级联动组件
import TypeNav from '@/components/TypeNav'
createApp(App)
  .component('type-nav', TypeNav) // 全局注册三级联动组件
  .mount('#app')
  1. 使用
    src / view / Home / index.vue
<!-- 三级联动组件 -->
    <type-nav></type-nav>

2. 首页局部组件

  1. 创建需要的局部组件
  2. 在 views / Home /indec.vue 中引入和使用
<template>
  <div>
    <!-- 三级联动组件 -->
    <type-nav></type-nav>
    <!-- 列表组件 -->
    <list-con></list-con>
    <!-- 今日推荐组件 -->
    <today-recommend></today-recommend>
    <!-- 商品排行组件 -->
    <rank></rank>
    <!-- 猜你喜欢组件 -->
    <like></like>
    <!-- 楼层 -->
    <floor></floor>
    <!-- 楼层 -->
    <floor></floor>
    <!-- 商标组件 -->
    <brand></brand>
  </div>
</template>

<script setup>
import ListCon from './ListContainer'
import todayRecommend from './TodayRecommend'
import rank from './Rank'
import like from './Like'
import floor from './Floor'
import brand from './Brand'
import {} from 'vue'
</script>
<style lang="scss" scoped></style>

3. 接口工具:postman

postman下载链接

  • 本项目中:服务器返回code为200,代表请求成功
  • 整个项目的接口前缀为 /api

4. axios 的二次封装

目的:基于axios封装一个请求工具,调用接口时使用。封装请求拦截器、响应拦截器,进行一些业务处理
在后续需要调用接口时,要引入该工具

  1. 安装 axios
cnpm install --save axios
  1. 新建 src / api / request.js
    api 文件夹,用于存放axios相关的
    request.js 用于二次封装axios
/** *****************axios的二次封装,主要是封装请求拦截器和响应拦截器 ************/
/** 实现步骤
 * 1. 创建一个新的axios实例
 * 2. 请求拦截器,如果有token进行头部携带
 * 3. 响应拦截器
 **/

// 导入axios
import axios from 'axios'

/**
 * TODO-1: 利用axios的create方法,创建一个新的axios实例
 */
const instance = axios.create({
  // axios的配置
  // baseUrl:基础路径,基于哪个路径
  //  如本项目中接口前缀为 /api,设置baseURL的作用就是后续见到访问地址时,会自动加上 /api ,避免每次都去书写
  baseURL: '/api',
  // 请求超时的时间,在5秒内无响应则请求失效
  timeout: 5000
})

/**
 * TODO-2:请求拦截器
 * 在发请求之前,请求拦截器可以进行监测,以便在请求发出前进行一些处理
 */
instance.interceptors.request.use(config => {
  // config:配置对象,其中含请求头header属性
  return config
})

/**
 * TODO-3:响应拦截器
 */
instance.interceptors.response.use(response => {
  // 响应成功的回调
  // 请求成功返回data,后续可直接使用data
  return response.data
}, err => {
  // 响应失败的回调
  // 终止Promise
  return Promise.reject(err)
})

// 对外暴露,外部才可以使用
export default instance

5. API 接口统一管理

  • 项目很少,比如只有几个接口,可以直接在组件生命周期中发送请求
  • 大项目(比如组件上百,接口几十的情况下),可以对接口统一进行管理,这样方便修改
  1. 新建 src / api / index.js 用于接口的统一管理
/** *************** 接口的统一管理 *******************/

// 导入封装好的axios
import requests from './request'

/**
 * 三级联动接口
 */
export const reqCategoryList = () => requests({
  url: '/product/getBaseCategoryList',
  method: 'get'
})

5.1 配置代理,解决跨域问题

module.exports = {
  // 代理跨域
  devServer: {
    proxy: {
      '/api': {
        target: 'http://39.98.123.211' // 服务器地址
        // pathRewrite: { '^/api': '' },
      }
    }
  }
}

6. vuex 模块式开发

  • vuex 是vue官方提供的插件,用于集中式状态管理,管理项目中公用的数据
  1. 新建 src / store / index.js
    如果安装的时候选择了vuex,则该文件会自动创建并在main.js中注册
import { createStore } from 'vuex'

// state:仓库存储数据的地方
const state = {}
// mutations:修改state的唯一手段
const mutations = {}
// actions:处理action,可以处理异步和书写业务逻辑
const actions = {}
// getters:仓库的计算属性,用于简化仓库数据
const getters = {}
export default createStore({
  state,
  mutations,
  actions,
  getters
})

  • 当项目过大,模块多,接口多,可以使用vuex的模块化管理
  • 让代码更好维护,让多种数据分类更加明确
  • 每个组件的state、actions、mutations、getters单独管理,最后引入
  1. 在store目录下建立需要使用vuex的组件文件夹,如:home/index.js、search/index.js,分别管理需要的vuex内容
    在这里插入图片描述
const state = {
   ...
}
const mutations = {
   ...
}
// getters:计算属性,项目中主要用于简化仓库中的数据
const getters = {
    ...
}
const actions = {
   ...
}

export default {
    state,
    mutations,
    getters,
    actions
}
  1. store/index.js中引入
import { createStore } from 'vuex'

import home from './home'
import search from './search'
export default createStore({
// 实现vuex仓库模块式开发存储数据
  modules: {
    home,
    search
  }
})
  • 开发者工具中查看
    在这里插入图片描述

7. TypeNav 三级联动组件

7. 1 三级联动组件的接口请求

  1. Home / index.vue 中,组件挂载完毕就向服务发请求获取三级联动列表
import { onMounted } from 'vue'
import { useStore } from 'vuex'
// 引入vuex的实例store
const store = useStore()
onMounted(() => {
  // 组件挂载完毕后,向服务发请求获取三级联动列表
  store.dispatch('getCategoryList')
})
  1. store / home / index.js 配置
// 导入三级联动的接口函数
import { reqCategoryList } from '@/api/index'
const state = {
  // state中的初始值要和服务器返回的类型保持一致
  categoryList: []
}
const mutations = {
  CATEGORYLIST(state, categoryList) {
    state.categoryList = categoryList
  }
}
const getters = {
}
const actions = {
  // 首页派发的‘getCategoryList’事件
  async getCategoryList({ commit }) {
    // 发送请求
    const result = await reqCategoryList()
    if (result.code === 200) {
    //  请求成功,派发一个mutation,并将返回的数据传递过去
      commit('CATEGORYLIST', result.data)
    }
  }
}

export default {
  state,
  mutations,
  getters,
  actions
}
  1. Home / index.vue 中使用
// 获取请求到的三级联动列表
const categoryList = store.state.home.categoryList

7.2 通过JS给三级联动设置背景色(事件委托)

  • 方式1: 直接修改css,添加一个hover时的背景颜色
 &:hover {
            background-color: skyblue;
          }
  • 通过JS设置 ,在 components / TypeNav / index.vue 中实现
<template>
...
<div class="item" :class="{ active: currentIndex === index }">
	<h3 @mouseenter="changeIndex(index)" @mouseleave="leaveIndex">
   		...
 	</h3>
 </div>
 ...
</template>
<script>
import { ref } from 'vue'
// 背景色的处理
// 存储鼠标移入的那一个分类的index
const currentIndex = ref(-1)
// 鼠标移入修改 currentIndex
const changeIndex = (index) => {
  currentIndex.value = index
}
// 鼠标移除,重置currentIndex
const leaveIndex = () => {
  currentIndex.value = -1
}
</script>
<style lang="scss" scoped>
.item {
     &.active {
       background-color: skyblue;
    }
}
</style>
  • 希望实现的效果:鼠标先在 “图书、音像、电子书” 分类然后移到 “全部商品分类” 时背景色仍然存在,可以通过 事件委托 实现
    在这里插入图片描述
  • 事件委托 || 事件委派:将本来是子元素的事情委派给父元素来处理
  • 此处将三级联动列表和全部商品分类外层包裹一个 div ,将鼠标移除事件交给这个 div
<!-- 事件委托/事件委派 -->
<div @mouseleave="leaveIndex">
        <h2 class="all">全部商品分类</h2>
        <div class="sort">
          <div class="all-sort-list2">
            <div
              class="item"
              v-for="(categoryItem, index) in categoryList"
              :key="categoryItem.categoryId"
              :class="{ active: currentIndex === index }"
            >
              <h3 @mouseenter="changeIndex(index)">
                <a href="">{{ categoryItem.categoryName }}</a>
              </h3>
              <div class="item-list clearfix">
                <div
                  class="subitem"
                  v-for="subItem of categoryItem.categoryChild"
                  :key="subItem.categoryId"
                >
                  <dl class="fore">
                    <dt>
                      <a href="">{{ subItem.categoryName }}</a>
                    </dt>
                    <dd>
                      <em
                        v-for="childItem of subItem.categoryChild"
                        :key="childItem.categoryId"
                      >
                        <a href="">{{ childItem.categoryName }}</a>
                      </em>
                    </dd>
                  </dl>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

7.3 性能优化:函数防抖与函数节流

  • 当短时间内频繁调用函数时(多次触发事件),浏览器会出现卡顿现象
  • 函数节流和函数防抖,两者都是优化高频率执行js代码的一种手段
  • 函数节流:在规定事件间隔范围内不会触发触发回调,只有大于该时间才会触发,这样把频繁触发变成了少量触发。即:用户操作很频繁,但会把频繁的操作变为少量的操作
  • 函数防抖:取消掉之前所有的触发,最后一次执行在规定时间后才会触发,即连续触发多次只会执行一次,即: 用户操作很频繁,但只执行一次
  • 函数防抖的应用场景:最常见的就是用户注册时候的手机号码验证和邮箱验证了。只有等用户输入完毕后,前端才需要检查格式是否正确,如果不正确,再弹出提示语。
  • 函数防抖在搜索表单也常见,在用户输入完毕后才发送请求
  • 函数节流应用场景,多数在监听页面元素滚动事件的时候会用到。因为滚动事件,是一个高频触发的事件
7.3.1 lodash 插件
  • lodash 插件内部已经封装好了函数节流与防抖的业务
  1. 安装
    :先搜索项目node_modules中是否含lodash,如果已经自带则不需要安装

cnpm i --save lodash

  1. 使用
    三级联动在频繁切换目录的时候会用到节流,将频繁的操作变为少量的操作
// 按需引入节流
import throttle from 'lodash/throttle'
// 函数节流
const changeIndex = throttle((index) => {
  currentIndex.value = index
}, 300)

7.4 三级联动组件的路由跳转和传参

  • 通过编程式导航实现,避免声明式导航出现卡顿的现象;
  • 利用事件委托,避免多个HTML同时使用同一函数时简化操作。
 <!-- 事件委托 -->
<div class="all-sort-list2" @click="goSearch">
import { useRouter } from 'vue-router'
// 引入 router
const router = useRouter()
// 跳转到搜索页
const toSearch = () => {
  router.push('/search')
}
  • 此时存在的问题:1. 点击父元素div内的任意元素都可以跳转,但实际效果是想要在点击a标签才跳转;2. 以及点击的是一级、二级、还是三级下的a标签;3. 参数的传递问题
  1. 确定a标签:给各级目录下的 a标签 添加自定义属性 data-categoryName,然后通过事件对象 e 的 dataset 属性来判断是否有自定义属性
<a :data-categoryName="categoryItem.categoryName">{{
   categoryItem.categoryName
}}</a>
<a :data-categoryName="subItem.categoryName">{{
   subItem.categoryName
}}</a>
<a :data-categoryName="childItem.categoryName">{{
    childItem.categoryName
}}</a>
  1. 判断是哪一级的 a标签,给 a标签 添加自定义属性 data-categoryId1
:data-categoryId1="categoryItem.categoryId"

:data-categoryId2="subItem.categoryId"

:data-categoryId3="childItem.categoryId"
  1. 修改后的跳转事件

// 跳转到搜索页
const toSearch = (e) => {
  // 获取到所以子节点
  const element = e.target
  // 通过dataset属性判断节点是否带有自定义属性,即a标签
  const { categoryname, categoryid1, categoryid2, categoryid3 } =
    element.dataset
  if (categoryname) {
    // 跳转时传的参数
    const location = {
      name: 'search'
    }
    const query = {
      categoryname
    }
    // 判断是1级、2级、还是3级下的a标签
    if (categoryid1) {
      // 添加参数ID
      query.categoryid1 = categoryid1
    } else if (categoryid2) {
      query.categoryid2 = categoryid2
    } else {
      query.categoryid3 = categoryid3
    }
    // 合并query参数
    location.query = query
    // 路由跳转
    router.push(location)
  }
}

8. ListContainer 组件与 Floor 组件

8.1 mockjs 模拟数据

服务器中没有这两个组件的数据,所以通过数据模拟

  1. 安装mockjs
 cnpm install --save mockjs
  1. 使用步骤
  • 新建 src / mock
  • 准备需要的JSON数据
  • 新建 mock / mockSever.js 文件,通过mockjs实现模拟数据
// 引入mockjs (Mock首字母必须大写)
import Mock from 'mockjs'
// 引入 json 数据
import banner from './banner.json'
import floor from './floors.json'

// mock数据:第一个参数为请求的地址;第二个参数为请求数据
// 首页轮播图
Mock.mock('/mock/banner', {
  code: 200,
  data: banner
})
// 首页楼层
Mock.mock('/mock/floor', {
  code: 200,
  data: floor
})

  • 在入口文件(main.js)中引入 mockSever.js
// 引入mockSever.js
import '@/mock/mockSever'

webpack中默认暴露的:图片、json 数据

8.2 获取轮播图数据

  1. 新建 src / api / mockAjax.js ,用于存放mock的ajax请求:要修改baseURL
/** *****************axios的二次封装,主要是封装请求拦截器和响应拦截器 ************/
/** 实现步骤
 * 1. 创建一个新的axios实例
 * 2. 请求拦截器,如果有token进行头部携带
 * 3. 响应拦截器
 **/

// 导入axios
import axios from 'axios'
// 引入进度条
import nprogress from 'nprogress'
// 引入进度条样式
import 'nprogress/nprogress.css'

/**
 * TODO-1: 利用axios的create方法,创建一个新的axios实例
 */
const instance = axios.create({
  // axios的配置
  // baseUrl:基础路径,基于哪个路径
  //  如本项目中接口前缀为 /api,设置baseURL的作用就是后续见到访问地址时,会自动加上 /api ,避免每次都去书写
  baseURL: '/mock',
  // 请求超时的时间,在5秒内无响应则请求失效
  timeout: 5000
})

/**
 * TODO-2:请求拦截器
 * 在发请求之前,请求拦截器可以进行监测,以便在请求发出前进行一些处理
 */
instance.interceptors.request.use(config => {
  // config:配置对象,其中含请求头header属性
  // 进度条开始
  nprogress.start()
  return config
})

/**
 * TODO-3:响应拦截器
 */
instance.interceptors.response.use(response => {
  // 响应成功的回调
  // 进度条结束
  nprogress.done()
  // 请求成功返回data,后续可直接使用data
  return response.data
}, err => {
  // 响应失败的回调
  console.log(err)
  // 终止Promise
  return Promise.reject(new Error('请求超时!'))
})

// 对外暴露,外部才可以使用
export default instance
  1. 配置 src / api / index.js
/** mock数据的接口 */
import mockRequest from './mockAjax'
/**
 * 首页轮播图的mock接口
 */
export const reqGetBannerList = () => mockRequest.get('/banner')
  1. views / home / ListContainer / index.vue 发送请求获取数据,并存储到仓库中
import { useStore } from 'vuex'
import { onMounted } from 'vue'
const store = useStore()
/** 获取轮播图数据 */
onMounted(() => {
  // 派发action:通过vuex发起ajax请求,将数据存在仓库中
  store.dispatch('getBannerList')
})
  1. 配置 store / home / index.js
import { reqGetBannerList } from '@/api'
const state = {
  bannerList: []
}
const mutations = {
  GETBANNERLIST(state, bannerList) {
    state.bannerList = bannerList
  }
}
const getters = {
}
const actions = {
  async getBannerList() {
    const result = await reqGetBannerList()
    console.log(result)
    if (result.code === 200) {
      this.commit('GETBANNERLIST', result.data)
    }
  }
}

export default {
  state,
  mutations,
  getters,
  actions
}
  1. 获取存储在仓库中的 轮播图列表
// 获取仓库中的轮播图列表
const bannerList = computed(() => store.state.home.bannerList)

8.3 封装公用 swiper 组件

swiper 是一个轮播图插件

  1. 安装swiper
cnpm install swiper --save
  1. main.js 中引入swiper样式
// 引入swiper样式
// 引入swiper样式
import 'swiper/scss'
import 'swiper/scss/navigation'
import 'swiper/scss/pagination'
  1. 新建 components / Carousel / index.vue
<template>
  <!-- swiper组件 -->
  <swiper navigation :pagination="{ clickable: true }" loop>
    <swiper-slide v-for="carousel of list" :key="carousel.id">
      <img :src="carousel.imgUrl" />
    </swiper-slide>
  </swiper>
</template>

<script setup>
import { defineProps } from 'vue'
// 引入swiper 组件
import { Swiper, SwiperSlide } from 'swiper/vue'
/**
 * 引入swiper模块
 * Navigation- 导航模块
 * Pagination- 分页模块
 */
import SwiperCore, { Navigation, Pagination } from 'swiper'
SwiperCore.use([Navigation, Pagination])

// 接收传递过来的数据
const props = defineProps({
  list: {
    type: Array,
    required: true
  }
})
</script>
<style lang="scss" scoped></style>

  1. 在组件中按需使用
// 引入swiper轮播图组件
import Swiper from '@/components/Carousel'
 <swiper :list="bannerList"></swiper>
  1. 或者直接在 main.js 注册为全局组件使用
// swiper组件
import Swiper from '@/components/Carousel'
createApp(App)
  .component('swiper', Swiper) // 全局注册swiper组件
  .mount('#app')

二、 Search 组件

1. search中三级联动列表

1.1商品分类与过渡动画

1.components / TypeNav / index.vue 中的配置

// 引入route
const route = useRoute()
/** 三级联动的显示与隐藏的控制 */
const showNav = ref(true)
onMounted(() => {
  // 组件挂载完毕后,向服务发请求获取三级联动列表
  store.dispatch('getCategoryList')
  // 判断是否在 home 页面,不在home页面就将 三级联动菜单隐藏
  if (route.path !== '/home') {
    console.log(route)
    showNav.value = false
  }
})

// 事件委托:鼠标移入的处理
const enterShow = () => {
  if (route.path !== '/home') {
    // 鼠标移入展示商品列表(三级联动)
    showNav.value = true
  }
}

// 事件委托:鼠标移除的处理
const leaveIndex = () => {
  // 鼠标移除,重置currentIndex
  currentIndex.value = -1
  if (route.path !== '/home') {
    // 鼠标移除,隐藏三级联动列表
    showNav.value = false
  }
}
  1. 过渡动画
  • 使用过渡动画的前提:组件 | 元素必须要有v-if 或 v-show 指令
  • v-show | v-if指令所在的元素 | 组件外层要包裹 transition 标签
  • 如果 transition 标签设置了 name 属性,在写动画效果时的类名要以 name值- 开头
<transition name="sort">
    <div class="sort" v-show="showNav">
    </div>
</transition>
 // 过渡动画样式
    // 过渡动画开始(进入)样式
    .sort-enter-from {
      height: 0px;
    }
    // 过渡动画结束样式
    .sort-enter-to {
      height: 461px;
    }
    // 定义动画时间、速率
    .sort-enter-active {
      transition: all 0.5s linear;
    }

1.2 性能优化:三级联动请求的优化

  • 目前系统的效果:在home 和 search 页面相互切换时,都会去发送请求获取三级联动列表,如果不停切换,则会不停请求。
  • 可以将获取三级组件的请求放到根组件APP.vue中,这样就只需要获取一次
    APP.vue
<script setup>
import { onMounted } from 'vue'
import { useStore } from 'vuex'
// 引入vuex的实例store
const store = useStore()

onMounted(() => {
/** 派发action,获取三级联动组件的列表 */
  store.dispatch('getCategoryList')
})
</script>

1.3 合并参数

跳转到search页面时,合并params和query参数

  1. 修改 components / TypeNav / index.vue 中的 goSearch 方法:在进行跳转前判断是否有params参数,有就一起传递过去
const toSearch = (e) => {
  // 获取到所以子节点
  const element = e.target
  // 通过dataset属性判断节点是否带有自定义属性,即a标签
  const { categoryname, categroy1id, categroy2id, categroy3id } =
    element.dataset
  if (categoryname) {
    // 跳转时传的参数
    const location = {
      name: 'search'
    }
    const query = {
      categoryname
    }
    // 判断是1级、2级、还是3级下的a标签
    if (categroy1id) {
      // 添加参数ID
      query.categroy1id = categroy1id
    } else if (categroy2id) {
      query.categroy2id = categroy2id
    } else {
      query.categroy3id = categroy3id
    }
    // 合并query参数
    location.query = query
    // 判断是否有params参数
    if (route.params) {
      location.params = route.params
    }
    // 路由跳转
    router.push(location)
  }
}
  1. 修改 components / Header / index.vue 的 handleSearch 方法:路由跳转前判断是否有query参数,有就一起传递
// 搜索按钮的回调函数
const handleSearch = () => {
  const location = {
    name: 'search',
    params: {
      keyword: keyword.value || undefined
    }
  }
  if (route.query) {
    location.query = route.query
  }
  router.push(location)
}

2. 开发 Search 模块中的其他结构

  1. src / api / index.js 新增配置
/**
 * 搜索页面接口
 * 使用时如果不传参,也需要写一个空对象,否则会报错
 */
export const reqGetSearchData = (parmas) => requests.post('/list')export const reqGetSearchData = (parmas) => requests({
  url: '/list',
  method: 'post',
  data: parmas
})
  1. src / store / search / index.js 中vuex 配置
  • onMounted只会执行一次,但是search请求需要根据不同参数发送不同的请求,所以不能写在onMounted中请求
  • 可以监听地址栏变化来发不同的请求
  • Object.assign() :合并对象,第一个参数为要返回的对象,是浅拷贝
import { reqGetSearchData } from '@/api/index'
const state = {
  searchData: {}
}

const mutations = {
  GETSEARCHDATA(state, searchData) {
    state.searchData = searchData
  }
}
// 简化仓库中的数据
const getters = {
  // 参数state:为当前仓库的 state
  goodsList(state) {
    return state.searchData.goodsList || []
  },
  trademarkList(state) {
    return state.searchData.trademarkList || []
  }
}
const actions = {
  // 获取search模块数据
  // 第二个参数为传递的数据,如果没传默认值为空对象
  async getSearchData ({ commit }, params = {}) {
    console.log(params)
    // 等待接口请求成功
    const result = await reqGetSearchData(params)
    if (result.code === 200) {
      commit('GETSEARCHDATA', result.data)
    }
  }
}

export default {
  state,
  mutations,
  getters,
  actions
}
  1. src / views / Search / index.vue 动态展示数据
  • 面包屑导航
  • 全局事件总线 实现header和search(兄弟组件)通信
  • 子组件向父组件传值
  • 数组去重问题
<script setup>
import SearchSelector from './SearchSelector/SearchSelector'
import { computed, onMounted, reactive, watch } from 'vue'
import { useStore } from 'vuex'
import { useRoute, useRouter } from 'vue-router'
import bus from '@/utils/eventBus'
const store = useStore()
const route = useRoute()
const router = useRouter()
onMounted(() => {
  // 获取路由参数
  const params = route.params
  const query = route.query

  // 合并参数对象
  Object.assign(reqParams, params, query)
  // 组件挂载完毕发送一次请求
  getSearchData()
})
/** search请求 */
// 要传递过去的参数
const reqParams = reactive({
  category1Id: '', // 一级分类ID
  category2Id: '', // 二级分类ID
  category3Id: '', // 三级分类ID
  categoryName: '', // 分类名字
  keyword: '', // 搜索关键字
  order: '1:desc', // 排序
  pageNo: 1, // 当前页数
  pageSize: 10, // 每页数据条数
  props: [], // 平台售卖属性的参数
  trademark: '' // 品牌
})
const getSearchData = () => {
  console.log(reqParams)
  // 派发action,获取搜索数据
  store.dispatch('getSearchData', reqParams)
}

// 获取仓库中的搜索数据
const goodsList = computed(() => store.getters.goodsList)

// 监听地址栏变化
watch(route, (newval, oldval) => {
  // 获取路由参数
  const params = route.params
  const query = route.query

  // 合并参数对象
  Object.assign(reqParams, params, query)
  // 地址栏变化就发送请求
  getSearchData()
  // 清空传递的ID
  reqParams.category1Id = undefined
  reqParams.category2Id = undefined
  reqParams.category3Id = undefined
})

/**
 * 面包屑
 */
const removeCategoryName = () => {
  // 删除面包屑标签
  reqParams.categoryName = undefined
  // 清空传递的ID
  reqParams.category1Id = undefined
  reqParams.category2Id = undefined
  reqParams.category3Id = undefined
  // 发送请求
  getSearchData()
  // 地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)
  // 严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
  if (route.params) {
    router.push({ name: 'search', params: route.params })
  }
}
/** 搜索关键字 */
const removeKeyword = () => {
  // 清空keyword
  reqParams.keyword = undefined
  getSearchData()
  // header组件中的搜索框也置空
  bus.emit('clearKeyword')
  // 地址栏也要清除keyword
  if (route.query) {
    router.push({
      name: 'search',
      query: route.query
    })
  } else {
    router.push({ name: 'search' })
  }
}
/** 接收子组件传递品牌数据 */
const trademarkInfoHandle = (trademark) => {
  // 整理trademark 参数
  reqParams.trademark = `${trademark.tmId}:${trademark.tmName}`
  // 发送请求
  getSearchData()
}
// 删除品牌信息
const removeTrademakr = () => {
  reqParams.trademark = undefined
  getSearchData()
}

/** 售卖属性数据 */
const attrsInfoHandle = (attr, attrValue) => {
  // 处理参数
  const props = `${attr.attrId}:${attrValue}:${attr.attrName}`
  // 数组去重
  if (reqParams.props.indexOf(props) === -1) {
    reqParams.props.push(props)
  }
  // 发送请求
  getSearchData()
}
// 删除属性
const removeAttr = (index) => {
  // 移除数组中的对应元素
  reqParams.props.splice(index, 1)
  getSearchData()
}

</script>

3. 排序问题

1:综合
2:价格
asc:升序
desc:降序
初始状态: 1:desc 综合降序

  • active类名的判断
  • 箭头的判断:和active类名一致
  • 图标:阿里巴巴矢量图标库 找到需要的图标 ==》在线链接 ==》 font-class 复制链接 ==》 在项目public / index.html 中引入
<!-- 引入阿里图标 -->
    <link rel="stylesheet" href="https//at.alicdn.com/t/font_3243888_dr0ck7v31mg.css">
<ul class="sui-nav">
              <li :class="{ active: isOne }" @click="changeOrder('1')">
                <a>
                  综合
                  <span
                    v-show="isOne"
                    class="iconfont"
                    :class="{
                      'icon-arrow-up': isAsc,
                      'icon-arrow-down': isDesc
                    }"
                  ></span>
                </a>
              </li>
              <li :class="{ active: isTwo }" @click="changeOrder('2')">
                <a>
                  价格
                  <span
                    v-show="isTwo"
                    class="iconfont"
                    :class="{
                      'icon-arrow-up': isAsc,
                      'icon-arrow-down': isDesc
                    }"
                  ></span>
                </a>
              </li>
            </ul>
/**
 * 排序处理
 */
// 判断当前是综合1还是价格2
const isOne = computed(() => reqParams.order.indexOf('1') !== -1)
const isTwo = computed(() => reqParams.order.indexOf('2') !== -1)
// 判断升序、降序
const isAsc = computed(() => reqParams.order.indexOf('asc') !== -1)
const isDesc = computed(() => reqParams.order.indexOf('desc') !== -1)
// 点击事件
const changeOrder = (flag) => {
  // 初始状态
  const originOrder = reqParams.order
  const originFlag = originOrder.split(':')[0]
  const originSort = originOrder.split(':')[1]
  // 点击后的order
  let newOrder = ''
  // 点击时判断点击的是综合还是价格
  if (originFlag === flag) {
    // 点击综合
    newOrder = `${originFlag}:${originSort === 'desc' ? 'asc' : 'desc'}`
  } else {
    // 点击价格:默认降序
    newOrder = `${flag}:${'desc'}`
  }
  // 修改后的参数
  reqParams.order = newOrder
  // 重新请求
  getSearchData()
}

4. 全局组件:分页组件

  • 分页器需要的数据
  • 当前页数:pageNo
  • 每页展示的数据:pageSize
  • 数据总数:total
  • 连续的页码数:continues,一般为5或7(奇数,对称),重点是要算出连续页码的起始和结束位置

一般先用假数据进行功能调试,调试成功再调用后台接口测试

父组件

<!-- 分页器 -->
        <pagination
          :pageNo="reqParams.pageNo"
          :pageSize="reqParams.pageSize"
          :total="totalList"
          :continues="5"
          @getPageNo="getPageNoHandle"
        >
        </pagination>
/**
 * 分页器
 */
// 获取子组件传递的数据
const getPageNoHandle = (num) => {
  reqParams.pageNo = num
  getSearchData()
}

子组件 components / Pagination / index.vue

<template>
  <div class="pagination">
    <button :disabled="pageNo === 1" @click="prevClickHandle(pageNo - 1)">
      上一页
    </button>
    <button
      v-if="startNumAndEndNum().start > 1"
      @click="prevClickHandle(1)"
      :class="{ active: pageNo === 1 }"
    >
      1
    </button>

    <button v-if="startNumAndEndNum().start > 2">···</button>

    <!-- 连续页码 -->
    <template v-for="(page, index) in startNumAndEndNum().end" :key="index">
      <button
        v-if="page >= startNumAndEndNum().start"
        @click="prevClickHandle(page)"
        :class="{ active: pageNo === page }"
      >
        {{ page }}
      </button>
    </template>

    <button v-if="startNumAndEndNum().end < totalPage - 1">···</button>
    <button
      v-if="startNumAndEndNum().end < totalPage"
      @click="prevClickHandle(totalPage)"
      :class="{ active: pageNo === totalPage }"
    >
      {{ totalPage }}
    </button>
    <button
      :disabled="totalPage === pageNo"
      @click="prevClickHandle(pageNo + 1)"
    >
      下一页
    </button>

    <!-- <button style="margin-left: 30px">共 {{ total }} 条</button> -->
  </div>
</template>

<script setup>
import { computed, defineProps, defineEmits } from 'vue'
const props = defineProps({
  pageNo: {
    type: Number
  },
  pageSize: {
    type: Number
  },
  total: {
    type: Number
  },
  continues: {
    type: Number
  }
})
console.log(props)

/**
 * 总页数:向上取整
 */
const totalPage = computed(() => Math.ceil(props.total / props.pageSize))

/**
 * 连续页码
 * 当前页在中间
 * 假设连续页码数为5:取当前页的前后两个数字
 */
const startNumAndEndNum = () => {
  // 解构,避免每次都去写props.xxx
  const { pageNo, continues } = props
  // 初始化起止页码
  let start = 0
  let end = 0
  // 连续页码数大于总页码
  if (continues > totalPage.value) {
    start = 1
    end = totalPage.value
  } else {
    // 起始位置
    start = pageNo - parseInt(continues / 2)
    // 结束位置
    end = pageNo + parseInt(continues / 2)
    // 起始位置出现负数或0
    if (start < 1) {
      start = 1
      end = continues
    }
    // 结束位置超过总页码时
    if (end > totalPage.value) {
      end = totalPage.value
      start = totalPage.value - continues + 1
    }
  }
  return { start, end }
}
const { start, end } = startNumAndEndNum()
console.log(start, end)
/**
 * 向父组件传递所点击的内容
 */
const emit = defineEmits(['getPageNo'])
// 点击上一页
const prevClickHandle = (num) => {
  emit('getPageNo', num)
}
</script>
<style lang="scss" scoped>
.pagination {
  text-align: center;
  button {
    margin: 0 5px;
    background-color: #f4f4f5;
    color: #606266;
    outline: none;
    border-radius: 2px;
    padding: 0 4px;
    vertical-align: top;
    display: inline-block;
    font-size: 13px;
    min-width: 35.5px;
    height: 28px;
    line-height: 28px;
    cursor: pointer;
    box-sizing: border-box;
    text-align: center;
    border: 0;

    &[disabled] {
      color: #c0c4cc;
      cursor: not-allowed;
    }

    &.active {
      cursor: not-allowed;
      background-color: #409eff;
      color: #fff;
    }
  }
}
</style>

  • 全局注册 main.js
// 分页组件
import Pagiination from '@/components/Pagination'
createApp(App)
  .use(store)
  .use(router)
  .component('pagination', Pagiination) // 全局注册分页组件
  .mount('#app')

三、详情组件

1. 滚动行为

  • 当前在页面进行跳转的时候,如果滚动条滚动了,跳转后的滚动条不会置顶而是在跳转前的位置
  • 官方文档说明
const router = createRouter({
  history: createWebHashHistory(),
  routes,
  // 滚动行为
  scrollBehavior(to, from, savedPosition) {
    // 始终滚动到顶部
    return { top: 0 }
  }
})

2. 获取数据

  1. src / api / index.js 配置接口
/***
 * 详情页接口
 */
export const reqGoodsDetail = (skuId) => requests({
  url: `/item/${skuId}`,
  method: 'get'
})
  1. 配置相关vuex
  • 新建 src / store / detail / index.js
import { reqGoodsDetail } from '@/api'
const state = {
  goodInfo: {}
}
const mutations = {
  GETGOODSDETAIL(state, goodInfo) {
    state.goodInfo = goodInfo
  }
}
const actions = {
  // 获取详情的action
  async getGoodsDetail({ commit }, skuId) {
    // 等待请求完成
    const result = await reqGoodsDetail(skuId)
    if (result.code === 200) {
      // 提交mutation
      commit('GETGOODSDETAIL', result.data)
    }
  }
}
export default {
  state,
  mutations,
  actions
}

  • src / store / index.js 中引入对应模块
import { createStore } from 'vuex'
// 导入模块
import detail from './detail'
export default createStore({
// 实现vuex仓库模块式开发存储数据
  modules: {
    detail
  }
})

3. swiper缩略图

  • 效果图
    在这里插入图片描述
  • swiper子组件
<template>
  <div class="thumb-example">
    <swiper :modules="[Thumbs]" :thumbs="{ swiper: thumbsSwiper }">
      <swiper-slide
        v-for="(imgItem, index) in imgList"
        :key="imgItem.id"
        :class="`img-item${index}`"
      >
        <img :src="imgItem.imgUrl" />
      </swiper-slide>
    </swiper>
    <swiper
      :modules="[Thumbs]"
      watch-slides-progress
      @swiper="setThumbsSwiper"
      :slides-per-view="5"
      :space-between="10"
      class="small-img-list"
    >
      <swiper-slide
        v-for="imgItem in imgList"
        :key="imgItem.id"
        :class="`img-item`"
      >
        <img :src="imgItem.imgUrl" />
      </swiper-slide>
    </swiper>
  </div>
</template>

<script setup>
import { ref, defineProps } from 'vue'
import { Thumbs } from 'swiper'
import { Swiper, SwiperSlide } from 'swiper/vue'
const thumbsSwiper = ref(null)
const setThumbsSwiper = (swiper) => {
  thumbsSwiper.value = swiper
}

// 接收父组件数据
defineProps({
  imgList: {
    type: Array
  }
})
</script>
<style lang="scss" scoped>
.small-img-list {
  margin-top: 10px;
}
</style>

  • 父组件
 <!-- swiper 缩略图 -->
<swiper-thumbs :imgList="skuImageList"></swiper-thumbs>
import swiperThumbs from './thumbs'
// 传递给子组件的数据
const skuImageList = computed(() => {
  return store.getters.skuInfo.skuImageList || []
})

4. 购物车的关键代码

  • api接口
/**
 * 添加购物车接口
 */
export const reqAddOrUpdateShopCart = (skuId, skuNum) => requests({
  url: `/cart/addToCart/${skuId}/${skuNum}`,
  method: 'post'
})
  • vuex 配置
 // 添加购物车
  async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
    // 此时请求成功也不会返回数据,返回200表示添加成功
    const result = await reqAddOrUpdateShopCart(skuId, skuNum)
    // 请求成功
    if (result.code === 200) {
      return 'OK'
    } else {
      return Promise.reject(new Error('请求出错!!!'))
    }
  }
  • 添加购物车功能(结合本地存储传参)
/**
 * 购物车
 */
// 购物车数量
const skuNum = ref(1)
// 数量框的输入处理
const changeSkuNum = (e) => {
  const value = e.target.value * 1
  // 如果输入的是非法字符
  if (isNaN(value) || value < 1) {
    skuNum.value = 1
  } else {
    skuNum.value = parseInt(value)
  }
}
// 添加购物车
const addShopCar = async () => {
  // 发送请求
  try {
    await store.dispatch('addOrUpdateShopCart', {
      skuId: route.params.skuId,
      skuNum: skuNum.value
    })
    /**
     *请求成功,进行路由跳转,同时传递产品信息
     简单的参数,通过query传递
     复杂的,如产品信息,采用本地存储
     **/
    sessionStorage.setItem('SKUINFO', JSON.stringify(skuInfo.value))
    router.push({
      name: 'addcartsucess',
      query: { skuNum: skuNum.value }
    })
  } catch (error) {
    console.log(error.message)
  }
}

5. uuid 临时游客身份

  • 在加入购物车的时候,在请求头传一个uuid
  • 新建 src / utils / uuid_token.js
import { v4 as uuidv4 } from 'uuid'
// 生成一个随机字符串,且不用每次刷新,而是持久存储
export const getUUID = () => {
// 先从本地存储获取,看是否有uuid
  let uuidToken = localStorage.getItem('UUIDTOKEN')
  // 如果没有,则生成游客临时身份
  if (!uuidToken) {
    uuidToken = uuidv4()
    localStorage.setItem('UUIDTOKEN', uuidToken)
  }
  return uuidToken
}

  • 请求拦截器中
instance.interceptors.request.use(config => {
  // config:配置对象,其中含请求头header属性
  // 游客身份处理
  if (store.state.detail.uuid_token) {
    // 配置请求头(需要和后端确认需要的字段)
    config.headers.userTempId = store.state.detail.uuid_token
  }
  return config
})

6. 购物车

  • shopCart / index.vue
<script setup>
// 按需引入节流
import throttle from 'lodash/throttle'
// 导入toRaw函数
// import { toRaw } from '@vue/reactivity'
import { useStore } from 'vuex'
import { computed, onMounted } from 'vue'
const store = useStore()
/**
 * 获取个人购物车数据
 */
const getCarList = () => {
  store.dispatch('getShopCarList')
}
onMounted(() => {
  getCarList()
})
// 获取仓库数据(考虑读写的情况)
/* const cartInfoList = computed(() => {
  return store.getters.cartList.cartInfoList
}) */
const cartInfoList = computed({
  // 读取
  get() {
    return store.getters.cartList.cartInfoList
  },
  // 修改
  set(value) {
    console.log(value)
  }
})
// 所选商品的总价
const totalPrice = computed(() => {
  let sum = 0
  for (const i in cartInfoList.value) {
    const item = cartInfoList.value[i]
    if (item.isChecked) {
      sum += item.skuNum * item.skuPrice
    }
  }
  return sum
})

// 全选状态
const isAllChecked = computed(() => {
  const cartList = store.state.shopCart.cartList
  let flag
  cartList.forEach((item) => {
    if (item.cartInfoList.every((info) => info.isChecked === 1)) {
      flag = true
    } else {
      flag = false
    }
  })
  return flag
})
// 全选框点击操作
const updateAllChecked = async (e) => {
  try {
    const isChecked = e.target.checked ? '1' : '0'
    // 派发action
    await store.dispatch('updateAllCheckd', isChecked)
    // 获取最新数据
    getCarList()
  } catch (error) {
    alert(error.message)
  }
}

// 修改数量
const changeNum = throttle(async (type, disNum, item) => {
  /**
   * type:元素类型,加减还是输入
   * disNum:加 =》变化量1 ,减 =》 变化量-1 , 输入 =》 最终数量
   * item:修改的对应商品
   */
  switch (type) {
    case 'add':
      disNum = 1
      break

    case 'mins':
      disNum = item.skuNum > 1 ? -1 : 0
      break
    case 'change':
      // 输入的非数字或负数
      if (isNaN(disNum) || disNum < 1) {
        disNum = 0
      } else {
        disNum = parseInt(disNum) - item.skuNum
      }
      break
  }

  // 派发action
  try {
    // 等待请求成功
    await store.dispatch('addOrUpdateShopCart', {
      skuId: item.skuId,
      skuNum: disNum
    })
    // 获取最新数据
    getCarList()
  } catch (error) {
    console.log(new Error('添加失败!'))
  }
}, 1000)

// 删除操作(删除单个)
const delCartById = async (item) => {
  try {
    await store.dispatch('deleteCartBySkuId', item.skuId)
    // 获取最新数据
    getCarList()
  } catch (error) {
    // 删除失败
    alert(error.message)
  }
}

// 切换选中状态
const updateChane = async (item, e) => {
  try {
    // 成功修改
    const checked = e.target.checked ? '1' : '0'
    await store.dispatch('changeCartChecked', {
      skuId: item.skuId,
      isChecked: checked
    })
    // 获取最新数据
    getCarList()
  } catch (error) {
    // 修改失败
    alert(error.message)
  }
}

// 删除选中的商品
const deleteAllCheckedGoods = async () => {
  try {
    // 派发action
    await store.dispatch('deleteAllCheckedCart')
    // 获取最新数据
    getCarList()
  } catch (error) {
    alert(error.message)
  }
}
</script>
  • store / shopcart / index.js
import { reqGetShopCarList, reqDeleteCartBySkuId, reqChangeCartChecked } from '@/api/index.js'
const state = {
  cartList: []
}
const mutations = {
  GETSHOPCARLIST(state, cartList) {
    state.cartList = cartList
  }
}
const actions = {
  // 获取购物车列表
  async getShopCarList() {
    const result = await reqGetShopCarList()
    if (result.code === 200) {
      this.commit('GETSHOPCARLIST', result.data)
    }
  },
  // 删除购物车商品(单个)
  async deleteCartBySkuId({ commit }, skuId) {
    const result = await reqDeleteCartBySkuId(skuId)
    if (result.code === 200) {
      return '已成功删除~'
    } else {
      return Promise.reject(new Error('删除失败!'))
    }
  },

  // 切换商品选中状态
  async changeCartChecked({ commit }, { skuId, isChecked }) {
    const result = await reqChangeCartChecked(skuId, isChecked)
    if (result.code === 200) {
      return '已成功修改状态'
    } else {
      return Promise.reject(new Error('修改出错啦~'))
    }
  },

  // 删除所选商品
  deleteAllCheckedCart({ getters, dispatch }) {
    const PromiseAll = []
    getters.cartList.cartInfoList.forEach(item => {
      const promise = item.isChecked === 1 ? dispatch('deleteCartBySkuId', item.skuId) : ''
      PromiseAll.push(promise)
    })

    return Promise.all(PromiseAll)
  },

  // 全选状态的切换
  updateAllCheckd({ state, dispatch }, isChecked) {
    const PromiseAll = []
    state.cartList[0].cartInfoList.forEach(item => {
      const promise = dispatch('changeCartChecked', { skuId: item.skuId, isChecked })
      PromiseAll.push(promise)
    })
    return Promise.all(PromiseAll)
  }
}
const getters = {
  cartList(state) {
    return state.cartList[0] || {}
  }
}
export default {
  state,
  mutations,
  actions,
  getters
}

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值