Browse Source

feat(购物车): 实现购物车页面样式和功能逻辑

添加购物车页面SCSS样式文件
实现购物车列表展示、商品选择、数量增减功能
添加购物车小球动画效果
完成全选、价格计算和结算功能
处理购物车与地址的关联逻辑
master
wei 4 days ago
parent
commit
0da3678502
  1. 328
      pages/shoppingCart/shoppingCart.scss
  2. 548
      pages/shoppingCart/shoppingCart.vue

328
pages/shoppingCart/shoppingCart.scss

@ -0,0 +1,328 @@
.address {
padding: 17rpx 0 33rpx 0;
margin: 0 29rpx 0 33rpx;
background: #FFFFFF;
display: flex;
align-items: center;
.left {
display: flex;
align-items: center;
flex: 1;
margin-right: 15rpx;
.name {
margin-left: 15rpx;
font-weight: bold;
font-size: $text-lg;
color: #333333;
}
}
}
.third {
margin: 20rpx 14rpx 0rpx 0;
background: #FFFFFF;
box-shadow: 0rpx 3rpx 14rpx 0rpx rgba(54, 77, 65, 0.1);
border-radius: 14rpx;
padding: 35rpx 22rpx 0rpx 22rpx;
box-sizing: border-box;
.top-box {
padding-bottom: 150rpx;
overflow: hidden;
.cart-list-touch-move-active {
-webkit-transform: translateX(-120rpx);
transform: translateX(-120rpx);
}
.cart-list {
display: flex;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1px solid #E4E6E3;
margin-bottom: 20rpx;
transition: .3s all;
position: relative;
.cart-list-item {
display: flex;
align-items: center;
position: relative;
flex: 1;
.of-stock {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #FFFFFF;
opacity: 0.6;
z-index: 999;
}
.image-box {
width: 174rpx;
height: 174rpx;
margin-left: 20rpx;
image {
width: 100%;
height: 100%;
}
}
.of-stock-text {
position: absolute;
left: 50%;
transform: translateX(-50%) translateY(-50%);
top: 50%;
font-weight: 400;
font-size: $text-lg;
color: #FFFFFF;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 60rpx;
padding: 16rpx 24rpx;
width: 40%;
text-align: center;
z-index: 999;
}
.info {
height: 100%;
margin-left: 22rpx;
display: flex;
flex-direction: column;
flex: 1;
.name {
font-weight: bold;
font-size: 29rpx;
color: #333333;
}
.under {
margin-top: 42rpx;
display: flex;
align-items: flex-end;
.price-box {
display: flex;
align-items: flex-end;
line-height: 1;
flex: 1;
flex-wrap: wrap;
.price {
font-weight: bold;
font-size: 26rpx;
color: #FF261F;
text {
font-size: $text-5xl;
}
}
.no-original {
font-size: 22rpx;
color: #959595;
margin-left: 9rpx;
display: flex;
align-items: center;
}
}
.choose-box {
display: flex;
align-items: center;
position: relative;
z-index: 9;
.line-one {
position: absolute;
right: 0;
top: 0;
width: 88%;
height: 1px;
background-color: #D6D6D6;
z-index: -1;
}
.line-two {
position: absolute;
left: 0;
bottom: 0;
width: 88%;
height: 1px;
background-color: #D6D6D6;
z-index: -1;
}
.minus {
width: 58rpx;
height: 58rpx;
background: #06CA64;
border-radius: 21rpx 0rpx 21rpx 0rpx;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 28rpx;
height: 4rpx;
}
}
.input {
width: 67rpx;
height: 58rpx;
line-height: 58rpx;
text-align: center;
font-size: $text-xl;
color: #333333;
margin: 0 10rpx;
}
.add {
width: 58rpx;
height: 58rpx;
background: #06CA64;
border-radius: 21rpx 0rpx 21rpx 0rpx;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 28rpx;
height: 28rpx;
}
}
}
}
.total {
font-weight: 400;
font-size: $text-sm;
color: #959595;
margin-top: 20rpx;
display: flex;
align-items: flex-end;
justify-content: flex-end;
.text {
font-size: $text-lg;
color: #FF261F;
}
}
}
}
.delete {
position: absolute;
right: -130rpx;
top: 0;
width: 120rpx;
height: 100%;
background-color: #FF261F;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: $text-4xl;
}
}
}
.under-box {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 26rpx;
position: fixed;
left: 0;
width: 100%;
// bottom: 90rpx;
background: #fff;
z-index: 999;
/* padding-bottom: 20rpx; */
padding: 0 14rpx 20rpx;
box-sizing: border-box;
.under-box-left {
display: flex;
align-items: center;
.radio-right {
margin-left: 15rpx;
display: flex;
flex-direction: column;
margin-top: 15rpx;
.text {
font-weight: bold;
font-size: $text-lg;
color: #545454;
}
.total-goods {
display: flex;
align-items: center;
.num {
font-weight: 500;
font-size: $text-sm;
color: #FF261F;
}
.name {
font-weight: 500;
font-size: $text-sm;
color: #999999;
}
}
}
}
.under-box-right {
display: flex;
align-items: center;
.one {
display: flex;
flex-direction: column;
line-height: 1;
align-items: flex-end;
.price-box {
display: flex;
align-items: center;
.text {
font-weight: bold;
font-size: $text-lg;
color: #333333;
}
.price {
font-weight: bold;
font-size: $text-lg;
color: #F23A3A;
text {
font-size: $text-5xl;
}
}
}
.tips {
font-weight: bold;
font-size: $text-sm;
color: #999999;
margin-top: 15rpx;
}
}
.two {
margin-left: 27rpx;
background: #06CA64;
border-radius: 60rpx;
font-weight: bold;
font-size: $text-2xl;
color: #FFFFFF;
padding: 20rpx 32rpx;
text-align: center;
line-height: 1;
}
}
.edit-box {
.btn {
margin-left: 27rpx;
border: 1px solid #06CA64;
border-radius: 60rpx;
font-weight: bold;
font-size: $text-2xl;
color: #06CA64;
padding: 20rpx 32rpx;
text-align: center;
line-height: 1;
}
}
}
}

