Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
1、效果展示
注意:这个演示图没有背景色,背景色建议在 App.vue 中新增代码实现全局背景色
css
<style>
page {
background-color: #f8f9fb;
}
</style>
2、安装 Pinia 和 Uview
官网
c
https://pinia.vuejs.org/zh/getting-started.html
安装命令
c
cnpm install pinia
Uiew 的安装以及配置参照我的这篇文章
Uniapp + Vite + Vue3 + uView + Pinia 实现自定义底部 Tabbar(最新保姆级教程)
3、配置 Pinia
- main.js
js
import { createPinia } from 'pinia'
const app = createSSRApp(App);
app.use(pinia);
- cart.js
js
// src/pages/store/cart/cart.js
import {
defineStore
} from 'pinia';
import {
reactive,
computed
} from 'vue';
export const useCartStore = defineStore('cart', () => {
// 用 reactive 管理购物车数据
const state = reactive({
cartItems: [], // 购物车商品列表
allChose: false // 全选状态
});
// 设置购物车数据
const setCartItems = (items) => {
state.cartItems = items.map(item => ({
...item,
isChoose: false, // 初始化为未选中状态
num: item.num || 1 // 初始化数量
}));
saveCartToLocalStorage(); // 每次设置后将数据持久化
};
// 计算已选中的商品数量
const selectedItemsCount = computed(() => {
return state.cartItems.reduce((count, shop) => {
return count + shop.items.filter(item => item.isChoose).reduce((shopCount, item) =>
shopCount + item.num, 0);
}, 0);
});
// 计算已选中商品的总价格
const totalSelectedPrice = computed(() => {
return state.cartItems.reduce((total, shop) => {
return total + shop.items.filter(item => item.isChoose).reduce((shopTotal, item) =>
shopTotal + item.price * item.num, 0);
}, 0);
});
// 切换商品的选中状态
const toggleItemChoose = (shopName, itemId) => {
const shop = state.cartItems.find(shop => shop.shopName === shopName);
console.log(shop);
if (shop) {
const cartItem = shop.items.find(cartItem => cartItem.id === itemId);
if (cartItem) {
cartItem.isChoose = !cartItem.isChoose;
}
updateAllChoseStatus(); // 每次切换选中状态后更新全选状态
saveCartToLocalStorage();
}
};
// 修改商品数量
const changeItemQuantity = (shopName, itemId, quantity) => {
const shop = state.cartItems.find(shop => shop.shopName === shopName);
if (shop) {
const cartItem = shop.items.find(cartItem => cartItem.id === itemId);
if (cartItem) {
cartItem.num = quantity;
}
saveCartToLocalStorage();
}
};
// 获取所有已选中的商品
const selectedItems = computed(() => {
return state.cartItems.reduce((selected, shop) => {
const selectedShopItems = shop.items.filter(item => item.isChoose);
if (selectedShopItems.length > 0) {
selected.push(...selectedShopItems);
}
return selected;
}, []);
});
// 切换全选状态
const toggleAllChose = () => {
state.allChose = !state.allChose;
state.cartItems.forEach(shop => {
shop.items.forEach(item => {
item.isChoose = state.allChose;
});
});
saveCartToLocalStorage();
};
// 更新全选状态
const updateAllChoseStatus = () => {
// 遍历所有店铺的所有商品,如果有一个未选中,则全选状态为 false
state.allChose = state.cartItems.every(shop =>
shop.items.every(item => item.isChoose)
);
};
// 将购物车数据保存到 localStorage
const saveCartToLocalStorage = () => {
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
};
// 从 localStorage 中恢复购物车数据
const loadCartFromLocalStorage = () => {
const savedCart = localStorage.getItem('cartItems');
if (savedCart) {
state.cartItems = JSON.parse(savedCart);
}
};
return {
state,
setCartItems, // 暴露 setCartItems 方法
selectedItems,
selectedItemsCount,
totalSelectedPrice,
toggleItemChoose,
changeItemQuantity,
toggleAllChose,
loadCartFromLocalStorage
};
});
4、页面展示
js
<template>
<view class="">
<view class="card">
<template v-for="(info, j) in state.cartItems" :key="j">
<view class="cart-data card-shadow">
<view class="" style="display: flex;">
{{info.shopName}}<up-icon name="arrow-right"></up-icon>
</view>
<template v-for="(item, index) in info.items" :key="index">
<view class="" style="display: flex;padding: 20rpx 0;align-items: center;">
<view>
<up-checkbox :customStyle="{marginBottom: '8px'}" usedAlone
v-model:checked="item.isChoose" @change="toggleItemChoose(item.shopName, item.id)">
</up-checkbox>
</view>
<view class="cart-image">
<up-image :src="item.image" mode="widthFix" height="200rpx" width="220rpx"
radius="10"></up-image>
</view>
<view>
<view class="cart-right">
<view style="margin-bottom: 10rpx;font-size: 30rpx;">{{item.title}}</view>
<view style="margin-bottom: 20rpx;font-size: 26rpx;color: #7d7e80;">{{item.type}}
</view>
<view class="" style="display: flex;align-items: center;">
<up-text mode="price" :text="item.price"></up-text>
<view class="" style="width: 10rpx;"></view>
<up-number-box v-model="item.num"
@change="val => changeItemQuantity(item,item.iid, val.value)"
min="1"></up-number-box>
</view>
</view>
</view>
</view>
</template>
</view>
</template>
</view>
<view class="foot card">
<view class="card-connect">
<up-checkbox :customStyle="{marginBottom: '8px'}" usedAlone v-model:checked="state.allChose"
@change="toggleAllChose">
</up-checkbox>
<view class="" style="display: flex; align-items: center;">
<view style="font-size: 28rpx;">全选</view>
<view style="padding-left: 20rpx;font-size: 24rpx;">已选{{selectedItemsCount}}件,合计</view>
<view class="" style="display: flex;flex: 1;">
<up-text mode="price" :text="totalSelectedPrice" color="red" size="18"></up-text>
</view>
</view>
<view class="" style="width: 20rpx;position: relative;">
</view>
<view class="" style="position: absolute;right: 40rpx;">
<view class="" style="display: flex;">
<up-button type="error" text="去结算" shape="circle" style="width: 150rpx;"
@click="toSubmitOrder"></up-button>
</view>
</view>
<up-toast ref="uToastRef"></up-toast>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted,
watch
} from 'vue';
import {
useCartStore
} from '@/pages/store/cart/cart.js'
import {
storeToRefs
} from "pinia";
// 使用 Pinia store
const cartStore = useCartStore();
// 获取状态和操作
const {
state,
selectedItemsCount,
totalSelectedPrice,
selectedItems
} = storeToRefs(cartStore);
const {
toggleItemChoose,
changeItemQuantity,
toggleAllChose
} = cartStore;
onMounted(async () => {
// 恢复购物车数据
// 模拟 API 请求获取购物车数据
const response = await fetchCartData();
const groupedItems = [];
response.forEach(item => {
// 查找是否已经存在相同店铺名的对象
const shop = groupedItems.find(shop => shop.shopName === item.shopName);
if (shop) {
// 如果存在,直接将商品添加到该店铺的商品列表中
shop.items.push(item);
} else {
// 如果不存在,创建一个新的店铺对象,并将商品添加进去
groupedItems.push({
shopName: item.shopName,
items: [item]
});
}
});
console.log(groupedItems);
cartStore.setCartItems(groupedItems);
});
// 创建响应式数据
const show = ref(false);
// 方法
const uToastRef = ref(null)
const showToast = (params) => {
uToastRef.value.show(params);
}
const toSubmitOrder = () => {
if (selectedItems.value.length > 0) {
uni.navigateTo({
url: "/pages/src/home/submit-order/submit-order"
})
} else {
showToast({
type: 'default',
title: '默认主题',
message: "您还没有选择商品哦",
});
}
}
// 模拟 API 请求函数
async function fetchCartData() {
return [{
id: 1,
shopName: "三只松鼠旗舰店",
isChoose: false,
image: "https://gw.alicdn.com/imgextra/i3/2218288872763/O1CN01rN6Cn91WHVIflhWLg_!!2218288872763.jpg",
title: "散装土鸡蛋 360枚 40斤",
type: "40斤 正负25g",
price: 29.9,
num: 1
},
{
id: 2,
isChoose: false,
shopName: "三只松鼠旗舰店",
image: "https://gw.alicdn.com/imgextra/i1/2218288872763/O1CN01DipCdH1WHVIqTtCQR_!!0-item_pic.jpg",
title: "散装土鸡蛋 360枚 40斤",
type: "40斤 正负25g",
price: 30,
num: 1
},
{
id: 2,
isChoose: false,
shopName: "三只耗子",
image: "https://gw.alicdn.com/imgextra/i1/2218288872763/O1CN01DipCdH1WHVIqTtCQR_!!0-item_pic.jpg",
title: "散装土鸡蛋 360枚 40斤",
type: "40斤 正负25g",
price: 29.9,
num: 1
}
];
}
</script>
<style lang="scss" scoped>
.foot {
position: fixed;
bottom: 0;
left: 0;
width: 90%;
/* 占据全宽 */
height: 100rpx;
/* Tabbar 高度 */
background-color: #FFF;
display: flex;
align-items: center;
.card-connect {
display: flex;
align-items: center;
justify-content: space-between;
}
}
.card {
margin: 20rpx;
padding: 20rpx;
background-color: #FFF;
border-radius: 20rpx;
}
.card-shadow {
border-radius: 20rpx;
box-shadow: 10rpx 10rpx 10rpx 10rpx rgba(0.2, 0.1, 0.2, 0.2);
}
.cart-data {
margin-bottom: 40rpx;
padding: 20rpx;
display: flex;
flex-wrap: wrap;
align-items: center;
.cart-image {
flex: 1;
}
.cart-right {
display: flex;
flex-direction: column;
padding-left: 20rpx;
}
}
</style>