Files
yshop-drink/yshop-drink-uniapp-vue3/pages/cart/cart.vue
T
KroytzandCursor 9384c68662 feat(uniapp): 美化购物车页并展示所属门店
重构购物车页样式,展示门店与取餐方式;门店信息本地持久化,结算逻辑保持原行为。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 11:46:02 +08:00

540 lines
11 KiB
Vue

<template>
<view class="cart-page">
<uv-navbar
:fixed="false"
title="购物车"
bgColor="transparent"
leftIcon=""
/>
<!-- 门店信息 -->
<view class="cart-store" @tap="selectShop" v-if="store && store.id">
<image
v-if="store.image"
class="cart-store__img"
:src="store.image"
mode="aspectFill"
/>
<view v-else class="cart-store__img cart-store__img--placeholder">
<uv-icon name="home-fill" size="22" color="#F28C38" />
</view>
<view class="cart-store__main">
<view class="cart-store__name-row">
<text class="cart-store__name">{{ store.name || '未选择门店' }}</text>
<view class="cart-store__tag">{{ orderTypeLabel }}</view>
</view>
<view class="cart-store__sub">
{{ storeHint }}
</view>
</view>
<view class="cart-store__switch">
<text>切换</text>
<uv-icon name="arrow-right" size="12" color="#F28C38" />
</view>
</view>
<view class="cart-store cart-store--empty" @tap="selectShop" v-else>
<view class="cart-store__img cart-store__img--placeholder">
<uv-icon name="map-fill" size="22" color="#F28C38" />
</view>
<view class="cart-store__main">
<view class="cart-store__name">请先选择门店</view>
<view class="cart-store__sub">选择门店后即可点餐结算</view>
</view>
<view class="cart-store__switch">
<text>去选择</text>
<uv-icon name="arrow-right" size="12" color="#F28C38" />
</view>
</view>
<!-- 商品列表 -->
<view class="cart-panel">
<view class="cart-panel__header">
<text class="cart-panel__title">已点 {{ getCartGoodsNumber }} 件</text>
<text
v-if="cart.length"
class="cart-panel__clear"
@tap="handleCartClear"
>清空</text>
</view>
<scroll-view class="cart-panel__list" scroll-y>
<uv-empty v-if="!cart.length" mode="car" text="购物车还是空的" />
<view
v-for="(item, index) in cart"
:key="index"
class="cart-item"
>
<image
v-if="item.image"
class="cart-item__img"
:src="item.image"
mode="aspectFill"
/>
<view v-else class="cart-item__img cart-item__img--placeholder" />
<view class="cart-item__body">
<view class="cart-item__top">
<text class="cart-item__name">{{ item.name }}</text>
<text class="cart-item__price">¥{{ item.price }}</text>
</view>
<view class="cart-item__props" v-if="item.valueStr">{{ item.valueStr }}</view>
<view class="cart-item__controls">
<button
type="default"
plain
class="cart-item__btn"
hover-class="none"
@tap="handleCartItemReduce(index)"
>
<text class="iconfont iconsami-select"></text>
</button>
<text class="cart-item__qty">{{ item.number }}</text>
<button
type="primary"
class="cart-item__btn cart-item__btn--add"
hover-class="none"
@tap="handleCartItemAdd(index)"
>
<text class="iconfont iconadd-select"></text>
</button>
</view>
</view>
</view>
<view class="cart-panel__spacer" />
</scroll-view>
</view>
<!-- 底部结算栏 -->
<view class="cart-bar" v-if="cart.length">
<view class="cart-bar__info">
<view class="cart-bar__store" v-if="store && store.name">
{{ store.name }} · {{ orderTypeLabel }}
</view>
<view class="cart-bar__price-row">
<text class="cart-bar__label">合计</text>
<text class="cart-bar__yen">¥</text>
<text class="cart-bar__price">{{ getCartGoodsPrice }}</text>
</view>
</view>
<button class="cart-bar__btn" @tap="toPay">去结算</button>
</view>
<uv-toast ref="uToast" />
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useMainStore } from '@/store/store'
import { storeToRefs } from 'pinia'
import { onShow } from '@dcloudio/uni-app'
import { kmUnit } from '@/utils/util'
const main = useMainStore()
const { orderType, store, isLogin } = storeToRefs(main)
const cart = ref([])
const uToast = ref()
const orderTypeLabel = computed(() =>
orderType.value === 'takeout' ? '外卖' : '自取'
)
const storeHint = computed(() => {
if (!store.value || !store.value.id) return '点击选择门店'
if (orderType.value === 'takeout') {
if (store.value.distance > 0) {
return `配送距离约 ${store.value.distance}km`
}
return '本店外卖配送'
}
if (store.value.dis) {
return `距离您 ${kmUnit(store.value.dis)}`
}
return '到店自取,下单免排队'
})
onShow(() => {
cart.value = uni.getStorageSync('cart') || []
// 从点餐页切过来时 pinia 里通常有门店;冷启动进购物车则从本地恢复
if (!store.value?.id) {
const cachedStore = uni.getStorageSync('store')
if (cachedStore && cachedStore.id) {
main.SET_STORE(cachedStore)
}
}
})
const getCartGoodsNumber = computed(() => {
if (!cart.value.length) return 0
return cart.value.reduce((acc, cur) => acc + cur.number, 0)
})
const getCartGoodsPrice = computed(() => {
if (!cart.value.length) return '0.00'
const price = cart.value.reduce((acc, cur) => acc + cur.number * cur.price, 0)
return parseFloat(price).toFixed(2)
})
const selectShop = () => {
uni.navigateTo({
url: '/pages/components/pages/shop/shop'
})
}
const handleCartItemAdd = (index) => {
cart.value[index].number += 1
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
}
const handleCartItemReduce = (index) => {
if (cart.value[index].number === 1) {
cart.value.splice(index, 1)
} else {
cart.value[index].number -= 1
}
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
}
const handleCartClear = () => {
uni.showModal({
title: '提示',
content: '确定清空购物车么',
success: ({ confirm }) => {
if (confirm) {
cart.value = []
uni.setStorageSync('cart', [])
}
}
})
}
const toPay = () => {
if (!cart.value.length) {
uToast.value.show({ message: '请先去点餐哦', type: 'error' })
return
}
if (!isLogin.value) {
uni.navigateTo({ url: '/pages/components/pages/login/login' })
return
}
if (store.value?.status == 0) {
uToast.value.show({ message: '不在店铺营业时间内', type: 'error' })
return
}
if (orderType.value == 'takeout' && store.value?.distance < store.value?.far) {
uToast.value.show({ message: '选中的地址不在配送范围', type: 'error' })
return
}
uni.showLoading({ title: '加载中' })
uni.setStorageSync('cart', JSON.parse(JSON.stringify(cart.value)))
uni.navigateTo({ url: '/pages/components/pages/pay/pay' })
uni.hideLoading()
}
</script>
<style lang="scss" scoped>
$bg-top: #fff3e6;
$bg-bottom: #ffffff;
$accent: #f28c38;
$accent-deep: #ff6817;
$text-main: #2b2b2b;
$text-sub: #9a9590;
$card-radius: 24rpx;
.cart-page {
min-height: 100vh;
padding: 0 24rpx 200rpx;
box-sizing: border-box;
background: linear-gradient(180deg, $bg-top 0%, #fff8f0 40%, $bg-bottom 100%);
}
.cart-store {
display: flex;
align-items: center;
padding: 24rpx;
margin-bottom: 20rpx;
border-radius: $card-radius;
background: #fff;
box-shadow: 0 8rpx 24rpx rgba(180, 120, 60, 0.08);
&--empty {
border: 2rpx dashed #f0d9bd;
background: #fffaf4;
box-shadow: none;
}
&__img {
width: 88rpx;
height: 88rpx;
border-radius: 18rpx;
flex-shrink: 0;
margin-right: 20rpx;
background: #fff3e6;
&--placeholder {
display: flex;
align-items: center;
justify-content: center;
}
}
&__main {
flex: 1;
min-width: 0;
}
&__name-row {
display: flex;
align-items: center;
gap: 12rpx;
}
&__name {
font-size: 30rpx;
font-weight: 700;
color: $text-main;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 360rpx;
}
&__tag {
flex-shrink: 0;
padding: 4rpx 14rpx;
border-radius: 999rpx;
font-size: 20rpx;
color: #fff;
background: linear-gradient(90deg, #ff9b20, #ff6817);
}
&__sub {
margin-top: 8rpx;
font-size: 22rpx;
color: $text-sub;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__switch {
display: flex;
align-items: center;
gap: 4rpx;
flex-shrink: 0;
margin-left: 12rpx;
font-size: 24rpx;
color: $accent;
}
}
.cart-panel {
border-radius: $card-radius;
background: #fff;
box-shadow: 0 8rpx 24rpx rgba(180, 120, 60, 0.08);
overflow: hidden;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 28rpx 12rpx;
}
&__title {
font-size: 28rpx;
font-weight: 700;
color: $text-main;
}
&__clear {
font-size: 24rpx;
color: $text-sub;
}
&__list {
max-height: calc(100vh - 480rpx);
min-height: 320rpx;
}
&__spacer {
height: 24rpx;
}
}
.cart-item {
display: flex;
padding: 24rpx 28rpx;
border-bottom: 1rpx solid #f3ebe3;
&:last-child {
border-bottom: none;
}
&__img {
width: 128rpx;
height: 128rpx;
border-radius: 18rpx;
flex-shrink: 0;
margin-right: 20rpx;
background: #fff3e6;
&--placeholder {
background: linear-gradient(135deg, #fff3e6, #f5e6d4);
}
}
&__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
&__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
}
&__name {
flex: 1;
font-size: 28rpx;
font-weight: 700;
color: $text-main;
line-height: 1.35;
}
&__price {
flex-shrink: 0;
font-size: 30rpx;
font-weight: 700;
color: $accent-deep;
}
&__props {
margin-top: 8rpx;
font-size: 22rpx;
color: $text-sub;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__controls {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: auto;
padding-top: 12rpx;
}
&__btn {
width: 48rpx;
height: 48rpx;
margin: 0;
padding: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: $accent;
border: 2rpx solid $accent !important;
background: #fff;
line-height: 1;
&::after {
border: none;
}
&--add {
color: #fff;
background: $accent;
border-color: $accent !important;
}
}
&__qty {
min-width: 52rpx;
text-align: center;
font-size: 28rpx;
font-weight: 600;
color: $text-main;
}
}
.cart-bar {
position: fixed;
left: 24rpx;
right: 24rpx;
bottom: calc(24rpx + env(safe-area-inset-bottom));
/* #ifdef H5 */
bottom: calc(24rpx + var(--window-bottom, 0px));
/* #endif */
z-index: 100;
display: flex;
align-items: center;
height: 112rpx;
padding: 0 10rpx 0 32rpx;
border-radius: 56rpx;
background: #2f241c;
box-shadow: 0 12rpx 32rpx rgba(47, 36, 28, 0.28);
&__info {
flex: 1;
min-width: 0;
padding-right: 16rpx;
}
&__store {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.62);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__price-row {
display: flex;
align-items: baseline;
margin-top: 4rpx;
}
&__label {
margin-right: 10rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.75);
}
&__yen {
font-size: 24rpx;
font-weight: 700;
color: #fff;
}
&__price {
font-size: 40rpx;
font-weight: 700;
color: #fff;
line-height: 1;
}
&__btn {
height: 88rpx;
margin: 0;
padding: 0 48rpx;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: 700;
color: #fff;
background: linear-gradient(90deg, #ff9b20, #ff6817);
line-height: 88rpx;
&::after {
border: none;
}
}
}
</style>