548
pages/shoppingCart/shoppingCart.vue

@ -4,7 +4,18 @@ import { ref } from "vue";
import customTabBar from "@/components/custom-tab-bar/my-tab-bar.vue";
import navv from "@/components/nav/nav.vue";
import topTitle from "@/components/topTitle/topTitle.vue";
import { getMyAreaApi, getUserInfoApi } from "@/libs/api";
import {
addCartApi,
editDefaultAddressApi,
getCartInfoApi,
getMyAreaApi,
getUserInfoApi,
previewApi,
} from "@/libs/api";
import { sleep } from "@/libs/utils";
import useStore from "@/store";
const store = useStore();
/**
* 选择收货地址
@ -18,16 +29,277 @@ const finished = ref(false);
* 购物车列表
*/
const cartLists = ref([]);
/**
* 服务费用
*/
const serviceFee = ref(0);
/**
* 配送费
*/
const shippingFee = ref(0);
/**
* 购物车总价
*/
const totalPrice = ref(0);
/**
* 是否生成订单
*/
const genOrder = ref(false);
/**
* 选中的购物车项
*/
const checkedItems = ref([]);
/**
* 选中所有购物车项
*/
const checkedAll = ref(false);
/**
* 购物车项数量
*/
const cartCount = ref(0);
/**
* 加载中
*/
// const isLoading = ref(false);
/**
* 是否显示购物车小球
*/
const showBall = ref(false);
/**
* 购物车小球的动画
*/
const actionTime = ref(true);
/**
* 购物车小球的动画
*/
const animationY = ref({});
/**
* 购物车小球的X轴动画
*/
const animationX = ref({});
/**
* 购物车小球的Y轴坐标
*/
const testY = ref(0);
/**
* 购物车小球的X轴坐标
*/
const testX = ref(0);
/**
* 触摸开始时的 X 坐标
*/
const startX = ref(0);
/**
* 触摸开始时的 Y 坐标
*/
const startY = ref(0);
/**
* 底部导航栏高度
*/
// const tabbarHeight = ref(0)
/**
* 点击选择收货地址
*/
function onGoSelectAddress() { }
/**
* 点击管理购物车
*/
function onManageCart() { }
/**
* 计算滑动角度
* @param {object} start 起点坐标
* @param {object} end 终点坐标
*/
function angle(start, end) {
// /Math.atan()
const _X = end.X - start.X;
const _Y = end.Y - start.Y;
return 360 * Math.atan(_Y / _X) / (2 * Math.PI);
}
/**
* 购物车项触摸开始事件
* @param e 触摸事件对象
*/
function onTouchStart(e) {
cartLists.value = cartLists.value.map(item => ({ ...item, isTouchMove: false }));
startX.value = e.changedTouches[0].clientX;
startY.value = e.changedTouches[0].clientY;
}
/**
* 购物车项触摸移动事件
* @param e 触摸事件对象
* @param index 购物车项索引
*/
function onTouchMove(e, index) {
const touchMoveX = e.changedTouches[0].clientX; //
const touchMoveY = e.changedTouches[0].clientY; //
const angleVal = angle({ X: startX.value, Y: startY.value }, { X: touchMoveX, Y: touchMoveY });
if (Math.abs(angleVal) > 30) {
return;
}
//
const isLeft = touchMoveX <= startX.value;
cartLists.value[index].isTouchMove = isLeft;
}
/**
* 购物车项选择事件
* @param index 购物车项索引
*/
async function onCheckboxChange(index) {
cartLists.value[index].checked = !cartLists.value[index].checked;
store.changeCartList(cartLists.value.filter(item => item.checked));
checkedItems.value = store.cartList.map(item => item.id);
await preview(checkedItems.value);
inspectCheck();
}
function onShowKeyboard() {}
/**
* 购物车项增加事件
* @param e 事件对象
* @param item 购物车项
// * @param index
*/
function onAdd(e, item) {
if (!actionTime.value) {
return;
}
actionTime.value = false;
testX.value = e.detail.x;
testY.value = e.detail.y;
createAnimation(e.detail.x, e.detail.y);
// const isPass = validates([
// () => item.stock == item.sum && "",
// () => item.stock == 0 && "0",
// ]);
// if (!isPass) {
// return;
// }
// 0,
item.quantity = Number(item.quantity) || 0;
// (item.minNum), +1
const diff = item.quantity === 0 ? (item.minNum || 1) : 1;
item.quantity += diff;
toCart(item, diff);
}
/**
* 创建购物车小球的动画
*/
async function createAnimation(eX, eY) {
const bottomX = (store.tabbarItemWidth || 83) * 2 - 30;
const bottomY = uni.getWindowInfo().windowHeight;
const stepX = flyX(bottomX, eX);
const stepY = flyY(bottomY, eY);
showBall.value = true;
animationX.value = stepX.export(); //
animationY.value = stepY.export(); //
await sleep(400);
actionTime.value = true;
showBall.value = false;
animationX.value = flyX(0, 0).export();
animationY.value = flyY(0, 0).export();
// await sleep(800);
}
/**
* 购物车小球水平移动动画
*/
function flyX(bottomX, ballX, duration = 400) {
const animation = uni.createAnimation({
duration,
timingFunction: "linear",
});
animation.translateX(-bottomX).step();
return animation;
}
/**
* 购物车小球垂直移动动画
*/
function flyY(bottomY, ballY, duration = 400) {
const animation = uni.createAnimation({
duration,
timingFunction: "ease-in",
});
animation.translateY(bottomY - ballY).step();
return animation;
}
/**
* 购物车项减少事件
* @param e 事件对象
* @param item 购物车项
// * @param index
*/
function onMinus(e, item) {
// 0,
item.sum = Number(item.sum) || 0;
// (item.minNum), -1
const minNum = item.minNum || 1;
const diff = -(item.sum === minNum ? minNum : 1);
item.sum += diff;
toCart(item, diff);
}
function onDelete() {}
function onSelectAllGoods() {}
function onGotoOrder() {}
function onDeleteCart() {}
/**
* 更新购物车
* @param item 商品项
* @param diff 变化量
*/
async function toCart(item, diff) {
const data = {
quantity: diff,
specId: item.specId,
// Chuxiao: item.chuxiao,
isChuxiao: false,
warehouseId: uni.getStorageSync("warehousId"),
addrId: uni.getStorageSync("addressId"),
isUpdate: 0,
// isChuxiao: item.chuxiao,
};
if (!data.specId) {
uni.showModal({
title: "提示",
content: "当前商品规格错误,请稍候再试",
showCancel: false,
confirmText: "确定",
});
}
if (!data.warehouseId || !data.addrId) {
uni.showModal({
title: "提示",
content: "请先选择收货地址,再添加商品",
showCancel: false,
confirmText: "确定",
});
}
// keys.push(item.id);
const res = await addCartApi(data);
// keys.splice(item.id, 1);
if (res.code !== "0") {
return;
}
initCartInfo();
}
/**
* 获取仓库列表数据
@ -47,7 +319,6 @@ async function getMyArea() {
}
if (!res.data.list.length) {
uni.showModal({
// title: '',
content: "请先添加收货地址",
confirmColor: "#3aa24b",
confirmText: "去添加",
@ -66,6 +337,38 @@ async function getMyArea() {
selectAddress.value = res.data.list.find(each => each.isLastDefaultAddr) || res.data.list[0];
uni.setStorageSync("warehousId", selectAddress.value.warehousId);
uni.setStorageSync("addressId", selectAddress.value.addrId);
if (selectAddress.value.addrId) {
editDefaultAddress();
}
else {
uni.showModal({
content: "请先添加收货地址",
confirmColor: "#3aa24b",
confirmText: "去添加",
success(res) {
if (res.confirm) {
uni.navigateTo({
// url: "../../address/pages/addressLists/addressLists",
});
}
},
});
}
// selectData.value = res.data.list;
initCartInfo();
}
async function editDefaultAddress() {
const res = await editDefaultAddressApi({
addrId: uni.getStorageSync("addressId"),
});
if (res.code !== "0") {
uni.showToast({
title: res.message,
duration: 3000,
});
}
}
/**
@ -105,6 +408,76 @@ async function getUserInfo() {
}
}
/**
* 初始化购物车列表
*/
async function initCartInfo() {
checkedItems.value = [];
// isLoading.value = true;
const res = await getCartInfoApi({
warehouseId: uni.getStorageSync("warehousId"),
addrId: uni.getStorageSync("addressId"),
});
if (res.code === "0") {
checkedItems.value = res.data.map(each => each.id);
//
store.changeCartList(res.data);
// isLoading.value = false;
cartLists.value = res.data.map(each => ({
...each,
checked: true,
isTouchMove: false,
}));
totalPrice.value = res.data.reduce((acc, cur) => acc + cur.amount, 0);
if (checkedItems.value.length) {
await preview(checkedItems.value);
}
else {
checkedItems.value = [];
genOrder.value = false;
serviceFee.value = 0;
shippingFee.value = 0;
totalPrice.value = 0;
}
// console.log(cartLists.value, 3333);
inspectCheck();
}
else if (res.code === "4") {
uni.navigateTo({
url: "/pages/login/login",
});
}
}
/**
* 预览订单 统计价格情况
* @param itemIds 购物车项id列表
*/
async function preview(itemIds) {
const res = await previewApi({
itemIds: itemIds.join(","),
addrId: uni.getStorageSync("addressId"),
});
if (res.code === "0") {
serviceFee.value = res.data.serviceFee;
shippingFee.value = res.data.shippingFee;
totalPrice.value = res.data.itemAmount;
}
else if (res.code === "1") {
genOrder.value = false;
}
}
/**
* 检查是否全选
*/
function inspectCheck() {
checkedAll.value = cartLists.value.every(each => each.checked);
cartCount.value = cartLists.value.filter(each => each.checked).length;
}
onShow(() => {
finished.value = true;
// isOne.value = true;
@ -123,6 +496,7 @@ onLoad(() => {
<template>
<navv>
<template #default="{ content, fixStyle }">
<!-- 购物车标题 -->
<topTitle
title="购物车"
:style="{ ...fixStyle, backgroundColor: '#fff', position: 'static' }"
@ -143,8 +517,153 @@ onLoad(() => {
</view>
<!-- 列表 -->
<view :style="{ 'height': `${content}px`, 'overflow-y': 'auto' }" />
<view
v-if="cartLists.length > 0"
class="third"
:style="{ 'height': `${content - store.tabbarBottomHeight}px`, 'overflow-y': 'auto' }"
>
<!-- 购物车列表 -->
<view class="top-box" :style="{ paddingBottom: `${store.tabbarBottomHeight}px` }">
<view
v-for="(item, index) in cartLists"
:key="item.id"
class="cart-list"
:class="[item.isTouchMove ? 'cart-list-touch-move-active' : '']"
@touchstart="(e) => onTouchStart(e)"
@touchmove="(e) => onTouchMove(e, index)"
>
<radio
:value="item.id"
:checked="item.checked"
color="#06CA64"
@click="() => onCheckboxChange(index)"
/>
<view
class="cart-list-item"
>
<!-- style="display: flex;align-items: center;position: relative;flex: 1;" -->
<view v-if="item.type == 2 || item.type == 3" class="of-stock" />
<view v-if="item.type == 2" class="of-stock-text">
下架
</view>
<view v-if="item.type == 3" class="of-stock-text">
补货中
</view>
<view class="image-box">
<image :src="item.productImage" mode="" />
</view>
<view class="info">
<view class="name">
{{ item.productName }}
</view>
<view class="under">
<view class="price-box">
<view class="price">
<text>{{ item.price }}</text>
</view>
<text class="no-original">
/{{ item.unit }}
</text>
</view>
<view v-if="item.quantity !== 0" class="choose-box">
<block v-if="item.quantity !== 0">
<view class="line-one" />
<view class="line-two" />
<view class="minus" @click.stop="(e) => onMinus(e, item, index)">
<image class="icon" src="/static/home/minus.png" mode="" />
</view>
<text
class="input"
@click="onShowKeyboard"
>
{{ item.quantity }}
</text>
</block>
<view class="add" @click.stop="(e) => onAdd(e, item, index)">
<image class="icon" src="/static/home/add.png" mode="" />
</view>
</view>
</view>
<view class="total">
合计<text class="text">
{{ item.amount }}
</text>
</view>
</view>
</view>
<view class="delete" @click="onDelete">
删除
</view>
</view>
</view>
<!-- 底部统计栏 -->
<view
class="under-box" :style="{
bottom: `${store.tabbarBottomHeight}px`,
}"
>
<view
class="under-box-left"
@click="onSelectAllGoods"
>
<radio :checked="checkedAll" color="#06CA64" />
<view class="radio-right">
<text class="text">
全选
</text>
<view class="total-goods">
<text class="num">
{{ cartCount }}
</text>
<text class="name">
类商品
</text>
</view>
</view>
</view>
<view v-if="finished" class="under-box-right">
<view class="one">
<view class="price-box">
<text class="text">
合计
</text>
<view class="price">
<text>{{ totalPrice }}</text>
</view>
</view>
<text class="tips">
超时服务费{{ serviceFee }}服务费{{ shippingFee }}
</text>
</view>
<button class="two" :disabled="!genOrder" @click="onGotoOrder">
去结算({{ cartCount }})
</button>
</view>
<view v-else class="edit-box">
<view class="btn" @click="onDeleteCart">
删除所选
</view>
</view>
</view>
</view>
<!-- 购物车小球 -->
<view
v-if="showBall"
:animation="animationY"
:style="{ position: 'fixed', top: `${testY}px` }"
>
<view
class="round"
:animation="animationX"
:style="{ position: 'fixed', left: `${testX}px` }"
/>
</view>
<!-- 底部导航栏 -->
<customTabBar tab-index="3" />
</template>
</navv>
@ -154,24 +673,5 @@ onLoad(() => {
:deep(.left .icon-back) {
display: none;
}
.address {
padding: 17rpx 0 33rpx 0;
margin: 0 29rpx 0 33rpx;
background: #FFFFFF;
display: flex;
align-items: center;
.left {
display: flex;
align-items: center;
flex: 1;
margin-right: 15rpx;
.name {
margin-left: 15rpx;
font-weight: bold;
font-size: $text-lg;
color: #333333;
}
}
}
@import "./shoppingCart.scss";
</style>
Loading…
Cancel
Save