当前位置: 首页 > article >正文

Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)

Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)

  • 1、效果展示
  • 2、安装 Pinia 和 Uview
  • 3、配置 Pinia
  • 4、页面展示

1、效果展示


在这里插入图片描述

注意:这个演示图没有背景色,背景色建议在 App.vue 中新增代码实现全局背景色

<style>
	page {
		background-color: #f8f9fb;
	}
</style>

2、安装 Pinia 和 Uview


官网

https://pinia.vuejs.org/zh/getting-started.html

安装命令

cnpm install pinia

Uiew 的安装以及配置参照我的这篇文章

Uniapp + Vite + Vue3 + uView + Pinia 实现自定义底部 Tabbar(最新保姆级教程)

3、配置 Pinia


  • main.js
import { createPinia } from 'pinia'
const app = createSSRApp(App);
app.use(pinia);
  • cart.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、页面展示


<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>

http://www.kler.cn/a/309124.html

相关文章:

  • NCC前端调用查询弹框
  • git之 revert和rebase
  • Python酷库之旅-第三方库Pandas(208)
  • nuxt3添加wowjs动效
  • 分布式----Ceph部署
  • 统信UOS开发环境支持Electron
  • Linux下root用户共享conda环境给其他用户
  • 力扣121-买卖股票的最佳时机(Java详细题解)
  • Encountered 31 files that should have been pointers, but weren‘t:(已解决,无废话)
  • System.out源码解读——err 和 out 一起用导致的顺序异常Bug
  • 论文翻译:USENIX-2021 Extracting Training Data from Large Language Models
  • 网络设备登录——《路由与交换技术》实验报告
  • 养宠浮毛严重怎么清理?希喂、范罗士、IAM宠物空气净化器真实测评
  • C++:初始化列表
  • 在线包装盒型生成工具,各种异型包装盒型,PDF导出方便
  • 【蜡笔小新专享】安装虚拟机、PHP、DVWA
  • Linux容器化管理——Docker常见命令总结
  • Apache Pulsar 与 Kafka Streams
  • React实现类似Vue的路由监听Hook
  • 在新电脑上将文件推送到已有的 Git 仓库
  • 【编程基础知识】Java命名规范及最佳实践
  • 孙怡带你深度学习(2)--PyTorch框架认识
  • Unity实战案例全解析:PVZ 植物卡片状态分析
  • 【乐企】基础版接口代码实现
  • Spring Boot校园管理系统:技术选型与架构设计
  • Java | Leetcode Java题解之第405题数字转换为十六进制数