This commit is contained in:
18631081161 2026-03-17 18:51:49 +08:00
parent 2d979e821e
commit d0a63a38af
16 changed files with 736 additions and 317 deletions

View File

@ -45,12 +45,14 @@
<el-tab-pane label="客服二维码" name="qrcode">
<el-form label-width="100px" style="max-width: 500px;">
<el-form-item label="二维码图片">
<el-input v-model="configs.qrcode" placeholder="二维码图片地址" />
<el-upload action="/api/upload/image" :headers="uploadHeaders" :show-file-list="false"
:on-success="(res) => configs.qrcode = res.url" accept="image/*" style="margin-top: 8px;">
<el-button size="small">上传图片</el-button>
:on-success="(res) => configs.qrcode = res.url" accept="image/*">
<el-image v-if="configs.qrcode" :src="configs.qrcode" style="width: 150px; cursor: pointer; border-radius: 8px;" fit="contain" />
<el-button v-else size="small">上传图片</el-button>
</el-upload>
<el-image v-if="configs.qrcode" :src="configs.qrcode" style="width: 150px; margin-top: 8px;" fit="contain" />
<div v-if="configs.qrcode" style="margin-top: 4px;">
<el-button size="small" text type="danger" @click="configs.qrcode = ''">移除</el-button>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="saveConfig('qrcode')">保存</el-button>

View File

