码云仓库地址
开发流程
- 拆分静态组件 | 页面
- 配置 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>
postman下载链接
目的:基于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>
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-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
}