campus-errand/miniapp/pages/food/food-order.vue
18631081161 1fd330b233
Some checks reported errors
continuous-integration/drone/push Build encountered an error
下单优化
2026-04-03 15:40:09 +08:00

481 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="food-order-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="order-summary">
<text class="section-title">订单商品</text>
<view v-for="shopGroup in cartStore.shopList" :key="shopGroup.shopInfo.id" class="shop-group">
<view class="shop-group-header">
<image v-if="shopGroup.shopInfo.photo" class="shop-group-photo" :src="shopGroup.shopInfo.photo" mode="aspectFill"></image>
<text class="shop-group-name">{{ shopGroup.shopInfo.name }}</text>
</view>
<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>
</view>
<view class="summary-total">
<text class="total-label">餐品总金额</text>
<text class="total-value">¥{{ goodsAmount }}</text>
</view>
</view>
<!-- 订单表单 -->
<view class="form-section">
<!-- 送达地点 -->
<view class="form-item">
<text class="form-label">送达地点</text>
<input class="form-input" v-model="form.deliveryLocation" placeholder="请输入送达地点" />
</view>
<!-- 备注信息 -->
<view class="form-item">
<text class="form-label">备注信息</text>
<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注信息(选填)" />
</view>
<!-- 手机号 -->
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" v-model="form.phone" type="number" placeholder="请输入联系手机号" maxlength="11" />
</view>
<!-- 跑腿佣金 -->
<view class="form-item">
<text class="form-label">跑腿佣金</text>
<view class="commission-input">
<text class="commission-unit">¥</text>
<input class="form-input commission-field" v-model="form.commission" type="digit" :placeholder="`最低${minCommission}元`" />
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<text class="pay-amount">支付金额:¥{{ payAmount }}</text>
<button class="submit-btn" @click="onSubmit" :loading="submitting" :disabled="submitting">确定</button>
</view>
</view>
</template>
<script>
import { createOrder, getMinCommission, cancelOrder, confirmPayment } from '../../utils/api'
import { useCartStore } from '../../stores/cart'
export default {
data() {
return {
form: {
deliveryLocation: '',
remark: '',
phone: '',
commission: ''
},
submitting: false,
statusBarHeight: 0,
minCommission: 1.0
}
},
computed: {
cartStore() {
return useCartStore()
},
packingFee() {
return this.cartStore.packingFee
},
/** 餐品总金额(含打包费) */
goodsAmount() {
return this.cartStore.totalPriceWithFee.toFixed(2)
},
/** 支付金额 = 商品总金额 + 跑腿佣金 */
payAmount() {
const goods = this.cartStore.totalPriceWithFee
const commission = parseFloat(this.form.commission) || 0
return (goods + commission).toFixed(2)
}
},
onLoad() {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
this.loadMinCommission()
this.restoreFormData()
// 如果购物车为空,返回上一页
if (this.cartStore.totalCount === 0) {
uni.showToast({ title: '购物车为空', icon: 'none' })
setTimeout(() => { uni.navigateBack() }, 1000)
}
},
methods: {
goBack() { uni.navigateBack() },
/** 恢复登录前保存的表单数据 */
restoreFormData() {
const saved = uni.getStorageSync('loginFormData')
if (saved) {
try {
const data = JSON.parse(saved)
Object.assign(this.form, data)
} catch (e) {}
uni.removeStorageSync('loginFormData')
}
},
async loadMinCommission() {
try {
const res = await getMinCommission()
if (res?.value) this.minCommission = parseFloat(res.value) || 1.0
} catch (e) {}
},
/** 校验佣金 */
validateCommission() {
const val = this.form.commission
if (!val) {
uni.showToast({ title: '请输入跑腿佣金', icon: 'none' })
return false
}
const num = parseFloat(val)
if (isNaN(num) || num < this.minCommission) {
uni.showToast({ title: `跑腿佣金不可低于${this.minCommission}`, icon: 'none' })
return false
}
if (val.includes('.') && val.split('.')[1].length > 2) {
uni.showToast({ title: '佣金最多支持小数点后2位', icon: 'none' })
return false
}
return true
},
/** 校验表单 */
validateForm() {
if (!this.form.deliveryLocation.trim()) {
uni.showToast({ title: '请输入送达地点', icon: 'none' })
return false
}
if (!this.form.phone.trim()) {
uni.showToast({ title: '请输入手机号', icon: 'none' })
return false
}
if (!/^1\d{10}$/.test(this.form.phone.trim())) {
uni.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
return false
}
return this.validateCommission()
},
/** 提交订单 */
async onSubmit() {
if (!this.validateForm()) return
// 未登录跳转登录页
const token = uni.getStorageSync('token')
if (!token) {
// 保存表单数据,登录后恢复
uni.setStorageSync('loginFormData', JSON.stringify(this.form))
uni.setStorageSync('loginRedirect', '/pages/food/food-order')
uni.navigateTo({ url: '/pages/login/login' })
return
}
this.submitting = true
try {
const commission = parseFloat(this.form.commission)
const goodsAmount = this.cartStore.totalPriceWithFee
// 构建美食街订单数据(多门店)
const foodItems = this.cartStore.getAllFoodItems()
const orderData = {
orderType: 'Food',
deliveryLocation: this.form.deliveryLocation.trim(),
remark: this.form.remark.trim(),
phone: this.form.phone.trim(),
goodsAmount: goodsAmount,
commission: commission,
totalAmount: goodsAmount + commission,
foodItems: foodItems
}
const result = await createOrder(orderData)
if (result.paymentParams) {
try {
await this.wxPay(result.paymentParams)
// 支付成功,确认订单上架
try { await confirmPayment(result.id) } catch (ex) {}
} catch (e) {
// 支付失败/取消,撤销订单
try { await cancelOrder(result.id) } catch (ex) {}
return
}
}
// 下单成功,清空购物车
this.cartStore.clearCart()
uni.showToast({ title: '下单成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1500)
} catch (e) {
// 错误已在 request 中处理
} finally {
this.submitting = false
}
},
/** 调用微信支付 */
wxPay(params) {
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: params.timeStamp,
nonceStr: params.nonceStr,
package: params.package_,
signType: params.signType,
paySign: params.paySign,
success: resolve,
fail: (err) => {
if (err.errMsg !== 'requestPayment:fail cancel') {
uni.showToast({ title: '支付失败', icon: 'none' })
}
reject(err)
}
})
})
}
}
}
</script>
<style scoped>
/* 自定义导航栏 */
.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;
}
.food-order-page {
padding: 24rpx;
padding-bottom: 200rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.order-summary {
background-color: #ffffff;
border-radius: 16rpx;
padding: 20rpx 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333333;
margin-bottom: 16rpx;
display: block;
}
.shop-group {
margin-bottom: 12rpx;
}
.shop-group-header {
display: flex;
align-items: center;
padding: 8rpx 0;
}
.shop-group-photo {
width: 56rpx;
height: 56rpx;
border-radius: 10rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.shop-group-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
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 {
font-size: 26rpx;
color: #333333;
}
.summary-total {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 16rpx;
margin-top: 12rpx;
border-top: 1rpx solid #f0f0f0;
}
.total-label {
font-size: 28rpx;
color: #333333;
font-weight: bold;
}
.total-value {
font-size: 32rpx;
color: #e64340;
font-weight: bold;
}
.form-section {
background-color: #ffffff;
border-radius: 16rpx;
padding: 20rpx 30rpx;
}
.form-item {
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.form-item:last-child {
border-bottom: none;
}
.form-label {
font-size: 28rpx;
color: #333333;
margin-bottom: 12rpx;
display: block;
}
.form-input {
font-size: 28rpx;
color: #333333;
height: 72rpx;
}
.form-textarea {
font-size: 28rpx;
color: #333333;
width: 100%;
height: 160rpx;
}
.commission-input {
display: flex;
align-items: center;
}
.commission-unit {
font-size: 32rpx;
color: #e64340;
margin-right: 8rpx;
font-weight: bold;
}
.commission-field {
flex: 1;
}
.submit-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding: 20rpx 30rpx;
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);
}
.pay-amount {
font-size: 32rpx;
color: #e64340;
font-weight: bold;
flex: 1;
}
.submit-btn {
width: 240rpx;
height: 80rpx;
line-height: 80rpx;
background-color: #FAD146;
color: #ffffff;
font-size: 30rpx;
border-radius: 40rpx;
border: none;
margin-right: 10rpx;
}
.submit-btn::after {
border: none;
}
.submit-btn[disabled] {
opacity: 0.6;
}
</style>