@ -11,6 +11,7 @@
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<view class="page-body">
<view v-if="qrcodeUrl" class="qrcode-card">
<text class="qrcode-title">客服二维码</text>
<image class="qrcode-image" :src="qrcodeUrl" mode="aspectFit" @click="previewImage"></image>
@ -20,6 +21,7 @@
<text>暂无客服二维码</text>
</view>
</view>
</view>
</template>
<script>
@ -94,9 +96,16 @@ export default {
width: 60rpx;
}
.qrcode-page {
min-height: 100vh;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-body {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;

View File

@ -352,7 +352,7 @@ export default {
}
.summary {
border-top: 2rpx solid #007AFF;
border-top: none;
}
/* 底部接单栏 */
@ -371,9 +371,10 @@ export default {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007AFF;
color: #ffffff;
background-color: #FAD146;
color: #333333;
font-size: 32rpx;
font-weight: bold;
border-radius: 44rpx;
border: none;
}
@ -419,6 +420,7 @@ export default {
.modal-actions {
display: flex;
gap: 20rpx;
margin-top: 30rpx;
}
.modal-btn {
@ -433,11 +435,16 @@ export default {
.modal-btn.cancel {
background-color: #f5f5f5;
color: #666666;
border: none;
}
.modal-btn.cancel::after {
border: none;
}
.modal-btn.confirm {
background-color: #007AFF;
color: #ffffff;
background-color: #FAD146;
color: #333333;
}
.cert-form .form-item {

View File

@ -14,10 +14,14 @@
<!-- 订单商品摘要 -->
<view class="order-summary">
<text class="section-title">订单商品</text>
<view class="summary-item" v-for="item in cartItems" :key="item.dish.id">
<view v-for="shopGroup in cartStore.shopList" :key="shopGroup.shopInfo.id" class="shop-group">
<text class="shop-group-name">{{ shopGroup.shopInfo.name }}</text>
<view class="summary-item" v-for="item in shopGroup.items" :key="item.dish.id">
<image class="summary-photo" :src="item.dish.photo" mode="aspectFill"></image>
<text class="summary-name">{{ item.dish.name }} × {{ item.quantity }}</text>
<text class="summary-price">¥{{ (item.dish.price * item.quantity).toFixed(2) }}</text>
</view>
</view>
<view class="summary-item" v-if="packingFee > 0">
<text class="summary-name">打包费</text>
<text class="summary-price">¥{{ packingFee.toFixed(2) }}</text>
@ -84,9 +88,6 @@ export default {
cartStore() {
return useCartStore()
},
cartItems() {
return this.cartStore.items
},
packingFee() {
return this.cartStore.packingFee
},
@ -153,13 +154,8 @@ export default {
const commission = parseFloat(this.form.commission)
const goodsAmount = this.cartStore.totalPriceWithFee
//
const foodItems = this.cartItems.map(item => ({
shopId: this.cartStore.shopInfo?.id,
dishId: item.dish.id,
quantity: item.quantity,
unitPrice: item.dish.price
}))
//
const foodItems = this.cartStore.getAllFoodItems()
const orderData = {
orderType: 'Food',
@ -268,6 +264,18 @@ export default {
display: block;
}
.shop-group {
margin-bottom: 12rpx;
}
.shop-group-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
padding: 8rpx 0;
display: block;
}
.summary-item {
display: flex;
justify-content: space-between;
@ -275,9 +283,18 @@ export default {
padding: 10rpx 0;
}
.summary-photo {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
flex-shrink: 0;
margin-right: 16rpx;
}
.summary-name {
font-size: 26rpx;
color: #666666;
flex: 1;
}
.summary-price {
@ -367,6 +384,7 @@ export default {
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
z-index: 999;
justify-content: flex-end;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}

View File

@ -39,11 +39,58 @@
<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() {
@ -51,7 +98,19 @@ export default {
shops: [],
loading: true,
statusBarHeight: 0,
bannerUrl: ''
bannerUrl: '',
showCartPopup: false
}
},
computed: {
cartStore() {
return useCartStore()
},
cartTotalCount() {
return this.cartStore.totalCount
},
cartTotalPrice() {
return this.cartStore.totalPrice.toFixed(2)
}
},
onShow() {
@ -77,6 +136,31 @@ export default {
},
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
}
}
}
@ -86,6 +170,7 @@ export default {
.food-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 140rpx;
}
/* 自定义导航栏 */
@ -214,4 +299,197 @@ export default {
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>

View File

@ -46,14 +46,10 @@
<view class="dish-bottom">
<text class="dish-price">¥{{ dish.price.toFixed(1) }}</text>
<view class="dish-actions">
<view class="action-btn minus-btn" v-if="getQuantity(dish.id) > 0" @click="onMinus(dish)">
<text class="action-text">-</text>
</view>
<image class="action-icon" src="/static/ic_reduce.png" mode="aspectFit" v-if="getQuantity(dish.id) > 0" @click="onMinus(dish)"></image>
<text class="dish-quantity"
v-if="getQuantity(dish.id) > 0">{{ getQuantity(dish.id) }}</text>
<view class="action-btn plus-btn" @click="onPlus(dish)">
<text class="action-text">+</text>
</view>
<image class="action-icon" src="/static/ic_add.png" mode="aspectFit" @click="onPlus(dish)"></image>
</view>
</view>
</view>
@ -78,31 +74,30 @@
<text class="cart-popup-clear" @click="clearCart">清空</text>
</view>
<scroll-view scroll-y class="cart-popup-list">
<view class="cart-popup-item" v-for="item in cartItems" :key="item.dish.id">
<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">
<view class="action-btn minus-btn" @click="onMinus(item.dish)">
<text class="action-text">-</text>
</view>
<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>
<view class="action-btn plus-btn" @click="onPlus(item.dish)">
<text class="action-text">+</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="packingFee > 0">
<view class="cart-fee-row" v-if="cartStore.packingFee > 0">
<text class="cart-fee-label">打包费</text>
<text class="cart-fee-value">¥{{ packingFee.toFixed(2) }}</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">¥{{ cartTotalPriceWithFee }}</text>
<text class="cart-fee-total">¥{{ cartStore.totalPriceWithFee.toFixed(2) }}</text>
</view>
</view>
</view>
@ -131,23 +126,11 @@
cartStore() {
return useCartStore()
},
cartItems() {
return this.cartStore.items
},
cartTotalCount() {
return this.cartStore.totalCount
},
cartTotalPrice() {
return this.cartStore.totalPrice.toFixed(2)
},
packingFee() {
if (!this.shop.packingFeeType) return 0
if (this.shop.packingFeeType === 'Fixed')
return this.cartTotalCount > 0 ? (this.shop.packingFeeAmount || 0) : 0
return this.cartTotalCount * (this.shop.packingFeeAmount || 0)
},
cartTotalPriceWithFee() {
return (parseFloat(this.cartTotalPrice) + this.packingFee).toFixed(2)
}
},
onLoad(options) {
@ -185,6 +168,22 @@
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
},
goCheckout() {
if (this.cartTotalCount === 0) return
uni.navigateTo({
@ -380,35 +379,10 @@
flex-shrink: 0;
}
.action-btn {
.action-icon {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.minus-btn {
background: #f0f0f0;
}
.plus-btn {
background: #FF8C00;
}
.action-text {
font-size: 36rpx;
line-height: 1;
display: block;
}
.minus-btn .action-text {
color: #666;
}
.plus-btn .action-text {
color: #fff;
flex-shrink: 0;
}
.dish-quantity {
@ -519,6 +493,18 @@
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;

View File

@ -12,7 +12,7 @@
<image v-if="bannerUrl" class="top-banner" :src="bannerUrl" mode="aspectFill"></image>
<!-- 分类标签 + 排序 -->
<view class="tab-sort-bar">
<view class="tab-sort-bar" :style="{ top: (statusBarHeight + 44) + 'px' }">
<scroll-view class="tab-scroll" scroll-x>
<view class="tab-item" v-for="tab in tabs" :key="tab.value"
:class="{ active: currentTab === tab.value }" @click="switchTab(tab.value)">
@ -32,7 +32,7 @@
</view>
<!-- 订单列表 -->
<scroll-view class="order-list" scroll-y @scrolltolower="loadMore">
<view class="order-list">
<view v-if="loading && orders.length === 0" class="loading-tip">加载中...</view>
<view v-else-if="orders.length === 0" class="empty-tip">暂无订单</view>
@ -53,6 +53,38 @@
</view>
</template>
<!-- 美食街专属卡片布局 -->
<template v-else-if="order.orderType === 'Food'">
<!-- 标题行门店数量 + 跑腿费 -->
<view class="card-title-row">
<text class="card-item-name">门店数量{{ order.shopCount || 0 }}</text>
<view class="card-commission">
<text class="commission-label">跑腿费</text>
<text class="commission-value">{{ order.commission }}</text>
</view>
</view>
<!-- 美食数量 -->
<view class="food-info-row">
<text class="food-info-label">美食数量</text>
<text class="food-info-value">{{ order.dishItemCount || 0 }}</text>
</view>
<!-- 垫付商品金额 -->
<view class="food-info-row" v-if="order.goodsAmount">
<text class="food-info-label">垫付商品金额</text>
<text class="food-info-price">¥{{ order.goodsAmount }}</text>
</view>
<!-- 送达地址 -->
<view class="info-row" v-if="order.deliveryLocation">
<view class="dot orange"></view>
<text class="info-text">送达地址{{ order.deliveryLocation }}</text>
</view>
<!-- 查看详情按钮 -->
<view class="card-footer" style="margin-top: 16rpx;">
<view class="footer-spacer"></view>
<button class="accept-btn" size="mini" @click="viewFoodDetail(order)">查看详情</button>
</view>
</template>
<!-- 其他类型默认卡片布局 -->
<template v-else>
<!-- 标题行物品名 + 跑腿费 -->
@ -89,13 +121,11 @@
<!-- 接单按钮 -->
<view class="card-footer">
<view class="footer-spacer"></view>
<button v-if="order.orderType === 'Food'" class="accept-btn" size="mini"
@click="viewFoodDetail(order)">查看详情</button>
<button v-else class="accept-btn" size="mini" @click="onAcceptClick(order)">接单</button>
<button class="accept-btn" size="mini" @click="onAcceptClick(order)">接单</button>
</view>
</template>
</view>
</scroll-view>
</view>
<!-- 接单确认弹窗 -->
<view class="modal-mask" v-if="showAcceptModal" @click="showAcceptModal = false">
@ -177,10 +207,6 @@
{
label: '佣金优先',
value: 'commission'
},
{
label: '距离优先',
value: 'distance'
}
],
sortIndex: 0,
@ -195,15 +221,13 @@
phone: ''
},
certSubmitting: false,
certStatus: null,
userLocation: null
certStatus: null
}
},
onShow() {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
this.loadBanner()
this.requestLocation()
this.loadOrders()
},
methods: {
@ -214,19 +238,6 @@
if (res?.value) this.bannerUrl = res.value
} catch (e) {}
},
/** 申请定位权限 */
requestLocation() {
uni.getLocation({
type: 'gcj02',
success: (res) => {
this.userLocation = {
latitude: res.latitude,
longitude: res.longitude
}
},
fail: () => {}
})
},
/** 加载订单列表 */
async loadOrders() {
this.loading = true
@ -236,10 +247,6 @@
type: this.currentTab,
sort: sortValue
}
if (sortValue === 'distance' && this.userLocation) {
params.latitude = this.userLocation.latitude
params.longitude = this.userLocation.longitude
}
const res = await getOrderHall(params)
this.orders = res || []
} catch (e) {
@ -370,10 +377,9 @@
<style scoped>
.hall-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 40rpx;
}
/* 自定义导航栏 */
@ -403,7 +409,6 @@
.top-banner {
width: 100%;
height: 374rpx;
flex-shrink: 0;
}
/* 分类标签 + 排序栏 */
@ -411,7 +416,10 @@
display: flex;
align-items: center;
padding: 0 20rpx;
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 99;
background-color: #f5f5f5;
}
.tab-scroll {
@ -482,7 +490,6 @@
/* 订单列表 */
.order-list {
flex: 1;
padding: 16rpx 0;
}
@ -566,6 +573,30 @@
color: #333;
}
/* 美食街卡片信息行 */
.food-info-row {
display: flex;
align-items: center;
padding: 6rpx 0;
}
.food-info-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.food-info-value {
font-size: 28rpx;
color: #333;
}
.food-info-price {
font-size: 30rpx;
color: #FF6B00;
font-weight: bold;
}
/* 信息行 */
.card-body {
margin-bottom: 20rpx;

View File

@ -11,8 +11,11 @@
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 筛选栏吸顶 -->
<view class="filter-sticky" :style="{ top: (statusBarHeight + 44) + 'px' }">
<!-- 状态筛选标签 -->
<scroll-view class="tab-bar" scroll-x>
<view class="tab-bar">
<scroll-view class="tab-scroll" scroll-x>
<view
class="tab-item"
v-for="tab in statusTabs"
@ -21,9 +24,11 @@
@click="switchStatus(tab.value)"
>{{ tab.label }}</view>
</scroll-view>
</view>
<!-- 类型筛选标签 -->
<scroll-view class="type-bar" scroll-x>
<view class="type-bar">
<scroll-view class="type-scroll" scroll-x>
<view
class="type-item"
v-for="tab in typeTabs"
@ -32,9 +37,11 @@
@click="switchType(tab.value)"
>{{ tab.label }}</view>
</scroll-view>
</view>
</view>
<!-- 订单列表 -->
<scroll-view class="order-list" scroll-y>
<view class="order-list">
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-else-if="filteredOrders.length === 0" class="empty-tip">暂无订单</view>
@ -124,7 +131,7 @@
</template>
</view>
</view>
</scroll-view>
</view>
<!-- 评价弹窗 -->
<view class="modal-mask" v-if="showReviewModal" @click="showReviewModal = false">
@ -365,18 +372,26 @@ export default {
width: 60rpx;
}
.my-orders-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 40rpx;
}
/* 筛选栏吸顶容器 */
.filter-sticky {
position: sticky;
top: 0;
z-index: 99;
background-color: #ffffff;
}
/* 状态筛选栏 */
.tab-bar {
white-space: nowrap;
background-color: #ffffff;
padding: 16rpx 24rpx;
flex-shrink: 0;
}
.tab-scroll {
white-space: nowrap;
}
.tab-item {
@ -390,17 +405,18 @@ export default {
}
.tab-item.active {
background-color: #007AFF;
background-color: #FFB700;
color: #ffffff;
}
/* 类型筛选栏 */
.type-bar {
white-space: nowrap;
background-color: #ffffff;
padding: 12rpx 24rpx;
border-top: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.type-scroll {
white-space: nowrap;
}
.type-item {
@ -414,13 +430,12 @@ export default {
}
.type-item.active {
color: #007AFF;
border-color: #007AFF;
color: #FFB700;
border-color: #FFB700;
}
/* 订单列表 */
.order-list {
flex: 1;
padding: 16rpx 24rpx;
}
@ -449,7 +464,7 @@ export default {
.order-type-tag {
font-size: 24rpx;
color: #ffffff;
background-color: #007AFF;
background-color: #FFB700;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
@ -514,19 +529,19 @@ export default {
}
.btn-primary {
background-color: #007AFF;
background-color: #FAD146;
}
.btn-primary text {
color: #ffffff;
color: #333333;
}
.btn-detail {
border: 1rpx solid #007AFF;
border: 1rpx solid #FFB700;
}
.btn-detail text {
color: #007AFF;
color: #FFB700;
}
.btn-cancel {
@ -619,7 +634,7 @@ export default {
}
.modal-btn.confirm {
background-color: #007AFF;
color: #ffffff;
background-color: #FAD146;
color: #333333;
}
</style>

View File

@ -11,8 +11,11 @@
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 筛选栏吸顶 -->
<view class="filter-sticky" :style="{ top: (statusBarHeight + 44) + 'px' }">
<!-- 状态筛选标签 -->
<scroll-view class="tab-bar" scroll-x>
<view class="tab-bar">
<scroll-view class="tab-scroll" scroll-x>
<view
class="tab-item"
v-for="tab in statusTabs"
@ -21,9 +24,11 @@
@click="switchStatus(tab.value)"
>{{ tab.label }}</view>
</scroll-view>
</view>
<!-- 类型筛选标签 -->
<scroll-view class="type-bar" scroll-x>
<view class="type-bar">
<scroll-view class="type-scroll" scroll-x>
<view
class="type-item"
v-for="tab in typeTabs"
@ -32,9 +37,11 @@
@click="switchType(tab.value)"
>{{ tab.label }}</view>
</scroll-view>
</view>
</view>
<!-- 订单列表 -->
<scroll-view class="order-list" scroll-y>
<view class="order-list">
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-else-if="filteredOrders.length === 0" class="empty-tip">暂无接单</view>
@ -104,7 +111,7 @@
</template>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
@ -244,17 +251,25 @@ export default {
width: 60rpx;
}
.my-taken-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 40rpx;
}
/* 筛选栏吸顶容器 */
.filter-sticky {
position: sticky;
top: 0;
z-index: 99;
background-color: #ffffff;
}
.tab-bar {
white-space: nowrap;
background-color: #ffffff;
padding: 16rpx 24rpx;
flex-shrink: 0;
}
.tab-scroll {
white-space: nowrap;
}
.tab-item {
@ -268,16 +283,17 @@ export default {
}
.tab-item.active {
background-color: #007AFF;
background-color: #FFB700;
color: #ffffff;
}
.type-bar {
white-space: nowrap;
background-color: #ffffff;
padding: 12rpx 24rpx;
border-top: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.type-scroll {
white-space: nowrap;
}
.type-item {
@ -291,12 +307,11 @@ export default {
}
.type-item.active {
color: #007AFF;
border-color: #007AFF;
color: #FFB700;
border-color: #FFB700;
}
.order-list {
flex: 1;
padding: 16rpx 24rpx;
}
@ -324,13 +339,13 @@ export default {
.order-type-tag {
font-size: 24rpx;
color: #ffffff;
background-color: #007AFF;
background-color: #FFB700;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.order-status { font-size: 24rpx; font-weight: 500; }
.status-progress { color: #007AFF; }
.status-progress { color: #FFB700; }
.status-done { color: #52c41a; }
.status-cancel { color: #999999; }
.status-confirm { color: #ff9900; }
@ -378,11 +393,11 @@ export default {
.btn text { font-size: 26rpx; }
.btn-primary { background-color: #007AFF; }
.btn-primary text { color: #ffffff; }
.btn-primary { background-color: #FAD146; }
.btn-primary text { color: #333333; }
.btn-detail { border: 1rpx solid #007AFF; }
.btn-detail text { color: #007AFF; }
.btn-detail { border: 1rpx solid #FFB700; }
.btn-detail text { color: #FFB700; }
.btn-contact { border: 1rpx solid #52c41a; }
.btn-contact text { color: #52c41a; }

View File

@ -582,19 +582,19 @@ export default {
}
.btn-primary {
background-color: #007AFF;
background-color: #FAD146;
}
.btn-primary text {
color: #ffffff;
color: #333333;
}
.btn-secondary {
border: 1rpx solid #007AFF;
border: 1rpx solid #FAD146;
}
.btn-secondary text {
color: #007AFF;
color: #333333;
}
.btn-cancel {

View File

@ -11,6 +11,7 @@
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<view class="page-body">
<!-- 已认证状态 -->
<view v-if="certStatus === 'Approved'" class="status-card success">
<text class="status-icon"></text>
@ -45,6 +46,7 @@
</view>
</view>
</view>
</view>
</template>
<script>
@ -142,8 +144,15 @@ export default {
width: 60rpx;
}
.cert-page {
min-height: 100vh;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-body {
flex: 1;
padding: 40rpx 24rpx;
}

BIN
miniapp/static/ic_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,42 +1,69 @@
/**
* 购物车状态管理
* 管理美食街购物车中的菜品数量和价格计算
* Requirements: 9.1-9.9
* 购物车状态管理多门店版
* 按门店分组存储菜品支持跨门店下单
*/
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// 购物车商品列表 [{ dish: { id, name, photo, price, shopId }, quantity }]
const items = ref([])
// 按门店分组存储: { shopId: { shopInfo, items: [{ dish, quantity }] } }
const shops = ref({})
// 当前门店信息 { id, name, packingFeeType, packingFeeAmount }
const shopInfo = ref(null)
// 当前门店 ID门店详情页使用
const currentShopId = ref(null)
// ==================== 计算属性 ====================
/** 购物车总数量 = 所有菜品数量之和 */
/** 所有门店列表 */
const shopList = computed(() => {
return Object.values(shops.value).filter(s => s.items.length > 0)
})
/** 当前门店的商品列表 */
const items = computed(() => {
if (!currentShopId.value || !shops.value[currentShopId.value]) return []
return shops.value[currentShopId.value].items
})
/** 当前门店信息 */
const shopInfo = computed(() => {
if (!currentShopId.value || !shops.value[currentShopId.value]) return null
return shops.value[currentShopId.value].shopInfo
})
/** 所有门店总数量 */
const totalCount = computed(() => {
return items.value.reduce((sum, item) => sum + item.quantity, 0)
})
/** 购物车商品总价 = 所有菜品的(数量 × 单价)之和 */
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => {
return sum + item.quantity * item.dish.price
}, 0)
})
/** 打包费计算 */
const packingFee = computed(() => {
if (!shopInfo.value || totalCount.value === 0) return 0
const { packingFeeType, packingFeeAmount } = shopInfo.value
if (!packingFeeAmount) return 0
if (packingFeeType === 'Fixed') {
return packingFeeAmount
let count = 0
for (const shop of Object.values(shops.value)) {
count += shop.items.reduce((sum, item) => sum + item.quantity, 0)
}
// PerItem 模式:总份数 × 单份打包费
return totalCount.value * packingFeeAmount
return count
})
/** 所有门店商品总价(不含打包费) */
const totalPrice = computed(() => {
let price = 0
for (const shop of Object.values(shops.value)) {
price += shop.items.reduce((sum, item) => sum + item.quantity * item.dish.price, 0)
}
return price
})
/** 所有门店打包费总和 */
const packingFee = computed(() => {
let fee = 0
for (const shop of Object.values(shops.value)) {
if (shop.items.length === 0) continue
const { packingFeeType, packingFeeAmount } = shop.shopInfo
if (!packingFeeAmount) continue
if (packingFeeType === 'Fixed') {
fee += packingFeeAmount
} else {
const shopCount = shop.items.reduce((s, i) => s + i.quantity, 0)
fee += shopCount * packingFeeAmount
}
}
return fee
})
/** 含打包费的总金额 */
@ -44,75 +71,87 @@ export const useCartStore = defineStore('cart', () => {
return totalPrice.value + packingFee.value
})
/** 当前门店的商品数量 */
const currentShopCount = computed(() => {
if (!currentShopId.value || !shops.value[currentShopId.value]) return 0
return shops.value[currentShopId.value].items.reduce((sum, item) => sum + item.quantity, 0)
})
// ==================== 方法 ====================
/**
* 设置门店信息
* @param {Object} info - 门店信息
*/
/** 设置当前门店信息 */
function setShopInfo(info) {
// 如果切换了门店,清空购物车
if (shopInfo.value && shopInfo.value.id !== info.id) {
items.value = []
currentShopId.value = info.id
if (!shops.value[info.id]) {
shops.value[info.id] = { shopInfo: info, items: [] }
} else {
// 更新门店信息
shops.value[info.id].shopInfo = info
}
shopInfo.value = info
}
/**
* 获取菜品在购物车中的数量
* @param {number} dishId - 菜品 ID
* @returns {number} 数量
*/
/** 获取菜品在当前门店购物车中的数量 */
function getQuantity(dishId) {
const item = items.value.find(i => i.dish.id === dishId)
if (!currentShopId.value || !shops.value[currentShopId.value]) return 0
const item = shops.value[currentShopId.value].items.find(i => i.dish.id === dishId)
return item ? item.quantity : 0
}
/**
* 添加菜品到购物车数量 +1
* @param {Object} dish - 菜品对象 { id, name, photo, price }
*/
/** 添加菜品到当前门店购物车 */
function addItem(dish) {
const existing = items.value.find(i => i.dish.id === dish.id)
if (!currentShopId.value) return
const shop = shops.value[currentShopId.value]
if (!shop) return
const existing = shop.items.find(i => i.dish.id === dish.id)
if (existing) {
existing.quantity += 1
} else {
items.value.push({
dish: { ...dish },
quantity: 1
shop.items.push({ dish: { ...dish }, quantity: 1 })
}
}
/** 减少当前门店菜品数量 */
function removeItem(dishId) {
if (!currentShopId.value) return
const shop = shops.value[currentShopId.value]
if (!shop) return
const index = shop.items.findIndex(i => i.dish.id === dishId)
if (index === -1) return
shop.items[index].quantity -= 1
if (shop.items[index].quantity <= 0) {
shop.items.splice(index, 1)
}
// 如果门店购物车为空,移除门店
if (shop.items.length === 0) {
delete shops.value[currentShopId.value]
}
}
/** 清空所有购物车 */
function clearCart() {
shops.value = {}
currentShopId.value = null
}
/** 获取所有门店的 foodItems下单用 */
function getAllFoodItems() {
const foodItems = []
for (const shop of Object.values(shops.value)) {
for (const item of shop.items) {
foodItems.push({
shopId: shop.shopInfo.id,
dishId: item.dish.id,
quantity: item.quantity,
unitPrice: item.dish.price
})
}
}
/**
* 减少菜品数量数量减至 0 时移除
* @param {number} dishId - 菜品 ID
*/
function removeItem(dishId) {
const index = items.value.findIndex(i => i.dish.id === dishId)
if (index === -1) return
items.value[index].quantity -= 1
if (items.value[index].quantity <= 0) {
items.value.splice(index, 1)
}
}
/** 清空购物车 */
function clearCart() {
items.value = []
return foodItems
}
return {
items,
shopInfo,
totalCount,
totalPrice,
packingFee,
totalPriceWithFee,
setShopInfo,
getQuantity,
addItem,
removeItem,
clearCart
shops, currentShopId, shopList, items, shopInfo,
totalCount, totalPrice, packingFee, totalPriceWithFee, currentShopCount,
setShopInfo, getQuantity, addItem, removeItem, clearCart, getAllFoodItems
}
})

View File

@ -93,7 +93,10 @@ public class FoodOrderItemResponse
{
public int Id { get; set; }
public int ShopId { get; set; }
public string ShopName { get; set; } = string.Empty;
public int DishId { get; set; }
public string DishName { get; set; } = string.Empty;
public string? DishPhoto { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}

View File

@ -658,7 +658,8 @@ app.MapGet("/api/orders/{id}", async (int id, HttpContext httpContext, AppDbCont
var currentUserId = int.Parse(userIdClaim.Value);
var order = await db.Orders
.Include(o => o.FoodOrderItems)
.Include(o => o.FoodOrderItems).ThenInclude(fi => fi.Shop)
.Include(o => o.FoodOrderItems).ThenInclude(fi => fi.Dish)
.Include(o => o.Runner)
.FirstOrDefaultAsync(o => o.Id == id);
@ -738,7 +739,10 @@ app.MapGet("/api/orders/{id}", async (int id, HttpContext httpContext, AppDbCont
{
Id = fi.Id,
ShopId = fi.ShopId,
ShopName = fi.Shop?.Name ?? "未知门店",
DishId = fi.DishId,
DishName = fi.Dish?.Name ?? "未知菜品",
DishPhoto = fi.Dish?.Photo,
Quantity = fi.Quantity,
UnitPrice = fi.UnitPrice
}).ToList();
@ -764,10 +768,13 @@ app.MapGet("/api/orders/hall", async (
query = query.Where(o => o.OrderType == orderType);
}
// 排序:佣金优先(默认)或距离优先(暂不支持距离,回退到佣金)
query = sort?.ToLower() == "distance"
? query.OrderByDescending(o => o.Commission) // 距离排序需地图 API暂用佣金排序
: query.OrderByDescending(o => o.Commission);
// 排序
query = sort?.ToLower() switch
{
"commission" => query.OrderByDescending(o => o.Commission),
"distance" => query.OrderByDescending(o => o.CreatedAt), // 距离排序需地址坐标,暂按时间排序
_ => query.OrderByDescending(o => o.CreatedAt) // 默认按时间排序
};
var orders = await query.ToListAsync();