Hello!宝子们,之前我们实现了vue3系列实战一:todolist,今天我们继续我们的vue3系列实战二:vue3完整的购物车系统,我相信每个前端开发人员绕不开的一个系统都是商城系统,无论你是企业开发还是个人开发多多少少都会接触到电商项目,话不多说,让我们开始今天的内容吧!
项目效果展示:
技术栈:vue3 + vite + elementplus + tailwindcss + pinia。
一:项目搭建和安装依赖库
vite官网
elementplus官网
npm create vite@latest vue3-cart -- --template vu+ts
安装elementplus:
npm install element-plus @element-plus/icons-vue
接下来我们在main.ts里面配置相关依赖
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import App from "./App.vue";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(ElementPlus);
app.mount("#app");
在App.vue,添加测试代码
<script setup lang="ts">
import {
Check,
Delete,
Edit,
Message,
Search,
Star,
} from "@element-plus/icons-vue";
</script>
<template>
<div>
<el-button :icon="Search" circle />
<el-button type="primary" :icon="Edit" circle />
<el-button type="success" :icon="Check" circle />
<el-button type="info" :icon="Message" circle />
<el-button type="warning" :icon="Star" circle />
<el-button type="danger" :icon="Delete" circle />
</div>
</template>
<style scoped></style>
在命令行运行项目
npm run dev
在浏览器中访问,得到一下画面,即代表项目和elementplus库创建成功:
接下来我们安装tailwindcss官网,这是一个css样式库,他可以很方便的让我们在template里面书写样式,大家可以在官网学习一下使用,非常简单方便。
注意:我使用的是的是tailwindcss x3版本 ,最新版本是 x4版本,大家可以自己选择版本。
安装依赖:
npm install -D tailwindcss@3 postcss autoprefixer
初始化文件:会生成两个配置文件。
px tailwindcss init -p
在tailwind.config.js里配置
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
在style.css里面配置
@tailwind base;
@tailwind components;
@tailwind utilities;
最后在App.vue 使用tailwindcss
最终得到页面:
上节课我们没有使用状态工具,所以我们的数据都是放在最顶层的父级组件里面,我们组件之间通信就比较麻烦,现在我们使用最新的pinia状态管理库,这也是官方最推荐的库。
npm install pinia
在main.ts添加依赖
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "./style.css";
import "element-plus/dist/index.css";
import App from "./App.vue";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import { createPinia } from "pinia";
const pinia = createPinia();
const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(ElementPlus);
app.use(pinia);
app.mount("#app");
现在让我们开始快速学习使用pinia:
首先使用defineStore定义一个counter store,我使用的是函数式的写法,大家也可以使用之前vue常用的选项写法,count就是全局的一个状态,你可以定义任意类型,可以在任何地方对count进行访问,对count进行增删改查我建议大家在方法中进行操作(可以理解为action)。
在App.vue进行测试:
<script setup lang="ts">
import { useCounterStore } from "./store/useCartStore";
const store = useCounterStore();
</script>
<template>
<div>
<h1>App</h1>
<p>counter: {{ store.count }}</p>
<button @click="store.increment()">Add</button>
</div>
</template>
<style scoped></style>
效果图展示:
注意:请不要这样使用store。
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useCounterStore } from "./store/useCartStore";
const store = useCounterStore();
const { count } = useCounterStore(); // 这样解构是不生效的
const { count: refCount } = storeToRefs(store); // 这样解构是生效的
</script>
<template>
<div>
<h1>App</h1>
<p>counter: {{ store.count }}</p>
<p>count: {{ count }}</p>
<p>refCount: {{ refCount }}</p>
<button @click="store.increment()">Add</button>
</div>
</template>
<style scoped></style>
效果图展示:
测试完成:让我们开始购物车项目!
二:功能实现
我们测试时候使用了pinia的函数式写法,所以我们正式项目就使用选项式写法,大家可以根据自己的想法来实现功能!
首先实现商品类型的定义:
// 商品类型
interface Product {
id: number; // 商品ID·
name: string; // 商品名称
price: number; // 商品价格
originalPrice: number; // 商品原价
image: string; // 商品图片
colors: string[]; // 商品颜色
sizes: string[]; // 商品尺码
description: string; // 商品描述
}
// 购物车项类型
interface CartItem {
id: string; // 唯一ID
product: Product; // 商品信息
selectedSize: string; // 选择的尺码
selectedColor: string; // 选择的颜色
quantity: number; // 数量
totalPrice: number; // 小计
}
接下来定义一个购物车store,让我们来思考一下如何设计一个购物车的store,首先,我们需要的是一个全局状态的state,然后对这个state进行增删改查,对这个state进行维护,比如当我们对state新增的时候需要考虑的是,当商品是否已经存在在购物车中?那么我们是否是新增商品还是对商品的数量进行新增,减少商品同理。
代码如下:
export const useCartStore = defineStore("cart", {
state: () => ({
items: [] as CartItem[],
}),
getters: {},
actions: {
// 添加商品到购物车
addToCart(product: Product, selectedSize: string, selectedColor: string) {
// 生成唯一ID
const itemId = `${product.id}-${selectedSize}-${selectedColor}`;
// 检查商品是否已在购物车中
const existingItem = this.items.find((item) => item.id === itemId);
if (existingItem) {
// 如果已存在,增加数量
existingItem.quantity++;
existingItem.totalPrice =
existingItem.product.price * existingItem.quantity;
} else {
// 如果不存在,添加新商品
const newItem: CartItem = {
id: itemId,
product,
selectedSize,
selectedColor,
quantity: 1,
totalPrice: product.price,
};
this.items.push(newItem);
}
// 显示添加成功提示
ElMessage({
type: "success",
message: "商品已添加到购物车",
});
},
// 从购物车移除商品
removeFromCart(itemId: string) {
this.items = this.items.filter((item) => item.id !== itemId);
},
// 更新购物车商品数量
updateQuantity(itemId: string, quantity: number) {
const item = this.items.find((item) => item.id === itemId);
if (item) {
item.quantity = quantity;
item.totalPrice = item.product.price * quantity;
}
},
// 清空购物车
clearCart() {
this.items = [];
},
},
});
现在我们来写页面。
来到App.vue,实现基础的商品列表布局,样式和数据不一步步带大家写了,我们着重完成功能!
<template>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4 mt-20">商品列表</h1>
<el-row :gutter="20">
<el-col
v-for="product in products"
:key="product.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
class="mb-4"
>
<div
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
>
<img
:src="product.image"
:alt="product.name"
class="w-full h-48 object-cover"
/>
<div class="p-4">
<h3 class="text-lg font-semibold mb-2">{{ product.name }}</h3>
<p class="text-gray-600 text-sm mb-2">{{ product.description }}</p>
<div class="flex items-center mb-2">
<span class="text-red-500 font-bold text-lg"
>¥{{ product.price }}</span
>
<span class="text-gray-400 line-through ml-2 text-sm"
>¥{{ product.originalPrice }}</span
>
</div>
<el-button type="primary" class="w-full"> 加入购物车 </el-button>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const products = ref([
{
id: 1,
name: "商品一",
price: 1299,
originalPrice: 1499,
image: "https://picsum.photos/id/1/400/400",
colors: ["black", "white", "red", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 2,
name: "商品二",
price: 1399,
originalPrice: 1599,
image: "https://picsum.photos/id/2/400/400",
colors: ["black", "white", "gray", "pink"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 3,
name: "商品三",
price: 599,
originalPrice: 699,
image: "https://picsum.photos/id/3/400/400",
colors: ["black", "white", "blue", "green"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 4,
name: "商品四",
price: 899,
originalPrice: 999,
image: "https://picsum.photos/id/4/400/400",
colors: ["black", "gray", "navy", "olive"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 5,
name: "商品五",
price: 399,
originalPrice: 499,
image: "https://picsum.photos/id/5/400/400",
colors: ["black", "gray", "blue", "pink"],
sizes: ["S", "M", "L", "XL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 6,
name: "商品六",
price: 1199,
originalPrice: 1399,
image: "https://picsum.photos/id/6/400/400",
colors: ["black", "navy", "red", "gray"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 7,
name: "商品七",
price: 1099,
originalPrice: 1299,
image: "https://picsum.photos/id/7/400/400",
colors: ["white", "black", "pink", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 8,
name: "商品八",
price: 1199,
originalPrice: 1399,
image: "https://picsum.photos/id/8/400/400",
colors: ["black", "white", "red", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
]);
</script>
<style scoped></style>
现在的效果是:
现在我们来实现购物车列表,我们现在在右上角新增一个按钮,使用弹窗的方式进行购物车列表的展示,大家可以用新页面展示也行,根据自己的想法来。
App.vue完整代码如下:
<template>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4 mt-20">商品列表</h1>
<!-- 购物车按钮 -->
<el-button
type="primary"
class="fixed right-4 bottom-4 z-50 shadow-lg"
@click="showCart = true"
>
<el-icon :size="20"><ShoppingCart /></el-icon>
<span class="ml-2">10</span>
</el-button>
<!-- 购物车弹窗 -->
<el-dialog v-model="showCart" title="购物车" width="80%">
<cart-list />
<template #footer>
<el-button>继续购物</el-button>
<el-button type="primary">去结算</el-button>
</template>
</el-dialog>
<el-row :gutter="20">
<el-col
v-for="product in products"
:key="product.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
class="mb-4"
>
<div
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
>
<img
:src="product.image"
:alt="product.name"
class="w-full h-48 object-cover"
/>
<div class="p-4">
<h3 class="text-lg font-semibold mb-2">{{ product.name }}</h3>
<p class="text-gray-600 text-sm mb-2">{{ product.description }}</p>
<div class="flex items-center mb-2">
<span class="text-red-500 font-bold text-lg"
>¥{{ product.price }}</span
>
<span class="text-gray-400 line-through ml-2 text-sm"
>¥{{ product.originalPrice }}</span
>
</div>
<el-button type="primary" class="w-full"> 加入购物车 </el-button>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import CartList from "./components/CartList.vue";
const showCart = ref(false);
const products = ref([
{
id: 1,
name: "商品一",
price: 1299,
originalPrice: 1499,
image: "https://picsum.photos/id/1/400/400",
colors: ["black", "white", "red", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 2,
name: "商品二",
price: 1399,
originalPrice: 1599,
image: "https://picsum.photos/id/2/400/400",
colors: ["black", "white", "gray", "pink"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 3,
name: "商品三",
price: 599,
originalPrice: 699,
image: "https://picsum.photos/id/3/400/400",
colors: ["black", "white", "blue", "green"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 4,
name: "商品四",
price: 899,
originalPrice: 999,
image: "https://picsum.photos/id/4/400/400",
colors: ["black", "gray", "navy", "olive"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 5,
name: "商品五",
price: 399,
originalPrice: 499,
image: "https://picsum.photos/id/5/400/400",
colors: ["black", "gray", "blue", "pink"],
sizes: ["S", "M", "L", "XL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 6,
name: "商品六",
price: 1199,
originalPrice: 1399,
image: "https://picsum.photos/id/6/400/400",
colors: ["black", "navy", "red", "gray"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 7,
name: "商品七",
price: 1099,
originalPrice: 1299,
image: "https://picsum.photos/id/7/400/400",
colors: ["white", "black", "pink", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 8,
name: "商品八",
price: 1199,
originalPrice: 1399,
image: "https://picsum.photos/id/8/400/400",
colors: ["black", "white", "red", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
]);
</script>
<style scoped>
.fixed {
position: fixed;
top: 4rem;
right: 6rem;
}
</style>
Cartlist.vue代码如下:
<template>
<div>
<div v-if="cartItems.length === 0" class="text-center text-gray-500">
购物车是空的
</div>
<div v-else class="space-y-4">
<div
v-for="(item, index) in cartItems"
:key="index"
class="flex items-center border-b pb-4"
>
<img
:src="item.product.image"
class="w-16 h-16 object-cover rounded mr-4"
/>
<div class="flex-1">
<h3 class="font-semibold">{{ item.product.name }}</h3>
<p class="text-sm text-gray-500">
¥{{ item.product.price }} x {{ item.quantity }}
</p>
</div>
<div class="flex items-center gap-4">
<el-input-number
v-model="item.quantity"
:min="1"
size="small"
class="w-24"
/>
<el-button type="danger" size="small"> 删除 </el-button>
</div>
</div>
<div class="text-xl font-bold text-right">总价: ¥ 100</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { CartItem } from "../store/useCartStore";
const cartItems = ref<CartItem[]>([]);
</script>
<style scoped></style>
现在页面的效果是:
现在我们来实现加入购物车功能:
首先我们抽离商品列表组件:
在productList.vue组件里面进行购物车添加操作:
完整的productList.vue代码如下:
<template>
<el-row :gutter="20">
<el-col
v-for="product in products"
:key="product.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
class="mb-4"
>
<div
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
>
<img
:src="product.image"
:alt="product.name"
class="w-full h-48 object-cover"
/>
<div class="p-4">
<h3 class="text-lg font-semibold mb-2">{{ product.name }}</h3>
<p class="text-gray-600 text-sm mb-2">{{ product.description }}</p>
<div class="flex items-center mb-2">
<span class="text-red-500 font-bold text-lg"
>¥{{ product.price }}</span
>
<span class="text-gray-400 line-through ml-2 text-sm"
>¥{{ product.originalPrice }}</span
>
</div>
<el-button
type="primary"
class="w-full"
@click="() => handleAdd(product)"
>
加入购物车
</el-button>
</div>
</div>
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useCartStore } from "../store/useCartStore";
const store = useCartStore(); // 使用购物车 store
const handleAdd = (product: any) => {
store.addToCart(product, "s", "red"); // 调用 store 中的 addToCart 方法,颜色和size可以根据需要修改,我这里就不做这个功能了,请大家自己拓展
};
const products = ref([
{
id: 1,
name: "商品一",
price: 1299,
originalPrice: 1499,
image: "https://picsum.photos/id/1/400/400",
colors: ["black", "white", "red", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 2,
name: "商品二",
price: 1399,
originalPrice: 1599,
image: "https://picsum.photos/id/2/400/400",
colors: ["black", "white", "gray", "pink"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 3,
name: "商品三",
price: 599,
originalPrice: 699,
image: "https://picsum.photos/id/3/400/400",
colors: ["black", "white", "blue", "green"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 4,
name: "商品四",
price: 899,
originalPrice: 999,
image: "https://picsum.photos/id/4/400/400",
colors: ["black", "gray", "navy", "olive"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 5,
name: "商品五",
price: 399,
originalPrice: 499,
image: "https://picsum.photos/id/5/400/400",
colors: ["black", "gray", "blue", "pink"],
sizes: ["S", "M", "L", "XL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 6,
name: "商品六",
price: 1199,
originalPrice: 1399,
image: "https://picsum.photos/id/6/400/400",
colors: ["black", "navy", "red", "gray"],
sizes: ["S", "M", "L", "XL", "XXL"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 7,
name: "商品七",
price: 1099,
originalPrice: 1299,
image: "https://picsum.photos/id/7/400/400",
colors: ["white", "black", "pink", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
{
id: 8,
name: "商品八",
price: 1199,
originalPrice: 1399,
image: "https://picsum.photos/id/8/400/400",
colors: ["black", "white", "red", "blue"],
sizes: ["38", "39", "40", "41", "42", "43", "44"],
description:
"商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述商品描述。",
},
]);
</script>
<style scoped></style>
CartList.vue购物车组件获取响应式数据进行列表展示
<template>
<div>
<div v-if="items.length === 0" class="text-center text-gray-500">
购物车是空的
</div>
<div v-else class="space-y-4">
<div
v-for="(item, index) in items"
:key="index"
class="flex items-center border-b pb-4"
>
<img
:src="item.product.image"
class="w-16 h-16 object-cover rounded mr-4"
/>
<div class="flex-1">
<h3 class="font-semibold">{{ item.product.name }}</h3>
<p class="text-sm text-gray-500">
¥{{ item.product.price }} x {{ item.quantity }}
</p>
</div>
<div class="flex items-center gap-4">
<el-input-number
v-model="item.quantity"
:min="1"
size="small"
class="w-24"
/>
<el-button type="danger" size="small"> 删除 </el-button>
</div>
</div>
<div class="text-xl font-bold text-right">总价: ¥ 100</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useCartStore } from "../store/useCartStore";
import { storeToRefs } from "pinia";
const store = useCartStore(); // 使用购物车 store
const { items } = storeToRefs(store); // 解构出购物车中的数据
</script>
<style scoped></style>
最终得到的效果是:
现在我们来实现后一个功能,购物车数量和总价,使用计算属性即可:
getters: {
totalQuantity(state) {
// 计算总数量
return state.items.reduce((total, item) => total + item.quantity, 0);
},
totalPrice(state) {
// 计算总价
return state.items.reduce(
(total, item) => total + item.product.price * item.quantity,
0
);
},
},
总价:
总数量:
现在我们完成最后一个删除功能:
删除购物车商品:
<template>
<div>
<div v-if="items.length === 0" class="text-center text-gray-500">
购物车是空的
</div>
<div v-else class="space-y-4">
<div
v-for="(item, index) in items"
:key="index"
class="flex items-center border-b pb-4"
>
<img
:src="item.product.image"
class="w-16 h-16 object-cover rounded mr-4"
/>
<div class="flex-1">
<h3 class="font-semibold">{{ item.product.name }}</h3>
<p class="text-sm text-gray-500">
¥{{ item.product.price }} x {{ item.quantity }}
</p>
</div>
<div class="flex items-center gap-4">
<el-input-number
v-model="item.quantity"
:min="1"
size="small"
class="w-24"
/>
<el-button
type="danger"
size="small"
@click="() => handleDelete(item)"
>
删除
</el-button>
</div>
</div>
<div class="text-xl font-bold text-right">
总价: ¥ {{ store.totalPrice }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useCartStore, type CartItem } from "../store/useCartStore";
import { storeToRefs } from "pinia";
const store = useCartStore(); // 使用购物车 store
const { items } = storeToRefs(store); // 解构出购物车中的数据
const handleDelete = (item: CartItem) => {
store.removeFromCart(item.id); // 调用 store 中的 removeFromCart 方法
};
</script>
<style scoped></style>
最终的效果展示:
总结:我们学会使用了状态管理工具:pinia,以及tailwindcss的使用,是不是特别方便呢?请大家认真练习,觉得有用请关注我,后续我会持续推出小练习系列,谢谢大家!