目的:打造購物車的頁面
"去結算"調整
調整src\views\shop\Cart.vue
<template>
......
<div class="check__btn">
<router-link :to="{ name: 'Home' }">
去結算
</router-link>
</div>
</div>
</div>
</template>
......
<style lang="scss" scoped>
......
.check {
......
&__btn {
......
// 去掉a標籤的下劃線
a {
color: $bg-color;
text-decoration: none; //去掉文本修飾
}
}
}
</style>
優化if語句
src\store\index.js
import { createStore } from 'vuex'
export default createStore({
state: {
cartList: {
// 第一層級:商鋪的id
// 第二層內容是商品內容以及購物數量
// shopId: {
// productID: {
// _id: '1',
// name: '番茄250g/份',
// imgUrl: '/i18n/9_16/img/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 0
// }
// }
}
},
mutations: {
/**
* 加入或減少購物車數量
* @param {*} state
* @param {String} shopId 店鋪id
* @param {String} productId 商品id
* @param {Object} productInfo 商品信息集
* @param {Number} num 加入購物車的數量
* @param {*} payload
*/
changeItemToCart(state, payload) {
const { shopId, productId, productInfo, num } = payload
// console.log(shopId, productId, productInfo)
const shopInfo = state.cartList[shopId] || {}
let product = shopInfo[productId]
if (!product) {
productInfo.count = 0
product = productInfo // 初始化
}
product.count += num
// && 短路運算符,前面的滿足纔會執行後面的邏輯,等價於if
num > 0 && (product.checked = true)
product.count <= 0 && (shopInfo[productId].count = 0)
// delete state.cartList[shopId]
shopInfo[productId] = product
// 賦值
state.cartList[shopId] = shopInfo
},
// 購物車勾選記錄
changeItemChecked(state, payload) {
const { shopId, productId } = payload
const product = state.cartList[shopId][productId]
product.checked = !product.checked
},
// 清除購物車
changeCleanCartProducts(state, payload) {
const { shopId } = payload
state.cartList[shopId] = {}
},
// 購物車全選或者取消全選
setCartItemsChecked(state, payload) {
const { shopId } = payload
const products = state.cartList[shopId]
if (products) {
for (const i in products) {
const product = products[i]
product.checked = true
}
}
}
},
actions: {},
modules: {}
})
之前存入src\store\index.js
// 第一層級:商鋪的id
// 第二層內容是商品內容以及購物數量
// shopId: {
// productID: {
// _id: '1',
// name: '番茄250g/份',
// imgUrl: '/i18n/9_16/img/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 0
// }
// }
在訂單層面,需要顯示商品名字等商鋪的信息,明顯這個結構不夠用。優化結構如下:
// shopId: {
// shopName: '沃什麼碼',
// productList: {
// productId: {
// _id: '1',
// name: '番茄250g/份',
// imgUrl: '/i18n/9_16/img/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 0
// }
// }
// }
那麼相應的更新代碼也要調整:
src\store\index.js
import { createStore } from 'vuex'
export default createStore({
state: {
cartList: {
// shopId: {
// shopName: '沃什麼碼',
// productList: {
// productId: {
// _id: '1',
// name: '番茄250g/份',
// imgUrl: '/i18n/9_16/img/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 0
// }
// }
// }
// ================== 之前版本的結構 ==================
// 第一層級:商鋪的id
// 第二層內容是商品內容以及購物數量
// shopId: {
// productID: {
// _id: '1',
// name: '番茄250g/份',
// imgUrl: '/i18n/9_16/img/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 0
// }
// }
}
},
mutations: {
/**
* 加入或減少購物車數量
* @param {*} state
* @param {String} shopId 店鋪id
* @param {String} productId 商品id
* @param {Object} productInfo 商品信息集
* @param {Number} num 加入購物車的數量
* @param {*} payload
*/
changeItemToCart(state, payload) {
const { shopId, productId, productInfo, num } = payload
// console.log(shopId, productId, productInfo)
const shopInfo = state.cartList[shopId] || {
shopName: '',
productList: {}
}
let product = shopInfo?.productList[productId]
if (!product) {
productInfo.count = 0
product = productInfo // 初始化
}
product.count += num
// && 短路運算符,前面的滿足纔會執行後面的邏輯,等價於if
num > 0 && (product.checked = true)
product.count <= 0 && (shopInfo[productId].count = 0)
// delete state.cartList[shopId]
shopInfo.productList[productId] = product
// 賦值
state.cartList[shopId] = shopInfo
},
// 購物車勾選記錄
changeItemChecked(state, payload) {
const { shopId, productId } = payload
const product = state.cartList[shopId].productList[productId]
product.checked = !product.checked
},
// 清除購物車
changeCleanCartProducts(state, payload) {
const { shopId } = payload
state.cartList[shopId].productList = {}
},
// 購物車全選或者取消全選
setCartItemsChecked(state, payload) {
const { shopId } = payload
const products = state.cartList[shopId].productList
if (products) {
for (const i in products) {
const product = products[i]
product.checked = true
}
}
},
/**
* 修改商店名稱
* @param {Object} state vuex對象
* @param {Object} payload 傳值
*/
changeShopName(state, payload) {
const { shopId, shopName } = payload
const shopInfo = state.cartList[shopId] || {
shopName: '',
productList: {}
}
shopInfo.shopName = shopName
state.cartList[shopId] = shopInfo
}
},
actions: {},
modules: {}
})
src\views\shop\Shop.vue
<template>
<div class="wrapper">
<div class="search">
<div class="search__back" @click="handleBackClick">
<i class="search__back__icon custom-icon custom-icon-back"></i>
</div>
<div class="search__content">
<span
><i class="search__content__icon custom-icon custom-icon-search"></i
></span>
<input class="search__content__input" placeholder="請輸入商品名稱" />
</div>
</div>
<!-- v-show="item.headImg" 防止撕裂圖片的出現 -->
<ShopInfo :item="item" :hideBorder="true" v-if="item.headImg" />
<Content :shopName="item.title" />
<Cart />
<Toast v-if="show" :message="message" />
</div>
</template>
<script>
import { reactive, toRefs, nextTick } from 'vue' // 路由跳轉方法
import { useRouter, useRoute } from 'vue-router'
import ShopInfo from '@/components/ShopInfo/ShopInfo'
import { get } from '@/utils/request.js'
import Toast, { useToastEffect } from '@/components/Toast/Toast'
import Content from '@/views/shop/Content'
import Cart from '@/views/shop/Cart'
// 獲取當前商鋪信息
const useShopInfoEffect = (toastMsg, route) => {
const data = reactive({ item: {} })
// eslint-disable-next-line no-unused-vars
const getItemData = async () => {
// 可以寫成: const resultData = await get(`/api/shop/${route.params.id}`)
console.log(' route.params.id:' + route.params.id)
const resultData = await get('/api/shop/' + route.params.id)
if (resultData?.code === 200 && resultData?.data) {
data.item = {
id: resultData.data?.id,
title: resultData.data?.name,
sales: resultData.data?.sales,
headImg: resultData.data?.imgUrl,
expressLimit: resultData.data?.expressLimit,
expressPrice: resultData.data?.expressPrice,
highlight: resultData.data?.slogon
}
console.log('data.item :' + JSON.stringify(data.item))
nextTick()
} else {
toastMsg('沒有數據!')
}
}
const { item } = toRefs(data)
return { item, getItemData }
}
// 後退按鈕事件
const useBackRouterEffect = router => {
const handleBackClick = () => {
router.back()
}
return { handleBackClick }
}
export default {
name: 'Shop',
components: { ShopInfo, Toast, Content, Cart },
// eslint-disable-next-line space-before-function-paren
setup() {
const router = useRouter() // 整個大路由的信息
const route = useRoute() // 當前訪問路徑的信息
const { show, message, toastMsg } = useToastEffect()
const { item, getItemData } = useShopInfoEffect(toastMsg, route)
const { handleBackClick } = useBackRouterEffect(router)
getItemData()
return { show, message, item, handleBackClick }
}
}
</script>
<style lang="scss" scoped>
@import '@/style/viriables';
.wrapper {
padding: 0 0.18rem;
}
.search {
margin: 0.14rem 0 0.04rem 0;
display: flex;
line-height: 0.32rem; //高度會將父元素撐開
&__back {
width: 0.3rem;
&__icon {
font-size: 0.2rem;
color: #b6b6b6;
}
}
&__content {
display: flex;
flex: 1;
background: $search-bg-color;
border-radius: 0.16rem;
&__icon {
padding-left: 0.1rem;
padding-right: 0.1rem;
width: 0.44rem;
text-align: center;
color: $search-font-color;
}
&__input {
padding-right: 0.2rem;
width: 100%;
display: block;
border: none;
outline: none;
background: none;
height: 0.32rem;
font-size: 0.14rem;
color: $content-font-color;
&::placeholder {
color: $content-font-color;
}
}
}
}
</style>
src\views\shop\Content.vue
<template>
<div class="content">
<div class="category">
<div
:class="{
category__item: true,
'category__item--active': currentTab === item.tab
}"
v-for="item in categories"
:key="item.tab"
@click="handleTabClick(item.tab)"
>
{{ item.name }}
</div>
</div>
<div class="product">
<div class="product__item" v-for="item in list" :key="item._id">
<img class="product__item__img" :src="item.imgUrl" />
<div class="product__item__detail">
<h4 class="product__item__title">{{ item.name }}</h4>
<p class="product__item__sales">月售{{ item.sales }}件</p>
<p class="product__item__price">
<span class="product__item__yen"> ¥{{ item.price }} </span>
<span class="product__item__origin">
¥{{ item.oldPrice }}
</span>
</p>
</div>
<div class="product__number">
<span
class="product__number__minus"
@click="
() => {
changeCartItem(shopId, item._id, item, -1, shopName)
}
"
>-</span
>
{{ cartList?.[shopId]?.productList.[item._id]?.count || 0 }}
<span
class="product__number__plus"
@click="
() => {
changeCartItem(shopId, item._id, item, 1, shopName)
}
"
>+</span
>
</div>
</div>
</div>
</div>
</template>
<script>
import { reactive, ref, toRefs, watchEffect } from 'vue'
import { useRoute } from 'vue-router' // 路由跳轉方法
import { useStore } from 'vuex'
import { get } from '@/utils/request.js'
import { useCommonCartEffect } from './commnCartEffect'
const categories = [
{
name: '全部商品',
tab: 'all'
},
{
name: '秒殺',
tab: 'seckill'
},
{
name: '新鮮水果',
tab: 'fruit'
},
{
name: '休閒食品',
tab: 'snack'
}
]
// 和tab切換相關的邏輯
const useTabEffect = () => {
const currentTab = ref(categories[0].tab)
const handleTabClick = tab => {
console.log('click:' + tab)
currentTab.value = tab
}
return { currentTab, handleTabClick }
}
// 當前列表內容相關的函數
const useContentListEffect = (currentTab, shopId) => {
const content = reactive({ list: [] })
const getContentData = async () => {
const result = await get(`/api/shop/${shopId}/products`, {
tab: currentTab.value
})
console.log('result:' + result)
if (result?.code === 200 && result?.data?.length) {
content.list = result.data
}
}
// watchEffect:當首次頁面加載時,或當其中監聽的數據發生變化時執行
watchEffect(() => {
getContentData()
})
const { list } = toRefs(content)
return { list }
}
export default {
name: 'Content',
props: {
id: String,
shopName: String
},
setup() {
const route = useRoute() // 獲取路由
const store = useStore()
const shopId = route.params.id
const { currentTab, handleTabClick } = useTabEffect()
const { list } = useContentListEffect(currentTab, shopId)
const { cartList, changeCartItemInfo } = useCommonCartEffect()
const changeShopName = (shopId, shopName) => {
store.commit('changeShopName', { shopId, shopName })
}
const changeCartItem = (shopId, productId, item, num, shopName) => {
changeCartItemInfo(shopId, productId, item, num)
changeShopName(shopId, shopName)
}
return {
cartList,
list,
categories,
handleTabClick,
currentTab,
shopId,
changeCartItem,
changeCartItemInfo
}
}
}
</script>
<style lang="scss" scoped>
@import '@/style/viriables.scss';
@import '@/style/mixins.scss';
.content {
display: flex;
position: absolute;
left: 0;
right: 0;
top: 1.6rem;
bottom: 0.5rem;
}
.category {
overflow-y: scroll;
width: 0.76rem;
background: $search-bg-color;
height: 100%;
&__item {
line-height: 0.4rem;
text-align: center;
font-size: 14px;
color: $content-font-color;
&--active {
background: $bg-color;
}
}
}
.product {
overflow-y: scroll;
flex: 1;
&__item {
position: relative;
display: flex;
padding: 0.12rem 0.16rem;
margin: 0 0.16rem;
border-bottom: 0.01rem solid $content-bg-color;
// 配合解決超出長度以省略號顯示而不會出現換行
&__detail {
overflow: hidden;
}
&__img {
width: 0.68rem;
height: 0.68rem;
margin-right: 0.16rem;
}
&__title {
margin: 0;
line-height: 0.2rem;
font-size: 0.14rem;
color: $content-font-color;
// 超出長度以省略號顯示而不會出現換行
@include ellipsis;
}
&__sales {
margin: 0.06rem 0;
line-height: 0.16rem;
font-size: 0.12rem;
color: $content-font-color;
}
&__price {
margin: 0;
line-height: 0.2rem;
font-size: 0.14rem;
color: $height-light-font-color;
}
&__yen {
font-size: 0.12rem;
}
&__origin {
margin-left: 0.06rem;
line-height: 0.2rem;
font-size: 0.12rem;
color: $light-font-color;
text-decoration: line-through; //中劃線
}
// 購物車選購數量和加減號
.product__number {
position: absolute;
right: 0rem;
bottom: 0.12rem;
&__minus,
&__plus {
display: inline-block;
width: 0.2rem;
height: 0.2rem;
line-height: 0.16rem;
border-radius: 50%;
font-size: 0.2rem;
text-align: center;
}
// 邊框白色
&__minus {
border: 0.01rem solid $medium-font-color;
color: $medium-font-color;
margin-right: 0.05rem;
}
//無邊框,背景藍色
&__plus {
color: $bg-color;
background: $btn-bg-color;
margin-left: 0.05rem;
}
}
}
}
</style>
src\views\shop\commnCartEffect.js
import { toRefs } from 'vue'
import { useStore } from 'vuex'
// 添加、減少到購物車功能
export const useCommonCartEffect = () => {
const store = useStore()
const { cartList } = toRefs(store.state)
/**
* 加入或減少購物車數量
* @param {String} shopId 店鋪id
* @param {String} productId 商品id
* @param {Object} productInfo 商品信息集
* @param {Number} num 加入購物車的數量
*/
const changeCartItemInfo = (shopId, productId, productInfo, num) => {
console.log(
'changeCartItemInfo:',
'shopId:' + shopId,
'productId:' + productId,
'productInfo:' + JSON.stringify(productInfo),
'num:' + num
)
// 更新vuex中的值
store.commit('changeItemToCart', { shopId, productId, productInfo, num })
}
return { cartList, changeCartItemInfo }
}
src\views\shop\Cart.vue
<template>
<!-- 蒙層 -->
<div class="mask" v-if="showCart" @click="handleCartShowChange"></div>
<div class="cart">
<div class="product" v-show="showCart">
<div class="product__header">
<div class="product__header__all" @click="setCartItemsChecked(shopId)">
<i
:class="[
'product__header__all__icon',
'custom-icon',
allChecked
? 'custom-icon-radio-checked'
: 'custom-icon-radio-unchecked'
]"
></i>
<span class="product__header__all__text">全選</span>
</div>
<div class="product__header__clear">
<span
class="product__header__clear__btn"
@click="cleanCartProducts(shopId)"
>清空購物車</span
>
</div>
</div>
<template v-for="item in productList" :key="item._id">
<div class="product__item" v-if="item.count > 0">
<div
class="product__item__checked"
@click="changeCartItemChecked(shopId, item._id)"
>
<i
:class="[
'custom-icon',
item.checked == true
? 'custom-icon-radio-checked'
: 'custom-icon-radio-unchecked'
]"
></i>
</div>
<img class="product__item__img" :src="item.imgUrl" />
<div class="product__item__detail">
<h4 class="product__item__title">{{ item.name }}</h4>
<p class="product__item__price">
<span class="product__item__yen"> ¥{{ item.price }} </span>
<span class="product__item__origin">
¥{{ item.oldPrice }}
</span>
</p>
</div>
<div class="product__number">
<span
class="product__number__minus"
@click="
() => {
0
changeCartItemInfo(shopId, item._id, item, -1)
}
"
>-</span
>
{{ cartList?.[shopId]?.productList.[item._id]?.count || 0 }}
<span
class="product__number__plus"
@click="
() => {
changeCartItemInfo(shopId, item._id, item, 1)
}
"
>+</span
>
</div>
</div>
</template>
</div>
<div class="check">
<div class="check__icon" @click="handleCartShowChange">
<img src="/i18n/9_16/img/basket.png" alt="" class="check__icon__img" />
<div class="check__icon__tag">
{{ total }}
</div>
</div>
<div class="check__info">
總計:<span class="check__info__price">¥ {{ totalPrice }}</span>
</div>
<div class="check__btn">
<router-link :to="{ name: 'Home' }">
去結算
</router-link>
</div>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router' // 路由跳轉方法
import { useStore } from 'vuex' // 路由跳轉方法
import { useCommonCartEffect } from './commnCartEffect'
const useCartEffect = shopId => {
const { changeCartItemInfo } = useCommonCartEffect()
const store = useStore()
// 單個勾選或者不勾選
const changeCartItemChecked = (shopId, productId) => {
store.commit('changeItemChecked', { shopId, productId })
}
// 清除購物車按鈕
const cleanCartProducts = shopId => {
store.commit('changeCleanCartProducts', { shopId })
}
// 購物車全選或者取消全選
const setCartItemsChecked = shopId => {
store.commit('setCartItemsChecked', { shopId })
}
// 計算shopId下所有cartList的商品數量total、價錢之和totalPrice
const cartList = store.state.cartList // 加入購物車的商品列表fv
const total = computed(() => {
const productList = cartList[shopId]?.productList
let count = 0
if (productList) {
for (const i in productList) {
const product = productList[i]
count += product.count
}
}
return count
})
const totalPrice = computed(() => {
const productList = cartList[shopId]?.productList
let count = 0
if (productList) {
for (const i in productList) {
const product = productList[i]
if (product.checked === true) {
count += product.count * product.price
}
}
}
return count.toFixed(2) // 保留2位小數
})
// 全選的計算屬性
const allChecked = computed(() => {
const productList = cartList[shopId]?.productList
let result = true
if (productList) {
for (const i in productList) {
const product = productList[i]
if (product.count > 0 && !product.checked) {
result = false
break
}
}
}
return result
})
const productList = computed(() => {
const productInfoList = cartList[shopId]?.productList || [] // 不存在默認空數組
return productInfoList
})
return {
cartList,
total,
totalPrice,
productList,
allChecked,
changeCartItemChecked,
changeCartItemInfo,
cleanCartProducts,
setCartItemsChecked
}
}
// 展示隱藏購物車
const toggleCartEffect = () => {
const showCart = ref(false)
// 顯示隱藏購物車具體內容
const handleCartShowChange = () => {
showCart.value = !showCart.value
}
return { showCart, handleCartShowChange }
}
export default {
name: 'Cart',
setup() {
const route = useRoute()
const shopId = route.params.id // 店鋪id
// 展示隱藏購物車
const { showCart, handleCartShowChange } = toggleCartEffect()
// 計算總價和加入購物車的總數量
const {
cartList,
total,
totalPrice,
productList,
allChecked,
changeCartItemChecked,
changeCartItemInfo,
cleanCartProducts,
setCartItemsChecked
} = useCartEffect(shopId)
return {
cartList,
total,
totalPrice,
productList,
shopId,
allChecked,
showCart,
handleCartShowChange,
changeCartItemChecked,
changeCartItemInfo,
cleanCartProducts,
setCartItemsChecked
}
}
}
</script>
<style lang="scss" scoped>
@import '@/style/viriables.scss';
@import '@/style/mixins.scss';
.mask {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.cart {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
background: $bg-color;
}
.product {
overflow-y: scroll;
flex: 1;
background: $bg-color;
&__header {
display: flex;
line-height: 0.52rem;
border-bottom: 0.01rem solid $content-bg-color;
font-size: 0.14rem;
color: $content-font-color;
&__all {
width: 0.64rem;
margin-left: 0.18rem;
&__icon {
display: inline-block;
vertical-align: top;
font-size: 0.2rem;
margin-right: 0.05rem;
color: $btn-bg-color;
}
&__text {
display: inline-block;
margin-left: 0.04rem;
line-height: 0.52rem;
}
}
&__clear {
flex: 1;
text-align: right;
margin-right: 0.16rem;
&__btn {
display: inline-block;
}
}
}
&__item {
position: relative;
display: flex;
padding: 0.12rem 0.16rem;
margin: 0 0.16rem;
border-bottom: 0.01rem solid $content-bg-color;
&__checked {
line-height: 0.5rem;
margin-right: 0.2rem;
color: $btn-bg-color;
i {
font-size: 0.25rem;
}
}
// 配合解決超出長度以省略號顯示而不會出現換行
&__detail {
overflow: hidden;
}
&__img {
width: 0.46rem;
height: 0.46rem;
margin-right: 0.16rem;
}
&__title {
margin: 0;
line-height: 0.2rem;
font-size: 0.14rem;
color: $content-font-color;
// 超出長度以省略號顯示而不會出現換行
@include ellipsis;
}
&__price {
margin: 0.06rem 0 0 0;
line-height: 0.2rem;
font-size: 0.14rem;
color: $height-light-font-color;
}
&__yen {
font-size: 0.12rem;
}
&__origin {
margin-left: 0.06rem;
line-height: 0.2rem;
font-size: 0.12rem;
color: $light-font-color;
text-decoration: line-through; //中劃線
}
// 購物車選購數量和加減號
.product__number {
position: absolute;
right: 0rem;
bottom: 0.26rem;
&__minus,
&__plus {
display: inline-block;
width: 0.2rem;
height: 0.2rem;
line-height: 0.16rem;
border-radius: 50%;
font-size: 0.2rem;
text-align: center;
}
// 邊框白色
&__minus {
border: 0.01rem solid $medium-font-color;
color: $medium-font-color;
margin-right: 0.05rem;
}
//無邊框,背景藍色
&__plus {
color: $bg-color;
background: $btn-bg-color;
margin-left: 0.05rem;
}
}
}
}
.check {
display: flex;
box-sizing: border-box; //往內塞入border
line-height: 0.49rem;
height: 0.49rem;
border-top: 0.01rem solid $content-bg-color;
&__icon {
width: 0.84rem;
position: relative;
&__img {
margin: 0.12rem auto;
display: block;
width: 0.28rem;
height: 0.28rem;
}
&__tag {
// 乘以2然後等比例縮小
position: absolute;
left: 0.46rem;
top: 0.04rem;
padding: 0 0.04rem;
min-width: 0.2rem;
height: 0.2rem;
line-height: 0.2rem;
text-align: center;
background-color: $height-light-font-color;
border-radius: 0.1rem;
font-size: 0.12rem;
color: $bg-color;
transform: scale(0.5);
transform-origin: left center;
}
}
&__info {
flex: 1;
color: $content-font-color;
font-size: 0.12rem;
&__price {
line-height: 0.49rem;
color: $height-light-font-color;
font-size: 0.18rem;
}
}
&__btn {
width: 0.98rem;
background-color: #4fb0f9;
text-align: center;
color: $bg-color;
font-size: 0.14rem;
// 去掉a標籤的下劃線
a {
color: $bg-color;
text-decoration: none; //去掉文本修飾
}
}
}
</style>
最終效果如下:
繼續優化一下src\views\shop\Content.vue中的流程代碼:
<script>
import { reactive, ref, toRefs, watchEffect } from 'vue'
import { useRoute } from 'vue-router' // 路由跳轉方法
import { useStore } from 'vuex'
import { get } from '@/utils/request.js'
import { useCommonCartEffect } from './commnCartEffect'
const categories = [
{
name: '全部商品',
tab: 'all'
},
{
name: '秒殺',
tab: 'seckill'
},
{
name: '新鮮水果',
tab: 'fruit'
},
{
name: '休閒食品',
tab: 'snack'
}
]
// 和tab切換相關的邏輯
const useTabEffect = () => {
const currentTab = ref(categories[0].tab)
const handleTabClick = tab => {
console.log('click:' + tab)
currentTab.value = tab
}
return { currentTab, handleTabClick }
}
// 當前列表內容相關的函數
const useContentListEffect = (currentTab, shopId) => {
const content = reactive({ list: [] })
const getContentData = async () => {
const result = await get(`/api/shop/${shopId}/products`, {
tab: currentTab.value
})
console.log('result:' + result)
if (result?.code === 200 && result?.data?.length) {
content.list = result.data
}
}
// watchEffect:當首次頁面加載時,或當其中監聽的數據發生變化時執行
watchEffect(() => {
getContentData()
})
const { list } = toRefs(content)
return { list }
}
const useCartEffect = shopId => {
const { cartList, changeCartItemInfo } = useCommonCartEffect()
const store = useStore()
const changeShopName = (shopId, shopName) => {
store.commit('changeShopName', { shopId, shopName })
}
const changeCartItem = (shopId, productId, item, num, shopName) => {
changeCartItemInfo(shopId, productId, item, num)
changeShopName(shopId, shopName)
}
return {
cartList,
changeCartItem
}
}
export default {
name: 'Content',
props: {
id: String,
shopName: String
},
setup() {
const route = useRoute() // 獲取路由
const shopId = route.params.id
const { currentTab, handleTabClick } = useTabEffect()
const { list } = useContentListEffect(currentTab, shopId)
const { cartList, changeCartItem } = useCartEffect(shopId)
return {
cartList,
list,
categories,
handleTabClick,
currentTab,
shopId,
changeCartItem
}
}
}
</script>
優化購物車爲空時,全選和清空購物車也不顯示,修改src\views\shop\Cart.vue
:
<template>
<!-- 蒙層 -->
<div
class="mask"
v-if="showCart && total > 0"
@click="handleCartShowChange"
></div>
<div class="cart">
<div class="product" v-show="showCart && total > 0">
......
優化計算屬性:
<template>
<!-- 蒙層 -->
<div
class="mask"
v-if="showCart && calculations.total > 0"
@click="handleCartShowChange"
></div>
<div class="cart">
<div class="product" v-show="showCart && calculations.total > 0">
<div class="product__header">
<div class="product__header__all" @click="setCartItemsChecked(shopId)">
<i
:class="[
'product__header__all__icon',
'custom-icon',
calculations.isAllChecked
? 'custom-icon-radio-checked'
: 'custom-icon-radio-unchecked'
]"
></i>
<span class="product__header__all__text">全選</span>
</div>
<div class="product__header__clear">
<span
class="product__header__clear__btn"
@click="cleanCartProducts(shopId)"
>清空購物車</span
>
</div>
</div>
<template v-for="item in productList" :key="item._id">
<div class="product__item" v-if="item.count > 0">
<div
class="product__item__checked"
@click="changeCartItemChecked(shopId, item._id)"
>
<i
:class="[
'custom-icon',
item.checked == true
? 'custom-icon-radio-checked'
: 'custom-icon-radio-unchecked'
]"
></i>
</div>
<img class="product__item__img" :src="item.imgUrl" />
<div class="product__item__detail">
<h4 class="product__item__title">{{ item.name }}</h4>
<p class="product__item__price">
<span class="product__item__yen"> ¥{{ item.price }} </span>
<span class="product__item__origin">
¥{{ item.oldPrice }}
</span>
</p>
</div>
<div class="product__number">
<span
class="product__number__minus"
@click="
() => {
0
changeCartItemInfo(shopId, item._id, item, -1)
}
"
>-</span
>
{{ cartList?.[shopId]?.productList.[item._id]?.count || 0 }}
<span
class="product__number__plus"
@click="
() => {
changeCartItemInfo(shopId, item._id, item, 1)
}
"
>+</span
>
</div>
</div>
</template>
</div>
<div class="check">
<div class="check__icon" @click="handleCartShowChange">
<img src="/i18n/9_16/img/basket.png" alt="" class="check__icon__img" />
<div class="check__icon__tag">
{{ calculations.total }}
</div>
</div>
<div class="check__info">
總計:<span class="check__info__price"
>¥ {{ calculations.totalPrice }}</span
>
</div>
<div class="check__btn">
<router-link :to="{ name: 'Home' }">
去結算
</router-link>
</div>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router' // 路由跳轉方法
import { useStore } from 'vuex' // 路由跳轉方法
import { useCommonCartEffect } from './commonCartEffect'
const useCartEffect = shopId => {
const store = useStore()
const { changeCartItemInfo } = useCommonCartEffect()
const cartList = store.state.cartList // 加入購物車的商品列表
// 單個勾選或者不勾選
const changeCartItemChecked = (shopId, productId) => {
store.commit('changeItemChecked', { shopId, productId })
}
// 清除購物車按鈕
const cleanCartProducts = shopId => {
store.commit('changeCleanCartProducts', { shopId })
}
// 購物車全選或者取消全選
const setCartItemsChecked = shopId => {
store.commit('setCartItemsChecked', { shopId })
}
// 計算shopId下所有cartList的商品數量total、價錢之和totalPrice
const calculations = computed(() => {
const productList = cartList[shopId]?.productList
const resultData = {
// 總商品數量
total: 0,
// 總金額
totalPrice: 0,
// 全選
isAllChecked: true
}
debugger
if (productList) {
for (const i in productList) {
const product = productList[i]
// 總商品數量
resultData.total += product.count
// 總金額
if (product.checked === true) {
resultData.totalPrice += product.count * product.price
}
// 全選
if (product.count > 0 && !product.checked) {
resultData.isAllChecked = false
}
}
resultData.totalPrice = resultData.totalPrice.toFixed(2) // 保留2位小數
}
return resultData
})
// const total = computed(() => {
// const productList = cartList[shopId]?.productList
// let count = 0
// if (productList) {
// for (const i in productList) {
// const product = productList[i]
// count += product.count
// }
// }
// return count
// })
// const totalPrice = computed(() => {
// const productList = cartList[shopId]?.productList
// let count = 0
// if (productList) {
// for (const i in productList) {
// const product = productList[i]
// if (product.checked === true) {
// count += product.count * product.price
// }
// }
// }
// return count.toFixed(2) // 保留2位小數
// })
// 全選的計算屬性
// const allChecked = computed(() => {
// const productList = cartList[shopId]?.productList
// let result = true
// if (productList) {
// for (const i in productList) {
// const product = productList[i]
// if (product.count > 0 && !product.checked) {
// result = false
// break
// }
// }
// }
// return result
// })
const productList = computed(() => {
const productInfoList = cartList[shopId]?.productList || [] // 不存在默認空數組
return productInfoList
})
return {
cartList,
calculations,
productList,
changeCartItemChecked,
changeCartItemInfo,
cleanCartProducts,
setCartItemsChecked
}
}
// 展示隱藏購物車
const toggleCartEffect = () => {
const showCart = ref(false)
// 顯示隱藏購物車具體內容
const handleCartShowChange = () => {
showCart.value = !showCart.value
}
return { showCart, handleCartShowChange }
}
export default {
name: 'Cart',
setup() {
const route = useRoute()
const shopId = route.params.id // 店鋪id
// 展示隱藏購物車
const { showCart, handleCartShowChange } = toggleCartEffect()
// 計算總價和加入購物車的總數量
const {
cartList,
calculations,
productList,
changeCartItemChecked,
changeCartItemInfo,
cleanCartProducts,
setCartItemsChecked
} = useCartEffect(shopId)
return {
cartList,
calculations,
productList,
shopId,
showCart,
handleCartShowChange,
changeCartItemChecked,
changeCartItemInfo,
cleanCartProducts,
setCartItemsChecked
}
}
}
</script>
<style lang="scss" scoped>
@import '@/style/viriables.scss';
@import '@/style/mixins.scss';
.mask {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.cart {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
background: $bg-color;
}
.product {
overflow-y: scroll;
flex: 1;
background: $bg-color;
&__header {
display: flex;
line-height: 0.52rem;
border-bottom: 0.01rem solid $content-bg-color;
font-size: 0.14rem;
color: $content-font-color;
&__all {
width: 0.64rem;
margin-left: 0.18rem;
&__icon {
display: inline-block;
vertical-align: top;
font-size: 0.2rem;
margin-right: 0.05rem;
color: $btn-bg-color;
}
&__text {
display: inline-block;
margin-left: 0.04rem;
line-height: 0.52rem;
}
}
&__clear {
flex: 1;
text-align: right;
margin-right: 0.16rem;
&__btn {
display: inline-block;
}
}
}
&__item {
position: relative;
display: flex;
padding: 0.12rem 0.16rem;
margin: 0 0.16rem;
border-bottom: 0.01rem solid $content-bg-color;
&__checked {
line-height: 0.5rem;
margin-right: 0.2rem;
color: $btn-bg-color;
i {
font-size: 0.25rem;
}
}
// 配合解決超出長度以省略號顯示而不會出現換行
&__detail {
overflow: hidden;
}
&__img {
width: 0.46rem;
height: 0.46rem;
margin-right: 0.16rem;
}
&__title {
margin: 0;
line-height: 0.2rem;
font-size: 0.14rem;
color: $content-font-color;
// 超出長度以省略號顯示而不會出現換行
@include ellipsis;
}
&__price {
margin: 0.06rem 0 0 0;
line-height: 0.2rem;
font-size: 0.14rem;
color: $height-light-font-color;
}
&__yen {
font-size: 0.12rem;
}
&__origin {
margin-left: 0.06rem;
line-height: 0.2rem;
font-size: 0.12rem;
color: $light-font-color;
text-decoration: line-through; //中劃線
}
// 購物車選購數量和加減號
.product__number {
position: absolute;
right: 0rem;
bottom: 0.26rem;
&__minus,
&__plus {
display: inline-block;
width: 0.2rem;
height: 0.2rem;
line-height: 0.16rem;
border-radius: 50%;
font-size: 0.2rem;
text-align: center;
}
// 邊框白色
&__minus {
border: 0.01rem solid $medium-font-color;
color: $medium-font-color;
margin-right: 0.05rem;
}
//無邊框,背景藍色
&__plus {
color: $bg-color;
background: $btn-bg-color;
margin-left: 0.05rem;
}
}
}
}
.check {
display: flex;
box-sizing: border-box; //往內塞入border
line-height: 0.49rem;
height: 0.49rem;
border-top: 0.01rem solid $content-bg-color;
&__icon {
width: 0.84rem;
position: relative;
&__img {
margin: 0.12rem auto;
display: block;
width: 0.28rem;
height: 0.28rem;
}
&__tag {
// 乘以2然後等比例縮小
position: absolute;
left: 0.46rem;
top: 0.04rem;
padding: 0 0.04rem;
min-width: 0.2rem;
height: 0.2rem;
line-height: 0.2rem;
text-align: center;
background-color: $height-light-font-color;
border-radius: 0.1rem;
font-size: 0.12rem;
color: $bg-color;
transform: scale(0.5);
transform-origin: left center;
}
}
&__info {
flex: 1;
color: $content-font-color;
font-size: 0.12rem;
&__price {
line-height: 0.49rem;
color: $height-light-font-color;
font-size: 0.18rem;
}
}
&__btn {
width: 0.98rem;
background-color: #4fb0f9;
text-align: center;
color: $bg-color;
font-size: 0.14rem;
// 去掉a標籤的下劃線
a {
color: $bg-color;
text-decoration: none; //去掉文本修飾
}
}
}
</style>
這裏還可以將參數封裝一下
src\views\shop\Content.vue
......
<div class="product__number">
<span
class="product__number__minus"
@click="
() => {
changeCartItem(shopId, item._id, item, -1, shopName)
}
"
>-</span
>
{{ getProductCartCount(shopId, item._id) }}
<span
class="product__number__plus"
@click="
() => {
changeCartItem(shopId, item._id, item, 1, shopName)
}
"
>+</span
>
</div>
......
<script>
......
const useCartEffect = shopId => {
const { changeCartItemInfo } = useCommonCartEffect()
const store = useStore()
const cartList = store.state.cartList // 加入購物車的商品
......
const getProductCartCount = (shopId, productId) => {
return cartList?.[shopId]?.productList?.[productId]?.count || 0
}
return {
cartList,
changeCartItem,
getProductCartCount
}
}
......
</script>
將值存入localstorage
修改src\store\index.js
import { createStore } from 'vuex'
const setLocalStorage = state => {
const cartList = state.cartList
const cartListString = JSON.stringify(cartList)
localStorage.cartList = cartListString
}
export default createStore({
state: {
cartList: {
// shopId: {
// shopName: '沃什麼碼',
// productList: {
// productId: {
// _id: '1',
// name: '番茄250g/份',
// imgUrl: '/i18n/9_16/img/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 0
// }
// }
// }
// ================== 之前版本的結構 ==================
// 第一層級:商鋪的id
// 第二層內容是商品內容以及購物數量
// shopId: {
// productID: {
// _id: '1',
// name: '番茄250g/份',
// imgUrl: '/i18n/9_16/img/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 0
// }
// }
}
},
mutations: {
/**
* 加入或減少購物車數量
* @param {*} state
* @param {String} shopId 店鋪id
* @param {String} productId 商品id
* @param {Object} productInfo 商品信息集
* @param {Number} num 加入購物車的數量
* @param {*} payload
*/
changeItemToCart(state, payload) {
const { shopId, productId, productInfo, num } = payload
// console.log(shopId, productId, productInfo)
const shopInfo = state.cartList[shopId] || {
shopName: '',
productList: {}
}
let product = shopInfo?.productList[productId]
if (!product) {
productInfo.count = 0
product = productInfo // 初始化
}
product.count += num
// && 短路運算符,前面的滿足纔會執行後面的邏輯,等價於if
num > 0 && (product.checked = true)
product.count <= 0 && (shopInfo[productId].count = 0)
// delete state.cartList[shopId]
shopInfo.productList[productId] = product
// 賦值
state.cartList[shopId] = shopInfo
setLocalStorage(state)
},
// 購物車勾選記錄
changeItemChecked(state, payload) {
const { shopId, productId } = payload
const product = state.cartList[shopId].productList[productId]
product.checked = !product.checked
setLocalStorage(state)
},
// 清除購物車
changeCleanCartProducts(state, payload) {
const { shopId } = payload
state.cartList[shopId].productList = {}
setLocalStorage(state)
},
// 購物車全選或者取消全選
setCartItemsChecked(state, payload) {
const { shopId } = payload
const products = state.cartList[shopId].productList
if (products) {
for (const i in products) {
const product = products[i]
product.checked = true
}
}
setLocalStorage(state)
},
/**
* 修改商店名稱
* @param {Object} state vuex對象
* @param {Object} payload 傳值
*/
changeShopName(state, payload) {
const { shopId, shopName } = payload
const shopInfo = state.cartList[shopId] || {
shopName: '',
productList: {}
}
shopInfo.shopName = shopName
state.cartList[shopId] = shopInfo
}
},
actions: {},
modules: {}
})
完善讀取:
import { createStore } from 'vuex'
/**
* 存入localStorage
* @param {Object} state
*/
const setLocalCartList = state => {
const cartList = state.cartList
const cartListString = JSON.stringify(cartList)
localStorage.cartList = cartListString
}
/**
* 從localStorage讀取
* @param {Object} state
*/
const getLocalCartList = () => {
return JSON.parse(localStorage.cartList) || {}
}
export default createStore({
state: {
cartList: getLocalCartList()
......
這樣刷新也能看到數據回顯。