赞
踩
- 拆分静态组件 | 页面
- 配置 api 请求
- vuex 配置
- 组件获取数据,动态展示数据
全局组件
(只需要注册一次,就可以在任意需要的地方使用)src / components / TypeNav / index.vue
main.js
中进行全局注册全局组件的名字
,后续使用用到的就是这个名字;第二个参数为要注册的组价
// main.js
// 导入三级联动组件
import TypeNav from '@/components/TypeNav'
createApp(App)
.component('type-nav', TypeNav) // 全局注册三级联动组件
.mount('#app')
src / view / Home / index.vue
<!-- 三级联动组件 -->
<type-nav></type-nav>
<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>
目的:基于axios封装一个请求工具,调用接口时使用。封装请求拦截器、响应拦截器,进行一些业务处理
在后续需要调用接口时,要引入该工具
cnpm install --save axios
src / api / request.js
/** *****************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
src / api / index.js
用于接口的统一管理/** *************** 接口的统一管理 *******************/
// 导入封装好的axios
import requests from './request'
/**
* 三级联动接口
*/
export const reqCategoryList = () => requests({
url: '/product/getBaseCategoryList',
method: 'get'
})
module.exports = {
// 代理跨域
devServer: {
proxy: {
'/api': {
target: 'http://39.98.123.211' // 服务器地址
// pathRewrite: { '^/api': '' },
}
}
}
}
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 })
const state = { ... } const mutations = { ... } // getters:计算属性,项目中主要用于简化仓库中的数据 const getters = { ... } const actions = { ... } export default { state, mutations, getters, actions }
import { createStore } from 'vuex'
import home from './home'
import search from './search'
export default createStore({
// 实现vuex仓库模块式开发存储数据
modules: {
home,
search
}
})
Home / index.vue
中,组件挂载完毕就向服务发请求获取三级联动列表import { onMounted } from 'vue'
import { useStore } from 'vuex'
// 引入vuex的实例store
const store = useStore()
onMounted(() => {
// 组件挂载完毕后,向服务发请求获取三级联动列表
store.dispatch('getCategoryList')
})
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 }
Home / index.vue
中使用// 获取请求到的三级联动列表
const categoryList = store.state.home.categoryList
&:hover {
background-color: skyblue;
}
<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 @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>
函数节流
:在规定事件间隔范围内不会触发触发回调,只有大于该时间才会触发,这样把频繁触发变成了少量触发。即:用户操作很频繁,但会把频繁的操作变为少量的操作函数防抖
:取消掉之前所有的触发,最后一次执行在规定时间后才会触发,即连续触发多次只会执行一次,即: 用户操作很频繁,但只执行一次函数防抖的应用场景
:最常见的就是用户注册时候的手机号码验证和邮箱验证了。只有等用户输入完毕后,前端才需要检查格式是否正确,如果不正确,再弹出提示语。函数节流应用场景
,多数在监听页面元素滚动事件的时候会用到。因为滚动事件,是一个高频触发的事件cnpm i --save lodash
// 按需引入节流
import throttle from 'lodash/throttle'
// 函数节流
const changeIndex = throttle((index) => {
currentIndex.value = index
}, 300)
<!-- 事件委托 -->
<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. 参数的传递问题
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>
:data-categoryId1="categoryItem.categoryId"
:data-categoryId2="subItem.categoryId"
:data-categoryId3="childItem.categoryId"
// 跳转到搜索页 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) } }
服务器中没有这两个组件的数据,所以通过数据模拟
cnpm install --save mockjs
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 })
// 引入mockSever.js
import '@/mock/mockSever'
webpack中默认暴露的:图片、json 数据
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
src / api / index.js
/** mock数据的接口 */
import mockRequest from './mockAjax'
/**
* 首页轮播图的mock接口
*/
export const reqGetBannerList = () => mockRequest.get('/banner')
views / home / ListContainer / index.vue
发送请求获取数据,并存储到仓库中import { useStore } from 'vuex'
import { onMounted } from 'vue'
const store = useStore()
/** 获取轮播图数据 */
onMounted(() => {
// 派发action:通过vuex发起ajax请求,将数据存在仓库中
store.dispatch('getBannerList')
})
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 }
// 获取仓库中的轮播图列表
const bannerList = computed(() => store.state.home.bannerList)
swiper 是一个轮播图插件
cnpm install swiper --save
main.js
中引入swiper样式// 引入swiper样式
// 引入swiper样式
import 'swiper/scss'
import 'swiper/scss/navigation'
import 'swiper/scss/pagination'
<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>
// 引入swiper轮播图组件
import Swiper from '@/components/Carousel'
<swiper :list="bannerList"></swiper>
main.js
注册为全局组件使用// swiper组件
import Swiper from '@/components/Carousel'
createApp(App)
.component('swiper', Swiper) // 全局注册swiper组件
.mount('#app')
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 } }
- 使用过渡动画的前提:
组件 | 元素必须要有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;
}
APP.vue
<script setup>
import { onMounted } from 'vue'
import { useStore } from 'vuex'
// 引入vuex的实例store
const store = useStore()
onMounted(() => {
/** 派发action,获取三级联动组件的列表 */
store.dispatch('getCategoryList')
})
</script>
跳转到search页面时,合并params和query参数
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) } }
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)
}
src / api / index.js
新增配置/**
* 搜索页面接口
* 使用时如果不传参,也需要写一个空对象,否则会报错
*/
export const reqGetSearchData = (parmas) => requests.post('/list')
或
export const reqGetSearchData = (parmas) => requests({
url: '/list',
method: 'post',
data: parmas
})
src / store / search / index.js
中vuex 配置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 }
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>
1:综合
2:价格
asc:升序
desc:降序
初始状态: 1:desc 综合降序
<!-- 引入阿里图标 -->
<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() }
- 当前页数: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>
// 分页组件
import Pagiination from '@/components/Pagination'
createApp(App)
.use(store)
.use(router)
.component('pagination', Pagiination) // 全局注册分页组件
.mount('#app')
const router = createRouter({
history: createWebHashHistory(),
routes,
// 滚动行为
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { top: 0 }
}
})
src / api / index.js
配置接口/***
* 详情页接口
*/
export const reqGoodsDetail = (skuId) => requests({
url: `/item/${skuId}`,
method: 'get'
})
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 }
import { createStore } from 'vuex'
// 导入模块
import detail from './detail'
export default createStore({
// 实现vuex仓库模块式开发存储数据
modules: {
detail
}
})
<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 || []
})
/**
* 添加购物车接口
*/
export const reqAddOrUpdateShopCart = (skuId, skuNum) => requests({
url: `/cart/addToCart/${skuId}/${skuNum}`,
method: 'post'
})
// 添加购物车
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) } }
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
})
<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>
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 }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。