campus-errand/miniapp/pages/food/food.vue
2026-03-17 18:51:49 +08:00

496 lines
10 KiB
Vue

<template>
<view class="food-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="nav-back" @click="goBack">
<image class="back-icon" src="/static/ic_back.png" mode="aspectFit"></image>
</view>
<text class="navbar-title">美食街</text>
<view class="nav-placeholder"></view>
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 顶部大图 -->
<view class="top-banner" v-if="bannerUrl">
<image class="banner-img" :src="bannerUrl" mode="aspectFill"></image>
</view>
<!-- 门店列表 -->
<view class="shop-list">
<view class="shop-card" v-for="shop in shops" :key="shop.id" @click="goShopDetail(shop.id)">
<image class="shop-photo" :src="shop.photo" mode="aspectFill"></image>
<view class="shop-info">
<text class="shop-name">{{ shop.name }}</text>
<view class="info-row">
<image class="info-icon" src="/static/ic_address.png" mode="aspectFit"></image>
<text class="info-text">{{ shop.location || '暂无位置信息' }}</text>
</view>
<view class="info-row">
<image class="info-icon" src="/static/ic_delicacy.png" mode="aspectFit"></image>
<text class="info-text">{{ shop.dishCount || 0 }}种美食</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && shops.length === 0" class="empty-state">
<text class="empty-text">暂无门店</text>
</view>
<!-- 底部购物车栏 -->
<view class="cart-bar">
<view class="cart-left" @click="cartTotalCount > 0 && (showCartPopup = true)">
<image class="cart-icon" src="/static/ic_courier.png" mode="aspectFit"></image>
<text class="cart-price">¥ {{ cartTotalCount > 0 ? cartTotalPrice : '0' }}</text>
</view>
<button class="cart-checkout-btn" :class="{ disabled: cartTotalCount === 0 }" @click="goCheckout">去结算</button>
</view>
<!-- 购物车弹窗 -->
<view class="cart-popup-mask" v-if="showCartPopup" @click="showCartPopup = false">
<view class="cart-popup" @click.stop>
<view class="cart-popup-header">
<text class="cart-popup-title">购物车</text>
<text class="cart-popup-clear" @click="clearCart">清空</text>
</view>
<scroll-view scroll-y class="cart-popup-list">
<view v-for="shopGroup in cartStore.shopList" :key="shopGroup.shopInfo.id" class="cart-shop-group">
<text class="cart-shop-name">{{ shopGroup.shopInfo.name }}</text>
<view class="cart-popup-item" v-for="item in shopGroup.items" :key="item.dish.id">
<image class="cart-item-photo" :src="item.dish.photo" mode="aspectFill"></image>
<view class="cart-item-info">
<text class="cart-item-name">{{ item.dish.name }}</text>
<text class="cart-item-price">¥{{ item.dish.price.toFixed(1) }}</text>
</view>
<view class="dish-actions">
<image class="action-icon" src="/static/ic_reduce.png" mode="aspectFit" @click="onPopupMinus(shopGroup.shopInfo.id, item.dish)"></image>
<text class="dish-quantity">{{ item.quantity }}</text>
<image class="action-icon" src="/static/ic_add.png" mode="aspectFit" @click="onPopupPlus(shopGroup.shopInfo.id, item.dish)"></image>
</view>
</view>
</view>
</scroll-view>
<view class="cart-popup-footer">
<view class="cart-fee-row" v-if="cartStore.packingFee > 0">
<text class="cart-fee-label">打包费</text>
<text class="cart-fee-value">¥{{ cartStore.packingFee.toFixed(2) }}</text>
</view>
<view class="cart-fee-row">
<text class="cart-fee-label">总价</text>
<text class="cart-fee-total">¥{{ cartStore.totalPriceWithFee.toFixed(2) }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getShops, getPageBanner } from '../../utils/api'
import { useCartStore } from '../../stores/cart'
export default {
data() {
return {
shops: [],
loading: true,
statusBarHeight: 0,
bannerUrl: '',
showCartPopup: false
}
},
computed: {
cartStore() {
return useCartStore()
},
cartTotalCount() {
return this.cartStore.totalCount
},
cartTotalPrice() {
return this.cartStore.totalPrice.toFixed(2)
}
},
onShow() {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
this.loadBanner()
this.loadShops()
},
methods: {
goBack() { uni.navigateBack() },
async loadBanner() {
try {
const res = await getPageBanner('food')
if (res?.value) this.bannerUrl = res.value
} catch (e) {}
},
async loadShops() {
this.loading = true
try {
const res = await getShops()
this.shops = res || []
} catch (e) {} finally { this.loading = false }
},
goShopDetail(id) {
uni.navigateTo({ url: `/pages/food/shop-detail?id=${id}` })
},
/** 去结算 */
goCheckout() {
if (this.cartTotalCount === 0) return
uni.navigateTo({ url: '/pages/food/food-order' })
},
/** 清空购物车 */
clearCart() {
this.cartStore.clearCart()
this.showCartPopup = false
},
/** 弹窗中减少指定门店的菜品 */
onPopupMinus(shopId, dish) {
const prevShopId = this.cartStore.currentShopId
this.cartStore.currentShopId = shopId
this.cartStore.removeItem(dish.id)
this.cartStore.currentShopId = prevShopId
if (this.cartStore.totalCount === 0) this.showCartPopup = false
},
/** 弹窗中增加指定门店的菜品 */
onPopupPlus(shopId, dish) {
const prevShopId = this.cartStore.currentShopId
this.cartStore.currentShopId = shopId
this.cartStore.addItem(dish)
this.cartStore.currentShopId = prevShopId
}
}
}
</script>
<style scoped>
.food-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 140rpx;
}
/* 自定义导航栏 */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
background: #FFB700;
}
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 40rpx;
height: 40rpx;
}
.navbar-title {
font-size: 34rpx;
font-weight: bold;
color: #363636;
}
.nav-placeholder {
width: 60rpx;
}
/* 顶部大图 */
.top-banner {
width: 100%;
}
.banner-img {
width: 100%;
height: 360rpx;
}
/* 门店列表 */
.shop-list {
padding: 0 24rpx;
}
.shop-card {
display: flex;
align-items: center;
background-color: #ffffff;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.shop-card:first-child {
border-radius: 20rpx 20rpx 0 0;
margin-top: 20rpx;
}
.shop-card:last-child {
border-radius: 0 0 20rpx 20rpx;
border-bottom: none;
}
.shop-card:first-child:last-child {
border-radius: 20rpx;
}
.shop-photo {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
flex-shrink: 0;
}
.shop-info {
flex: 1;
padding-left: 24rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.shop-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 16rpx;
}
.info-row {
display: flex;
align-items: flex-start;
margin-bottom: 8rpx;
}
.info-icon {
width: 28rpx;
height: 28rpx;
margin-right: 8rpx;
margin-top: 4rpx;
flex-shrink: 0;
}
.info-text {
font-size: 26rpx;
color: #999999;
line-height: 1.4;
}
/* 空状态 */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding-top: 200rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
/* 底部购物车栏 */
.cart-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 110rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.cart-left {
display: flex;
align-items: center;
}
.cart-icon {
width: 80rpx;
height: 80rpx;
margin-right: 16rpx;
}
.cart-price {
font-size: 36rpx;
color: #FF6B00;
font-weight: bold;
}
.cart-checkout-btn {
width: 220rpx;
height: 72rpx;
line-height: 72rpx;
background: linear-gradient(135deg, #FFB700, #FF9500);
color: #fff;
font-size: 28rpx;
border-radius: 36rpx;
border: none;
text-align: center;
margin-right: 10rpx;
}
.cart-checkout-btn.disabled {
background: #e0e0e0;
color: #999;
}
/* 购物车弹窗 */
.cart-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
display: flex;
align-items: flex-end;
}
.cart-popup {
width: 100%;
max-height: 60vh;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
}
.cart-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.cart-popup-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.cart-popup-clear {
font-size: 26rpx;
color: #999;
}
.cart-popup-list {
max-height: 50vh;
padding: 0 30rpx;
box-sizing: border-box;
width: 100%;
}
.cart-shop-group {
margin-bottom: 16rpx;
}
.cart-shop-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
padding: 16rpx 0 8rpx;
display: block;
}
.cart-popup-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.cart-item-photo {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
.cart-item-info {
flex: 1;
padding: 0 16rpx;
display: flex;
flex-direction: column;
min-width: 0;
}
.cart-item-name {
font-size: 26rpx;
color: #333;
}
.cart-item-price {
font-size: 26rpx;
color: #FF6B00;
}
.dish-actions {
display: flex;
align-items: center;
flex-shrink: 0;
}
.action-icon {
width: 48rpx;
height: 48rpx;
flex-shrink: 0;
}
.dish-quantity {
font-size: 28rpx;
color: #333;
min-width: 48rpx;
text-align: center;
}
.cart-popup-footer {
padding: 16rpx 30rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
}
.cart-fee-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8rpx 0;
}
.cart-fee-label {
font-size: 26rpx;
color: #666;
}
.cart-fee-value {
font-size: 26rpx;
color: #333;
}
.cart-fee-total {
font-size: 32rpx;
color: #FF6B00;
font-weight: bold;
}
</style>