修改流程
This commit is contained in:
+87
-6
@@ -234,14 +234,15 @@
|
|||||||
"featured-dishes": "Featured Dishes",
|
"featured-dishes": "Featured Dishes",
|
||||||
"nearby-merchants": "Nearby Merchants",
|
"nearby-merchants": "Nearby Merchants",
|
||||||
"open-member": "Become a Member",
|
"open-member": "Become a Member",
|
||||||
"recharge-now": "Recharge Now",
|
"recharge-now": "Deposit Now",
|
||||||
"quickTabs": {
|
"quickTabs": {
|
||||||
"memberZone": "Member Zone",
|
"memberZone": "Members' Picks",
|
||||||
"liveSeafoodAir": "Limited Live Seafood",
|
"liveSeafoodAir": "Live Seafood – Limited Supply",
|
||||||
"mustEatList": "CHEFLINK Must-Eat",
|
"mustEatList": "Must-Try List",
|
||||||
"newCalendar": "New Arrival Calendar",
|
"newCalendar": "New Arrival",
|
||||||
"newCalendarNav": "Today's New",
|
"newCalendarNav": "Today's New",
|
||||||
"freshSeafoodToday": "Fresh Seafood Today",
|
"freshSeafoodToday": "Day-Boat Fresh Catch",
|
||||||
|
"groupCatering": "Catering & Group Orders",
|
||||||
"energyMeal": "Energy Meals"
|
"energyMeal": "Energy Meals"
|
||||||
},
|
},
|
||||||
"mustEatListTabs": {
|
"mustEatListTabs": {
|
||||||
@@ -481,6 +482,8 @@
|
|||||||
"deliveryInfo": "Delivery",
|
"deliveryInfo": "Delivery",
|
||||||
"driverTip": "Driver tip",
|
"driverTip": "Driver tip",
|
||||||
"driverTipNote": "100% of your tip goes to the driver.",
|
"driverTipNote": "100% of your tip goes to the driver.",
|
||||||
|
"tipByDeliveryDatePrefix": "Delivery date ",
|
||||||
|
"tipNextTime": "Maybe next time",
|
||||||
"fillAddressHint": "Add delivery address and contact",
|
"fillAddressHint": "Add delivery address and contact",
|
||||||
"freeTag": "Free",
|
"freeTag": "Free",
|
||||||
"localDelivery": "Local delivery",
|
"localDelivery": "Local delivery",
|
||||||
@@ -488,6 +491,8 @@
|
|||||||
"localDeliverySubtitleSuffix": ".",
|
"localDeliverySubtitleSuffix": ".",
|
||||||
"memberThanksPrefix": "Thanks for being a ",
|
"memberThanksPrefix": "Thanks for being a ",
|
||||||
"memberThanksSuffix": " member.",
|
"memberThanksSuffix": " member.",
|
||||||
|
"missingParamsHint": "Missing checkout parameters. Please return to cart and try again.",
|
||||||
|
"emptyCartHint": "Your cart is empty. Please add items before checkout.",
|
||||||
"orderTotalLine": "Order total",
|
"orderTotalLine": "Order total",
|
||||||
"orderBreakdown": "Order details",
|
"orderBreakdown": "Order details",
|
||||||
"pay": "Pay",
|
"pay": "Pay",
|
||||||
@@ -551,6 +556,7 @@
|
|||||||
"cancellationTitle": "Merchant processing in progress",
|
"cancellationTitle": "Merchant processing in progress",
|
||||||
"cancelled": "Cancelled",
|
"cancelled": "Cancelled",
|
||||||
"deliveryAddress": "Delivery address",
|
"deliveryAddress": "Delivery address",
|
||||||
|
"deliveryDate": "Delivery date",
|
||||||
"deliveryPhotos": "Delivery of photos",
|
"deliveryPhotos": "Delivery of photos",
|
||||||
"deliveryTime": "Delivery time",
|
"deliveryTime": "Delivery time",
|
||||||
"beforeDeadline": " before",
|
"beforeDeadline": " before",
|
||||||
@@ -596,8 +602,71 @@
|
|||||||
"empty": "No energy meals yet",
|
"empty": "No energy meals yet",
|
||||||
"unavailable": "This bundle is unavailable"
|
"unavailable": "This bundle is unavailable"
|
||||||
},
|
},
|
||||||
|
"groupCatering": {
|
||||||
|
"cancelAction": "Cancel Reservation",
|
||||||
|
"cancelConfirm": "Are you sure you want to cancel this group meal reservation?",
|
||||||
|
"cancelReasonPlaceholder": "Enter a cancellation reason (optional)",
|
||||||
|
"cancelSuccess": "Cancelled successfully",
|
||||||
|
"cancelTitle": "Cancel Reservation",
|
||||||
|
"contactNamePlaceholder": "Enter contact name",
|
||||||
|
"contactPhonePlaceholder": "Enter contact phone",
|
||||||
|
"detailTitle": "Reservation Details",
|
||||||
|
"fieldCancelReason": "Cancellation Reason",
|
||||||
|
"fieldContactName": "Contact Name",
|
||||||
|
"fieldContactPhone": "Contact Phone",
|
||||||
|
"fieldEstimatedTotal": "Estimated Total",
|
||||||
|
"fieldExpectedTime": "Expected Meal/Delivery Time",
|
||||||
|
"fieldHandleRemark": "Merchant Notes",
|
||||||
|
"fieldMerchant": "Merchant",
|
||||||
|
"fieldPeopleCount": "Headcount",
|
||||||
|
"fieldPerCapitaPrice": "Price Per Person",
|
||||||
|
"fieldRemark": "Remarks",
|
||||||
|
"fieldScene": "Occasion",
|
||||||
|
"intro": "Group meal reservations are inquiry requests sent to merchants. They do not go through cart checkout or payment. Fill in headcount, budget, and expected time, and the merchant will follow up with menu and pricing details.",
|
||||||
|
"listEmpty": "No group meal reservations yet",
|
||||||
|
"emptyAction": "Submit a Reservation",
|
||||||
|
"merchantEmpty": "No merchants available",
|
||||||
|
"peopleCountPlaceholder": "Enter headcount",
|
||||||
|
"perCapitaPricePlaceholder": "Enter price per person",
|
||||||
|
"remarkPlaceholder": "e.g. delivery address or dietary notes",
|
||||||
|
"sceneOptions": {
|
||||||
|
"birthday": "Birthday Party",
|
||||||
|
"company": "Company Meeting",
|
||||||
|
"other": "Other",
|
||||||
|
"party": "Party / Celebration",
|
||||||
|
"school": "School Event"
|
||||||
|
},
|
||||||
|
"scenePlaceholder": "Custom occasion",
|
||||||
|
"selectDate": "Select date",
|
||||||
|
"selectExpectedTime": "Select expected time",
|
||||||
|
"selectMerchant": "Select merchant",
|
||||||
|
"selectTime": "Select time",
|
||||||
|
"statusCancelled": "Cancelled by User",
|
||||||
|
"statusCompleted": "Completed",
|
||||||
|
"statusConfirmed": "Confirmed",
|
||||||
|
"statusContacted": "Contacted",
|
||||||
|
"statusPending": "Pending",
|
||||||
|
"statusRejected": "Rejected",
|
||||||
|
"submitAction": "Submit Reservation",
|
||||||
|
"submitSuccess": "Submitted successfully",
|
||||||
|
"tabList": "My Reservations",
|
||||||
|
"tabSubmit": "New Reservation",
|
||||||
|
"tips": "Note: Estimated total = headcount × price per person. Final pricing is subject to merchant confirmation.",
|
||||||
|
"title": "Catering & Group Orders",
|
||||||
|
"validation": {
|
||||||
|
"contactName": "Please enter contact name",
|
||||||
|
"contactPhone": "Please enter contact phone",
|
||||||
|
"expectedTime": "Please select expected meal/delivery time",
|
||||||
|
"merchant": "Please select a merchant",
|
||||||
|
"peopleCount": "Headcount must be greater than 0",
|
||||||
|
"perCapitaPrice": "Price per person cannot be less than 0",
|
||||||
|
"scene": "Please enter an occasion"
|
||||||
|
},
|
||||||
|
"viewAndCancel": "View details / cancellable"
|
||||||
|
},
|
||||||
"store": {
|
"store": {
|
||||||
"addToCart": "Add to cart",
|
"addToCart": "Add to cart",
|
||||||
|
"quantity": "Quantity",
|
||||||
"appetizers": "Appetizers",
|
"appetizers": "Appetizers",
|
||||||
"merchantDiscounts": "Merchant discounts",
|
"merchantDiscounts": "Merchant discounts",
|
||||||
"claimNow": "Claim now",
|
"claimNow": "Claim now",
|
||||||
@@ -845,10 +914,22 @@
|
|||||||
"password-length-limit": "Password must be 8-16 characters long"
|
"password-length-limit": "Password must be 8-16 characters long"
|
||||||
},
|
},
|
||||||
"recharge": {
|
"recharge": {
|
||||||
|
"activityDetailTitle": "Recharge Promotion",
|
||||||
|
"activityPlaceholderDesc": "Promotion details are coming soon.",
|
||||||
|
"activityRechargeBtn": "Deposit Now",
|
||||||
|
"activityRulesTitle": "Promotion Rules",
|
||||||
"amount": "Recharge amount",
|
"amount": "Recharge amount",
|
||||||
"amount-invalid": "The recharge amount cannot be less than 0",
|
"amount-invalid": "The recharge amount cannot be less than 0",
|
||||||
"description": "Please enter your recharge amount",
|
"description": "Please enter your recharge amount",
|
||||||
"pay-method": "Please select a payment method",
|
"pay-method": "Please select a payment method",
|
||||||
|
"payMethodStripe": "Credit card (Stripe)",
|
||||||
|
"payMethodZip": "ZIP voucher payment",
|
||||||
|
"zipPayHint": "Scan the QR code to pay, then upload your payment proof",
|
||||||
|
"uploadVoucher": "Upload payment proof",
|
||||||
|
"voucherRequired": "Please upload payment proof first",
|
||||||
|
"voucherUploaded": "Proof uploaded",
|
||||||
|
"zipSubmitSuccess": "Proof submitted — pending review",
|
||||||
|
"promotionTier": "Deposit {recharge}, get {gift} bonus",
|
||||||
"success": "Recharge successful",
|
"success": "Recharge successful",
|
||||||
"title": "Recharge Balance"
|
"title": "Recharge Balance"
|
||||||
},
|
},
|
||||||
|
|||||||
+85
-4
@@ -236,13 +236,14 @@
|
|||||||
"open-member": "开通会员",
|
"open-member": "开通会员",
|
||||||
"recharge-now": "立即充值",
|
"recharge-now": "立即充值",
|
||||||
"quickTabs": {
|
"quickTabs": {
|
||||||
"memberZone": "会员专区",
|
"memberZone": "会员精选",
|
||||||
"liveSeafoodAir": "限量空运活海鲜",
|
"liveSeafoodAir": "限量活海鲜",
|
||||||
"mustEatList": "CHEFLINK必吃榜",
|
"mustEatList": "必吃榜单",
|
||||||
"newCalendar": "上新日历",
|
"newCalendar": "上新日历",
|
||||||
"newCalendarNav": "今日上新",
|
"newCalendarNav": "今日上新",
|
||||||
"freshSeafoodToday": "今日现打海鲜",
|
"freshSeafoodToday": "今日现打海鲜",
|
||||||
"energyMeal": "能量餐"
|
"groupCatering": "团餐预定",
|
||||||
|
"energyMeal": "能量餐食"
|
||||||
},
|
},
|
||||||
"mustEatListTabs": {
|
"mustEatListTabs": {
|
||||||
"merchant": "商家榜单",
|
"merchant": "商家榜单",
|
||||||
@@ -481,6 +482,8 @@
|
|||||||
"deliveryInfo": "配送信息",
|
"deliveryInfo": "配送信息",
|
||||||
"driverTip": "司机小费",
|
"driverTip": "司机小费",
|
||||||
"driverTipNote": "您所支付的小费 100% 归司机所有",
|
"driverTipNote": "您所支付的小费 100% 归司机所有",
|
||||||
|
"tipByDeliveryDatePrefix": "配送日期 ",
|
||||||
|
"tipNextTime": "下次再说吧",
|
||||||
"fillAddressHint": "请填写您的送货地址及联系方式",
|
"fillAddressHint": "请填写您的送货地址及联系方式",
|
||||||
"freeTag": "免费",
|
"freeTag": "免费",
|
||||||
"localDelivery": "本地配送",
|
"localDelivery": "本地配送",
|
||||||
@@ -488,6 +491,8 @@
|
|||||||
"localDeliverySubtitleSuffix": " 统一配送!",
|
"localDeliverySubtitleSuffix": " 统一配送!",
|
||||||
"memberThanksPrefix": "感谢您成为 ",
|
"memberThanksPrefix": "感谢您成为 ",
|
||||||
"memberThanksSuffix": " 会员。",
|
"memberThanksSuffix": " 会员。",
|
||||||
|
"missingParamsHint": "页面参数缺失,请返回购物车重新结算",
|
||||||
|
"emptyCartHint": "购物车暂无商品,请返回添加后再结算",
|
||||||
"orderTotalLine": "订单总计",
|
"orderTotalLine": "订单总计",
|
||||||
"orderBreakdown": "订单明细",
|
"orderBreakdown": "订单明细",
|
||||||
"pay": "付款",
|
"pay": "付款",
|
||||||
@@ -551,6 +556,7 @@
|
|||||||
"cancellationTitle": "商户处理中",
|
"cancellationTitle": "商户处理中",
|
||||||
"cancelled": "已取消",
|
"cancelled": "已取消",
|
||||||
"deliveryAddress": "配送地址",
|
"deliveryAddress": "配送地址",
|
||||||
|
"deliveryDate": "配送日期",
|
||||||
"deliveryPhotos": "送达照片",
|
"deliveryPhotos": "送达照片",
|
||||||
"deliveryTime": "送达时间",
|
"deliveryTime": "送达时间",
|
||||||
"beforeDeadline": "前",
|
"beforeDeadline": "前",
|
||||||
@@ -596,8 +602,71 @@
|
|||||||
"empty": "暂无能量餐",
|
"empty": "暂无能量餐",
|
||||||
"unavailable": "套餐暂不可购买"
|
"unavailable": "套餐暂不可购买"
|
||||||
},
|
},
|
||||||
|
"groupCatering": {
|
||||||
|
"cancelAction": "取消预定",
|
||||||
|
"cancelConfirm": "确定要取消这条团餐预定吗?",
|
||||||
|
"cancelReasonPlaceholder": "请输入取消原因(选填)",
|
||||||
|
"cancelSuccess": "取消成功",
|
||||||
|
"cancelTitle": "取消预定",
|
||||||
|
"contactNamePlaceholder": "请输入联系人",
|
||||||
|
"contactPhonePlaceholder": "请输入联系电话",
|
||||||
|
"detailTitle": "预定详情",
|
||||||
|
"fieldCancelReason": "取消原因",
|
||||||
|
"fieldContactName": "联系人",
|
||||||
|
"fieldContactPhone": "联系电话",
|
||||||
|
"fieldEstimatedTotal": "预计总价",
|
||||||
|
"fieldExpectedTime": "期望用餐/配送时间",
|
||||||
|
"fieldHandleRemark": "商家备注",
|
||||||
|
"fieldMerchant": "选择商家",
|
||||||
|
"fieldPeopleCount": "团餐人数",
|
||||||
|
"fieldPerCapitaPrice": "人均单价",
|
||||||
|
"fieldRemark": "备注",
|
||||||
|
"fieldScene": "用餐场景",
|
||||||
|
"intro": "团餐预定是向商家提交的咨询线索,不进入购物车或支付流程。请填写人数、预算和期望时间,商家会与您进一步沟通菜单与报价。",
|
||||||
|
"listEmpty": "暂无团餐预定记录",
|
||||||
|
"emptyAction": "去提交预定",
|
||||||
|
"merchantEmpty": "暂无可选商家",
|
||||||
|
"peopleCountPlaceholder": "请输入人数",
|
||||||
|
"perCapitaPricePlaceholder": "请输入人均单价",
|
||||||
|
"remarkPlaceholder": "例如配送地址、忌口要求等",
|
||||||
|
"sceneOptions": {
|
||||||
|
"birthday": "生日宴",
|
||||||
|
"company": "公司会议",
|
||||||
|
"other": "其他",
|
||||||
|
"party": "派对宴请",
|
||||||
|
"school": "学校活动"
|
||||||
|
},
|
||||||
|
"scenePlaceholder": "可自定义场景",
|
||||||
|
"selectDate": "选择日期",
|
||||||
|
"selectExpectedTime": "请选择期望时间",
|
||||||
|
"selectMerchant": "请选择商家",
|
||||||
|
"selectTime": "选择时间",
|
||||||
|
"statusCancelled": "用户取消",
|
||||||
|
"statusCompleted": "已完成",
|
||||||
|
"statusConfirmed": "已确认",
|
||||||
|
"statusContacted": "已联系",
|
||||||
|
"statusPending": "待处理",
|
||||||
|
"statusRejected": "已拒绝",
|
||||||
|
"submitAction": "提交预定",
|
||||||
|
"submitSuccess": "提交成功",
|
||||||
|
"tabList": "我的预定",
|
||||||
|
"tabSubmit": "提交预定",
|
||||||
|
"tips": "温馨提示:预计总价 = 团餐人数 × 人均单价,最终以商家确认报价为准。",
|
||||||
|
"title": "团餐预定",
|
||||||
|
"validation": {
|
||||||
|
"contactName": "请输入联系人",
|
||||||
|
"contactPhone": "请输入联系电话",
|
||||||
|
"expectedTime": "请选择期望用餐/配送时间",
|
||||||
|
"merchant": "请选择商家",
|
||||||
|
"peopleCount": "团餐人数必须大于 0",
|
||||||
|
"perCapitaPrice": "人均单价不能小于 0",
|
||||||
|
"scene": "请输入用餐场景"
|
||||||
|
},
|
||||||
|
"viewAndCancel": "查看详情 / 可取消"
|
||||||
|
},
|
||||||
"store": {
|
"store": {
|
||||||
"addToCart": "加入购物车",
|
"addToCart": "加入购物车",
|
||||||
|
"quantity": "数量",
|
||||||
"appetizers": "开胃菜",
|
"appetizers": "开胃菜",
|
||||||
"merchantDiscounts": "商家折扣",
|
"merchantDiscounts": "商家折扣",
|
||||||
"claimNow": "立即领取",
|
"claimNow": "立即领取",
|
||||||
@@ -845,10 +914,22 @@
|
|||||||
"password-length-limit": "密码长度必须为8-16位"
|
"password-length-limit": "密码长度必须为8-16位"
|
||||||
},
|
},
|
||||||
"recharge": {
|
"recharge": {
|
||||||
|
"activityDetailTitle": "充值活动",
|
||||||
|
"activityPlaceholderDesc": "活动详情即将上线,敬请期待。",
|
||||||
|
"activityRechargeBtn": "立即充值",
|
||||||
|
"activityRulesTitle": "活动规则",
|
||||||
"amount": "充值金额",
|
"amount": "充值金额",
|
||||||
"amount-invalid": "充值金额不能小于0",
|
"amount-invalid": "充值金额不能小于0",
|
||||||
"description": "请输入您的充值金额",
|
"description": "请输入您的充值金额",
|
||||||
"pay-method": "请选择支付方式",
|
"pay-method": "请选择支付方式",
|
||||||
|
"payMethodStripe": "信用卡(Stripe)",
|
||||||
|
"payMethodZip": "ZIP 凭证支付",
|
||||||
|
"zipPayHint": "请扫码完成转账后,上传支付凭证截图",
|
||||||
|
"uploadVoucher": "上传支付凭证",
|
||||||
|
"voucherRequired": "请先上传支付凭证",
|
||||||
|
"voucherUploaded": "凭证已上传",
|
||||||
|
"zipSubmitSuccess": "已提交凭证,等待后台审核",
|
||||||
|
"promotionTier": "充{recharge}送{gift}",
|
||||||
"success": "充值成功",
|
"success": "充值成功",
|
||||||
"title": "余额充值"
|
"title": "余额充值"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
"name" : "CHEFLINK delivery",
|
"name" : "CHEFLINK delivery",
|
||||||
"appid" : "__UNI__06509BE",
|
"appid" : "__UNI__06509BE",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "3.2.5",
|
"versionName" : "3.2.9",
|
||||||
"versionCode" : 325,
|
"versionCode" : 329,
|
||||||
"transformPx" : false,
|
"transformPx" : false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus" : {
|
"app-plus" : {
|
||||||
|
|||||||
@@ -160,8 +160,8 @@ defineExpose({
|
|||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="flex-1 min-w-0"></view>
|
<view v-else class="flex-1 min-w-0"></view>
|
||||||
<view class="featured-dish-add center shrink-0">
|
<view class="featured-dish-add shrink-0">
|
||||||
<image src="@img/chef/1285.png" class="w-28rpx h-28rpx" />
|
<image src="/static/app/images/add_cart.png" class="featured-dish-add__icon" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
@@ -282,11 +282,13 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.featured-dish-add {
|
.featured-dish-add {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-add__icon {
|
||||||
width: 56rpx;
|
width: 56rpx;
|
||||||
height: 56rpx;
|
height: 56rpx;
|
||||||
border-radius: 50%;
|
display: block;
|
||||||
background: #14181b;
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.featured-dish-promo {
|
.featured-dish-promo {
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ defineExpose({
|
|||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="flex-1 min-w-0"></view>
|
<view v-else class="flex-1 min-w-0"></view>
|
||||||
<view class="featured-dish-add center shrink-0">
|
<view class="featured-dish-add shrink-0">
|
||||||
<image src="@img/chef/1285.png" class="w-28rpx h-28rpx" />
|
<image src="/static/app/images/add_cart.png" class="featured-dish-add__icon" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
@@ -267,10 +267,13 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.featured-dish-add {
|
.featured-dish-add {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-add__icon {
|
||||||
width: 52rpx;
|
width: 52rpx;
|
||||||
height: 52rpx;
|
height: 52rpx;
|
||||||
border-radius: 50%;
|
display: block;
|
||||||
background: #14181b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.featured-dish-promo-text {
|
.featured-dish-promo-text {
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { dayjs } from '@/plugin/index'
|
||||||
|
import {
|
||||||
|
appGroupMealReservationCancelPost,
|
||||||
|
appGroupMealReservationIdGet,
|
||||||
|
type GroupMealReservationVo,
|
||||||
|
} from '@/service'
|
||||||
|
import { useConfigStore } from '@/store'
|
||||||
|
import { formatTimestampWithMonthName } from '@/utils/utils'
|
||||||
|
import {
|
||||||
|
calcGroupMealEstimatedTotal,
|
||||||
|
canCancelGroupMeal,
|
||||||
|
formatGroupMealMoney,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
const reservationId = ref('')
|
||||||
|
const loading = ref(true)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const detail = ref<GroupMealReservationVo>({})
|
||||||
|
const cancelReason = ref('')
|
||||||
|
|
||||||
|
const estimatedTotal = computed(() =>
|
||||||
|
formatGroupMealMoney(calcGroupMealEstimatedTotal(detail.value)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const status = Number(detail.value.status)
|
||||||
|
const keyMap: Record<number, string> = {
|
||||||
|
1: 'statusPending',
|
||||||
|
2: 'statusContacted',
|
||||||
|
3: 'statusConfirmed',
|
||||||
|
4: 'statusRejected',
|
||||||
|
5: 'statusCompleted',
|
||||||
|
6: 'statusCancelled',
|
||||||
|
}
|
||||||
|
const key = keyMap[status]
|
||||||
|
return key ? t(`pages-store.groupCatering.${key}`) : '--'
|
||||||
|
})
|
||||||
|
|
||||||
|
const canCancel = computed(() => canCancelGroupMeal(Number(detail.value.status)))
|
||||||
|
|
||||||
|
function formatExpectedTime(value?: number) {
|
||||||
|
if (!value) return '--'
|
||||||
|
return formatTimestampWithMonthName(Number(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
if (!reservationId.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await appGroupMealReservationIdGet({ id: reservationId.value })
|
||||||
|
detail.value = res.data ?? {}
|
||||||
|
} catch {
|
||||||
|
detail.value = {}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
if (!canCancel.value || submitting.value || !reservationId.value) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await appGroupMealReservationCancelPost({
|
||||||
|
body: {
|
||||||
|
id: reservationId.value,
|
||||||
|
cancelReason: cancelReason.value.trim() || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: t('pages-store.groupCatering.cancelSuccess'),
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 800)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmCancel() {
|
||||||
|
uni.showModal({
|
||||||
|
title: t('pages-store.groupCatering.cancelTitle'),
|
||||||
|
content: t('pages-store.groupCatering.cancelConfirm'),
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
handleCancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((query?: Record<string, string | undefined>) => {
|
||||||
|
reservationId.value = String(query?.id ?? '')
|
||||||
|
loadDetail()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="group-detail-page">
|
||||||
|
<navbar :title="t('pages-store.groupCatering.detailTitle')" circle-back />
|
||||||
|
|
||||||
|
<view v-if="loading" class="center py-120rpx text-28rpx text-#999">
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="group-detail-page__content px-30rpx pt-20rpx">
|
||||||
|
<view class="group-detail-card">
|
||||||
|
<view class="group-detail-card__head">
|
||||||
|
<text class="group-detail-card__merchant">
|
||||||
|
{{ detail.merchant?.merchantName || '--' }}
|
||||||
|
</text>
|
||||||
|
<text class="group-detail-card__status">{{ statusText }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-detail-row">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldScene') }}</text>
|
||||||
|
<text class="group-detail-row__value">{{ detail.scene || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-detail-row">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldPeopleCount') }}</text>
|
||||||
|
<text class="group-detail-row__value">{{ detail.peopleCount ?? '--' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-detail-row">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldPerCapitaPrice') }}</text>
|
||||||
|
<text class="group-detail-row__value">${{ formatGroupMealMoney(Number(detail.perCapitaPrice) || 0) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-detail-row">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldEstimatedTotal') }}</text>
|
||||||
|
<text class="group-detail-row__value group-detail-row__value--accent">${{ estimatedTotal }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-detail-row">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldExpectedTime') }}</text>
|
||||||
|
<text class="group-detail-row__value">{{ formatExpectedTime(detail.expectedTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-detail-row">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldContactName') }}</text>
|
||||||
|
<text class="group-detail-row__value">{{ detail.contactName || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-detail-row">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldContactPhone') }}</text>
|
||||||
|
<text class="group-detail-row__value">{{ detail.contactPhone || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="detail.remark" class="group-detail-row group-detail-row--column">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldRemark') }}</text>
|
||||||
|
<text class="group-detail-row__value">{{ detail.remark }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="detail.handleRemark" class="group-detail-row group-detail-row--column">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldHandleRemark') }}</text>
|
||||||
|
<text class="group-detail-row__value">{{ detail.handleRemark }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="detail.cancelReason" class="group-detail-row group-detail-row--column">
|
||||||
|
<text class="group-detail-row__label">{{ t('pages-store.groupCatering.fieldCancelReason') }}</text>
|
||||||
|
<text class="group-detail-row__value">{{ detail.cancelReason }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="canCancel" class="group-detail-card mt-24rpx">
|
||||||
|
<view class="text-28rpx text-#333 font-500 mb-20rpx">
|
||||||
|
{{ t('pages-store.groupCatering.fieldCancelReason') }}
|
||||||
|
</view>
|
||||||
|
<textarea
|
||||||
|
v-model="cancelReason"
|
||||||
|
class="group-detail-textarea"
|
||||||
|
:placeholder="t('pages-store.groupCatering.cancelReasonPlaceholder')"
|
||||||
|
maxlength="200"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="h-180rpx" />
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]" />
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="canCancel"
|
||||||
|
class="group-detail-page__footer fixed bottom-0 left-0 right-0 px-30rpx pt-16rpx bg-#fff"
|
||||||
|
>
|
||||||
|
<wd-button
|
||||||
|
custom-class="!h-98rpx !text-30rpx !text-#fff !lh-98rpx !rounded-16rpx"
|
||||||
|
block
|
||||||
|
:loading="submitting"
|
||||||
|
@click="confirmCancel"
|
||||||
|
>
|
||||||
|
{{ t('pages-store.groupCatering.cancelAction') }}
|
||||||
|
</wd-button>
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
page {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.group-detail-card {
|
||||||
|
padding: 28rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-card__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-card__merchant {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-card__status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #fff7ef;
|
||||||
|
color: #ce7138;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 18rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f3f3f3;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--column {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-row__label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-row__value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
&--accent {
|
||||||
|
color: #e02e24;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-row--column .group-detail-row__value {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-detail-page__footer {
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,893 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { dayjs } from '@/plugin/index'
|
||||||
|
import {
|
||||||
|
appGroupMealReservationMyListPost,
|
||||||
|
appGroupMealReservationSubmitPost,
|
||||||
|
appMerchantDetailMerchantIdGet,
|
||||||
|
appMerchantFeaturedListPost,
|
||||||
|
appMerchantNearbyListPost,
|
||||||
|
type GroupMealReservationVo,
|
||||||
|
} from '@/service'
|
||||||
|
import { useConfigStore, useUserStore } from '@/store'
|
||||||
|
import { formatTimestampWithMonthName } from '@/utils/utils'
|
||||||
|
import {
|
||||||
|
calcGroupMealEstimatedTotal,
|
||||||
|
canCancelGroupMeal,
|
||||||
|
formatGroupMealMoney,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
type PageTab = 'submit' | 'list'
|
||||||
|
const activeTab = ref<PageTab>('submit')
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const selectedMerchant = ref<{ id: string; merchantName: string } | null>(null)
|
||||||
|
const merchantPickerVisible = ref(false)
|
||||||
|
const merchantOptions = ref<Array<{ id: string; merchantName: string }>>([])
|
||||||
|
const merchantLoading = ref(false)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
peopleCount: '1',
|
||||||
|
perCapitaPrice: '',
|
||||||
|
scene: '',
|
||||||
|
contactName: '',
|
||||||
|
contactPhone: '',
|
||||||
|
remark: '',
|
||||||
|
})
|
||||||
|
const expectedDate = ref('')
|
||||||
|
const expectedTime = ref('')
|
||||||
|
|
||||||
|
const sceneOptions = computed(() => [
|
||||||
|
t('pages-store.groupCatering.sceneOptions.company'),
|
||||||
|
t('pages-store.groupCatering.sceneOptions.birthday'),
|
||||||
|
t('pages-store.groupCatering.sceneOptions.school'),
|
||||||
|
t('pages-store.groupCatering.sceneOptions.party'),
|
||||||
|
t('pages-store.groupCatering.sceneOptions.other'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const estimatedTotal = computed(() => {
|
||||||
|
const people = Number(form.value.peopleCount)
|
||||||
|
const price = Number(form.value.perCapitaPrice)
|
||||||
|
return formatGroupMealMoney(calcGroupMealEstimatedTotal({
|
||||||
|
peopleCount: people,
|
||||||
|
perCapitaPrice: price,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedTimeText = computed(() => {
|
||||||
|
if (!expectedDate.value || !expectedTime.value) {
|
||||||
|
return t('pages-store.groupCatering.selectExpectedTime')
|
||||||
|
}
|
||||||
|
return `${expectedDate.value} ${expectedTime.value}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
const reservationList = ref<GroupMealReservationVo[]>([])
|
||||||
|
const listLoading = ref(false)
|
||||||
|
const listLoadingMore = ref(false)
|
||||||
|
const listQueried = ref(false)
|
||||||
|
const listPageNum = ref(1)
|
||||||
|
const listHasMore = ref(true)
|
||||||
|
|
||||||
|
const showListEmpty = computed(
|
||||||
|
() => listQueried.value && !listLoading.value && reservationList.value.length === 0,
|
||||||
|
)
|
||||||
|
const showListLoading = computed(
|
||||||
|
() => listLoading.value && reservationList.value.length === 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
function prefillContact() {
|
||||||
|
const info = userStore.userInfo ?? {}
|
||||||
|
const nameParts = [info.firstName, info.lastName].filter(Boolean).join(' ').trim()
|
||||||
|
form.value.contactName = nameParts || String(info.nickName ?? '').trim()
|
||||||
|
const phone = String(info.phone ?? '').trim()
|
||||||
|
const areaCode = String(info.areaCode ?? '').trim()
|
||||||
|
form.value.contactPhone = phone
|
||||||
|
? `${areaCode ? `${areaCode} ` : ''}${phone}`.trim()
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExpectedTimestamp() {
|
||||||
|
if (!expectedDate.value || !expectedTime.value) return 0
|
||||||
|
const ts = dayjs(`${expectedDate.value} ${expectedTime.value}`).valueOf()
|
||||||
|
return Number.isFinite(ts) ? ts : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm() {
|
||||||
|
if (!userStore.checkLogin()) return false
|
||||||
|
if (!selectedMerchant.value?.id) {
|
||||||
|
uni.showToast({ title: t('pages-store.groupCatering.validation.merchant'), icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const peopleCount = Number(form.value.peopleCount)
|
||||||
|
if (!Number.isFinite(peopleCount) || peopleCount <= 0) {
|
||||||
|
uni.showToast({ title: t('pages-store.groupCatering.validation.peopleCount'), icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const perCapitaPrice = Number(form.value.perCapitaPrice)
|
||||||
|
if (!Number.isFinite(perCapitaPrice) || perCapitaPrice < 0) {
|
||||||
|
uni.showToast({ title: t('pages-store.groupCatering.validation.perCapitaPrice'), icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!form.value.scene.trim()) {
|
||||||
|
uni.showToast({ title: t('pages-store.groupCatering.validation.scene'), icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!form.value.contactName.trim()) {
|
||||||
|
uni.showToast({ title: t('pages-store.groupCatering.validation.contactName'), icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!form.value.contactPhone.trim()) {
|
||||||
|
uni.showToast({ title: t('pages-store.groupCatering.validation.contactPhone'), icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const expectedTimestamp = buildExpectedTimestamp()
|
||||||
|
if (!expectedTimestamp) {
|
||||||
|
uni.showToast({ title: t('pages-store.groupCatering.validation.expectedTime'), icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!validateForm() || submitting.value || !selectedMerchant.value) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await appGroupMealReservationSubmitPost({
|
||||||
|
body: {
|
||||||
|
merchantId: selectedMerchant.value.id,
|
||||||
|
peopleCount: Number(form.value.peopleCount),
|
||||||
|
perCapitaPrice: Number(form.value.perCapitaPrice),
|
||||||
|
scene: form.value.scene.trim(),
|
||||||
|
contactName: form.value.contactName.trim(),
|
||||||
|
contactPhone: form.value.contactPhone.trim(),
|
||||||
|
expectedTime: buildExpectedTimestamp(),
|
||||||
|
remark: form.value.remark.trim() || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: t('pages-store.groupCatering.submitSuccess'),
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
reloadList()
|
||||||
|
activeTab.value = 'list'
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMerchantOptions() {
|
||||||
|
merchantLoading.value = true
|
||||||
|
try {
|
||||||
|
const body = {
|
||||||
|
lat: userStore.userLocation.latitude,
|
||||||
|
lng: userStore.userLocation.longitude,
|
||||||
|
}
|
||||||
|
const [featuredRes, nearbyRes] = await Promise.all([
|
||||||
|
appMerchantFeaturedListPost({ body }),
|
||||||
|
appMerchantNearbyListPost({ body }),
|
||||||
|
])
|
||||||
|
const merged = new Map<string, { id: string; merchantName: string }>()
|
||||||
|
const appendList = (list: unknown) => {
|
||||||
|
if (!Array.isArray(list)) return
|
||||||
|
list.forEach((item: any) => {
|
||||||
|
const id = String(item?.id ?? '')
|
||||||
|
const merchantName = String(item?.merchantName ?? '').trim()
|
||||||
|
if (id && merchantName && !merged.has(id)) {
|
||||||
|
merged.set(id, { id, merchantName })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
appendList(featuredRes?.data)
|
||||||
|
appendList(nearbyRes?.data)
|
||||||
|
merchantOptions.value = Array.from(merged.values())
|
||||||
|
} catch {
|
||||||
|
merchantOptions.value = []
|
||||||
|
} finally {
|
||||||
|
merchantLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMerchantPicker() {
|
||||||
|
merchantPickerVisible.value = true
|
||||||
|
if (merchantOptions.value.length === 0) {
|
||||||
|
loadMerchantOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectMerchant(item: { id: string; merchantName: string }) {
|
||||||
|
selectedMerchant.value = item
|
||||||
|
merchantPickerVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initMerchantFromQuery(merchantId: string) {
|
||||||
|
if (!merchantId) return
|
||||||
|
try {
|
||||||
|
const res = await appMerchantDetailMerchantIdGet({
|
||||||
|
params: { merchantId },
|
||||||
|
})
|
||||||
|
const name = String(res.data?.merchantName ?? '').trim()
|
||||||
|
if (name) {
|
||||||
|
selectedMerchant.value = { id: merchantId, merchantName: name }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
selectedMerchant.value = { id: merchantId, merchantName: merchantId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseReservationRows(res: unknown) {
|
||||||
|
const payload = res as Record<string, unknown> | null | undefined
|
||||||
|
if (!payload) return { rows: [] as GroupMealReservationVo[], total: 0 }
|
||||||
|
const rows = Array.isArray(payload.rows)
|
||||||
|
? payload.rows
|
||||||
|
: Array.isArray((payload.data as Record<string, unknown> | undefined)?.rows)
|
||||||
|
? ((payload.data as Record<string, unknown>).rows as GroupMealReservationVo[])
|
||||||
|
: []
|
||||||
|
const total = Number(
|
||||||
|
payload.total ?? (payload.data as Record<string, unknown> | undefined)?.total ?? rows.length,
|
||||||
|
)
|
||||||
|
return { rows, total }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchReservationList(reset = false) {
|
||||||
|
if (reset) {
|
||||||
|
listPageNum.value = 1
|
||||||
|
listHasMore.value = true
|
||||||
|
listLoading.value = true
|
||||||
|
listQueried.value = false
|
||||||
|
} else if (!listHasMore.value || listLoadingMore.value || listLoading.value) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
listLoadingMore.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userStore.isLogin) {
|
||||||
|
reservationList.value = []
|
||||||
|
listHasMore.value = false
|
||||||
|
listLoading.value = false
|
||||||
|
listLoadingMore.value = false
|
||||||
|
listQueried.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await appGroupMealReservationMyListPost({
|
||||||
|
params: { pageNum: listPageNum.value, pageSize: PAGE_SIZE },
|
||||||
|
body: {},
|
||||||
|
})
|
||||||
|
const { rows, total } = parseReservationRows(res)
|
||||||
|
reservationList.value = reset ? rows : [...reservationList.value, ...rows]
|
||||||
|
listHasMore.value = reservationList.value.length < total
|
||||||
|
if (listHasMore.value) {
|
||||||
|
listPageNum.value += 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (reset) {
|
||||||
|
reservationList.value = []
|
||||||
|
}
|
||||||
|
listHasMore.value = false
|
||||||
|
} finally {
|
||||||
|
listLoading.value = false
|
||||||
|
listLoadingMore.value = false
|
||||||
|
listQueried.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadList() {
|
||||||
|
fetchReservationList(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tab: PageTab) {
|
||||||
|
activeTab.value = tab
|
||||||
|
if (tab === 'list') {
|
||||||
|
reloadList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status?: number) {
|
||||||
|
const keyMap: Record<number, string> = {
|
||||||
|
1: 'statusPending',
|
||||||
|
2: 'statusContacted',
|
||||||
|
3: 'statusConfirmed',
|
||||||
|
4: 'statusRejected',
|
||||||
|
5: 'statusCompleted',
|
||||||
|
6: 'statusCancelled',
|
||||||
|
}
|
||||||
|
const key = keyMap[Number(status)]
|
||||||
|
return key ? t(`pages-store.groupCatering.${key}`) : '--'
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(item: GroupMealReservationVo) {
|
||||||
|
if (!item.id) return
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages-store/pages/group-catering/detail?id=${item.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDateChange(e: { detail: { value: string } }) {
|
||||||
|
expectedDate.value = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTimeChange(e: { detail: { value: string } }) {
|
||||||
|
expectedTime.value = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((query?: Record<string, string | undefined>) => {
|
||||||
|
prefillContact()
|
||||||
|
if (query?.merchantId) {
|
||||||
|
initMerchantFromQuery(String(query.merchantId))
|
||||||
|
}
|
||||||
|
if (query?.tab === 'list') {
|
||||||
|
activeTab.value = 'list'
|
||||||
|
reloadList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (activeTab.value === 'list' && listQueried.value) {
|
||||||
|
reloadList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="group-catering-page">
|
||||||
|
<navbar :title="t('pages-store.groupCatering.title')" circle-back />
|
||||||
|
|
||||||
|
<view class="group-catering-tabs px-30rpx pt-16rpx pb-8rpx bg-#fff">
|
||||||
|
<view class="group-catering-tabs__inner">
|
||||||
|
<view
|
||||||
|
class="group-catering-tabs__item"
|
||||||
|
:class="{ 'group-catering-tabs__item--active': activeTab === 'submit' }"
|
||||||
|
@click="switchTab('submit')"
|
||||||
|
>
|
||||||
|
{{ t('pages-store.groupCatering.tabSubmit') }}
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="group-catering-tabs__item"
|
||||||
|
:class="{ 'group-catering-tabs__item--active': activeTab === 'list' }"
|
||||||
|
@click="switchTab('list')"
|
||||||
|
>
|
||||||
|
{{ t('pages-store.groupCatering.tabList') }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="activeTab === 'submit'" class="group-catering-form px-30rpx pt-24rpx">
|
||||||
|
<view class="group-form-card">
|
||||||
|
<text class="group-form-card__intro">{{ t('pages-store.groupCatering.intro') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-card mt-24rpx">
|
||||||
|
<view class="group-form-item" @click="openMerchantPicker">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldMerchant') }}</text>
|
||||||
|
<view class="group-form-item__value-wrap">
|
||||||
|
<text
|
||||||
|
class="group-form-item__value"
|
||||||
|
:class="{ 'group-form-item__value--placeholder': !selectedMerchant }"
|
||||||
|
>
|
||||||
|
{{ selectedMerchant?.merchantName || t('pages-store.groupCatering.selectMerchant') }}
|
||||||
|
</text>
|
||||||
|
<text class="group-form-item__arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-item">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldPeopleCount') }}</text>
|
||||||
|
<input
|
||||||
|
v-model="form.peopleCount"
|
||||||
|
class="group-form-item__input"
|
||||||
|
type="number"
|
||||||
|
:placeholder="t('pages-store.groupCatering.peopleCountPlaceholder')"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-item">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldPerCapitaPrice') }}</text>
|
||||||
|
<input
|
||||||
|
v-model="form.perCapitaPrice"
|
||||||
|
class="group-form-item__input"
|
||||||
|
type="digit"
|
||||||
|
:placeholder="t('pages-store.groupCatering.perCapitaPricePlaceholder')"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-item group-form-item--column">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldEstimatedTotal') }}</text>
|
||||||
|
<text class="group-form-item__estimate">${{ estimatedTotal }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-item group-form-item--column">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldScene') }}</text>
|
||||||
|
<view class="group-form-scenes">
|
||||||
|
<view
|
||||||
|
v-for="scene in sceneOptions"
|
||||||
|
:key="scene"
|
||||||
|
class="group-form-scene"
|
||||||
|
:class="{ 'group-form-scene--active': form.scene === scene }"
|
||||||
|
@click="form.scene = scene"
|
||||||
|
>
|
||||||
|
{{ scene }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<input
|
||||||
|
v-model="form.scene"
|
||||||
|
class="group-form-item__input mt-16rpx"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('pages-store.groupCatering.scenePlaceholder')"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-item">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldExpectedTime') }}</text>
|
||||||
|
<view class="group-form-item__picker-row">
|
||||||
|
<picker mode="date" :value="expectedDate" @change="onDateChange">
|
||||||
|
<view class="group-form-picker">{{ expectedDate || t('pages-store.groupCatering.selectDate') }}</view>
|
||||||
|
</picker>
|
||||||
|
<picker mode="time" :value="expectedTime" @change="onTimeChange">
|
||||||
|
<view class="group-form-picker">{{ expectedTime || t('pages-store.groupCatering.selectTime') }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<text class="group-form-item__hint">{{ expectedTimeText }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-item">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldContactName') }}</text>
|
||||||
|
<input
|
||||||
|
v-model="form.contactName"
|
||||||
|
class="group-form-item__input"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('pages-store.groupCatering.contactNamePlaceholder')"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-item">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldContactPhone') }}</text>
|
||||||
|
<input
|
||||||
|
v-model="form.contactPhone"
|
||||||
|
class="group-form-item__input"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('pages-store.groupCatering.contactPhonePlaceholder')"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-item group-form-item--column">
|
||||||
|
<text class="group-form-item__label">{{ t('pages-store.groupCatering.fieldRemark') }}</text>
|
||||||
|
<textarea
|
||||||
|
v-model="form.remark"
|
||||||
|
class="group-form-textarea"
|
||||||
|
:placeholder="t('pages-store.groupCatering.remarkPlaceholder')"
|
||||||
|
maxlength="200"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="group-form-tips mt-24rpx">
|
||||||
|
<text>{{ t('pages-store.groupCatering.tips') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="h-180rpx" />
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]" />
|
||||||
|
|
||||||
|
<view class="group-catering-page__footer fixed bottom-0 left-0 right-0 px-30rpx pt-16rpx bg-#fff">
|
||||||
|
<wd-button
|
||||||
|
custom-class="!h-98rpx !text-30rpx !text-#fff !lh-98rpx !rounded-16rpx"
|
||||||
|
block
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ t('pages-store.groupCatering.submitAction') }}
|
||||||
|
</wd-button>
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="group-catering-list-panel">
|
||||||
|
<view v-if="showListLoading" class="group-catering-loading">
|
||||||
|
<text class="group-catering-loading__text">{{ t('common.loading') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="showListEmpty" class="group-catering-empty">
|
||||||
|
<image
|
||||||
|
class="group-catering-empty__img"
|
||||||
|
src="@img/chef/100.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<text class="group-catering-empty__text">
|
||||||
|
{{ t('pages-store.groupCatering.listEmpty') }}
|
||||||
|
</text>
|
||||||
|
<view class="group-catering-empty__btn" @click="switchTab('submit')">
|
||||||
|
{{ t('pages-store.groupCatering.emptyAction') }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
v-else
|
||||||
|
scroll-y
|
||||||
|
class="group-catering-scroll"
|
||||||
|
:lower-threshold="80"
|
||||||
|
@scrolltolower="fetchReservationList(false)"
|
||||||
|
>
|
||||||
|
<view class="px-30rpx pt-24rpx">
|
||||||
|
<view
|
||||||
|
v-for="item in reservationList"
|
||||||
|
:key="String(item.id)"
|
||||||
|
class="group-list-card"
|
||||||
|
@click="openDetail(item)"
|
||||||
|
>
|
||||||
|
<view class="group-list-card__head">
|
||||||
|
<text class="group-list-card__merchant">{{ item.merchant?.merchantName || '--' }}</text>
|
||||||
|
<text class="group-list-card__status">{{ getStatusText(item.status) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-list-card__row">
|
||||||
|
<text>{{ t('pages-store.groupCatering.fieldPeopleCount') }}: {{ item.peopleCount ?? '--' }}</text>
|
||||||
|
<text>${{ formatGroupMealMoney(Number(item.perCapitaPrice) || 0) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-list-card__row">
|
||||||
|
<text>{{ t('pages-store.groupCatering.fieldEstimatedTotal') }}: ${{ formatGroupMealMoney(calcGroupMealEstimatedTotal(item)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="group-list-card__row">
|
||||||
|
<text>{{ formatTimestampWithMonthName(Number(item.expectedTime)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="canCancelGroupMeal(Number(item.status))" class="group-list-card__action">
|
||||||
|
{{ t('pages-store.groupCatering.viewAndCancel') }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="listLoadingMore" class="group-catering-loading-more">
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</view>
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]" />
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<wd-popup
|
||||||
|
v-model="merchantPickerVisible"
|
||||||
|
position="bottom"
|
||||||
|
custom-style="border-radius: 24rpx 24rpx 0 0;"
|
||||||
|
>
|
||||||
|
<view class="merchant-picker">
|
||||||
|
<view class="merchant-picker__title">{{ t('pages-store.groupCatering.selectMerchant') }}</view>
|
||||||
|
<scroll-view scroll-y class="merchant-picker__list">
|
||||||
|
<view v-if="merchantLoading" class="center py-80rpx text-28rpx text-#999">
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-for="item in merchantOptions"
|
||||||
|
:key="item.id"
|
||||||
|
class="merchant-picker__item"
|
||||||
|
@click="selectMerchant(item)"
|
||||||
|
>
|
||||||
|
{{ item.merchantName }}
|
||||||
|
</view>
|
||||||
|
<view v-if="!merchantLoading && merchantOptions.length === 0" class="center py-80rpx text-28rpx text-#999">
|
||||||
|
{{ t('pages-store.groupCatering.merchantEmpty') }}
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]" />
|
||||||
|
</view>
|
||||||
|
</wd-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
page {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.group-catering-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-list-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-height: calc(100vh - 280rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-scroll {
|
||||||
|
height: calc(100vh - 280rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: calc(100vh - 280rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-loading__text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-loading-more {
|
||||||
|
padding: 24rpx 0 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-tabs__inner {
|
||||||
|
display: flex;
|
||||||
|
padding: 8rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-tabs__item {
|
||||||
|
flex: 1;
|
||||||
|
height: 72rpx;
|
||||||
|
line-height: 72rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-card {
|
||||||
|
padding: 28rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-card__intro {
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item {
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f3f3f3;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item__label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item__value-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item__value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&--placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item__arrow {
|
||||||
|
color: #999;
|
||||||
|
font-size: 34rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item__input {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item__estimate {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e02e24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item__picker-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-picker {
|
||||||
|
flex: 1;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-item__hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-scenes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-scene {
|
||||||
|
padding: 12rpx 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: #fff7ef;
|
||||||
|
color: #ce7138;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form-tips {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #fff7ef;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #a0672d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list-card {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list-card__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list-card__merchant {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list-card__status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #fff7ef;
|
||||||
|
color: #ce7138;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list-card__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list-card__action {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #ce7138;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-page__footer {
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-picker {
|
||||||
|
max-height: 70vh;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-picker__title {
|
||||||
|
padding: 28rpx 30rpx 16rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-picker__list {
|
||||||
|
max-height: 56vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-picker__item {
|
||||||
|
padding: 28rpx 30rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 1rpx solid #f3f3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: calc(100vh - 280rpx);
|
||||||
|
padding: 80rpx 40rpx 120rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-empty__img {
|
||||||
|
width: 280rpx;
|
||||||
|
height: 280rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-empty__text {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-catering-empty__btn {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
min-width: 280rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 40rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
background: #14181b;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { GroupMealReservationVo } from '@/service/groupMealReservation'
|
||||||
|
|
||||||
|
/** 消费端团餐预定状态 */
|
||||||
|
export const GROUP_MEAL_STATUS = {
|
||||||
|
PENDING: 1,
|
||||||
|
CONTACTED: 2,
|
||||||
|
CONFIRMED: 3,
|
||||||
|
REJECTED: 4,
|
||||||
|
COMPLETED: 5,
|
||||||
|
USER_CANCELLED: 6,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function canCancelGroupMeal(status?: number) {
|
||||||
|
return status === GROUP_MEAL_STATUS.PENDING || status === GROUP_MEAL_STATUS.CONTACTED
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calcGroupMealEstimatedTotal(item: Pick<
|
||||||
|
GroupMealReservationVo,
|
||||||
|
'peopleCount' | 'perCapitaPrice'
|
||||||
|
>) {
|
||||||
|
const people = Number(item.peopleCount)
|
||||||
|
const price = Number(item.perCapitaPrice)
|
||||||
|
if (!Number.isFinite(people) || !Number.isFinite(price) || people <= 0 || price < 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return people * price
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatGroupMealMoney(value: number) {
|
||||||
|
return value.toFixed(2)
|
||||||
|
}
|
||||||
@@ -432,13 +432,13 @@ onShow(() => {
|
|||||||
</view>
|
</view>
|
||||||
<view v-else class="flex-1 min-w-0" />
|
<view v-else class="flex-1 min-w-0" />
|
||||||
<view
|
<view
|
||||||
class="cat-dish-add center shrink-0"
|
class="cat-dish-add shrink-0"
|
||||||
:class="{
|
:class="{
|
||||||
'cat-dish-add--busy': addingDishId === item.id,
|
'cat-dish-add--busy': addingDishId === item.id,
|
||||||
}"
|
}"
|
||||||
@click.stop="onAddCartClick(item)"
|
@click.stop="onAddCartClick(item)"
|
||||||
>
|
>
|
||||||
<image src="@img/chef/1285.png" class="w-28rpx h-28rpx" />
|
<image src="/static/app/images/add_cart.png" class="cat-dish-add__icon" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
@@ -698,11 +698,13 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cat-dish-add {
|
.cat-dish-add {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-dish-add__icon {
|
||||||
width: 56rpx;
|
width: 56rpx;
|
||||||
height: 56rpx;
|
height: 56rpx;
|
||||||
border-radius: 50%;
|
display: block;
|
||||||
background: #14181b;
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cat-dish-add--busy {
|
.cat-dish-add--busy {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import CheckoutSkeleton from "./components/checkout-skeleton.vue";
|
|||||||
import ChangePhone from "./components/change-phone.vue";
|
import ChangePhone from "./components/change-phone.vue";
|
||||||
import PriceDetail from "./components/price-detail.vue";
|
import PriceDetail from "./components/price-detail.vue";
|
||||||
import VisitMethod from "@/components/visit-method/index.vue";
|
import VisitMethod from "@/components/visit-method/index.vue";
|
||||||
import {ref} from "vue";
|
import { ref, computed, watch } from "vue";
|
||||||
import {
|
import {
|
||||||
appMerchantCartListByMerchantIdPost,
|
appMerchantCartListByMerchantIdPost,
|
||||||
appMerchantCartCalculateSavingsPost,
|
appMerchantCartCalculateSavingsPost,
|
||||||
@@ -33,6 +33,60 @@ import {
|
|||||||
loadCheckoutMerchantAppointments,
|
loadCheckoutMerchantAppointments,
|
||||||
type MerchantAppointmentSlot,
|
type MerchantAppointmentSlot,
|
||||||
} from "@/utils/deliverySchedule";
|
} from "@/utils/deliverySchedule";
|
||||||
|
import {
|
||||||
|
TIP_NEXT_TIME,
|
||||||
|
appendDeliveryScheduleFields,
|
||||||
|
buildDeliveryDateTipMap,
|
||||||
|
buildReceiveMethod,
|
||||||
|
collectDeliveryDatesFromMerchants,
|
||||||
|
formatDeliveryDateLabel,
|
||||||
|
optionalAddressId,
|
||||||
|
pickCheckoutDeliveryFee,
|
||||||
|
pickCheckoutGoodsAmount,
|
||||||
|
pickCheckoutPaidAmount,
|
||||||
|
pickCheckoutTax,
|
||||||
|
pickCheckoutTip,
|
||||||
|
resolveTipAmount,
|
||||||
|
stringifyCartIds,
|
||||||
|
} from "./utils/checkout-order";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
function getRouteQuery(): Record<string, string> {
|
||||||
|
try {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const page = pages[pages.length - 1] as UniApp.PageInstance & {
|
||||||
|
$page?: { options?: Record<string, string> }
|
||||||
|
options?: Record<string, string>
|
||||||
|
}
|
||||||
|
return page?.$page?.options ?? page?.options ?? {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDecodeURIComponent(value: string): string {
|
||||||
|
if (!value) return ''
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value)
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePageQuery(options?: Record<string, any>) {
|
||||||
|
const route = getRouteQuery()
|
||||||
|
return {
|
||||||
|
cartIds: String(options?.cartIds ?? route.cartIds ?? '').trim(),
|
||||||
|
type: String(options?.type ?? route.type ?? '').trim(),
|
||||||
|
storeId: String(options?.storeId ?? route.storeId ?? '').trim(),
|
||||||
|
orderRemark: safeDecodeURIComponent(String(options?.orderRemark ?? route.orderRemark ?? '')).trim(),
|
||||||
|
needTableware: String(options?.needTableware ?? route.needTableware ?? '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -282,19 +336,13 @@ function handleClickSegmented(index: number) {
|
|||||||
// 折叠面板
|
// 折叠面板
|
||||||
const collapseValue = ref([""]);
|
const collapseValue = ref([""]);
|
||||||
|
|
||||||
// 单个订单的小费(与设计稿一致:默认 $2)
|
// 小费选项:-1 表示「下次一定」(金额为 $0)
|
||||||
const selectedTipIndex = ref(2);
|
// 单店小费
|
||||||
|
const selectedTipIndex = ref(TIP_NEXT_TIME);
|
||||||
const diyTipValue = ref('')
|
const diyTipValue = ref('')
|
||||||
// 批量订单:每个店铺的小费索引映射 merchantId -> tipIndex
|
// 多店:按配送日期 yyyy-MM-dd 选择小费
|
||||||
const merchantTipIndexMap = ref<Record<string, number>>({})
|
const deliveryDateTipIndexMap = ref<Record<string, number>>({})
|
||||||
// 批量订单:每个店铺的自定义小费值映射 merchantId -> diyTipValue
|
const deliveryDateDiyTipValueMap = ref<Record<string, string>>({})
|
||||||
const merchantDiyTipValueMap = ref<Record<string, string>>({})
|
|
||||||
|
|
||||||
const tipOptions = ref([
|
|
||||||
{ label: "$ 2", value: 2 },
|
|
||||||
{ label: "$ 3", value: 3 },
|
|
||||||
{ label: "$ 4", value: 4 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 单个订单的小费选择
|
// 单个订单的小费选择
|
||||||
function selectedTipChange(item: any) {
|
function selectedTipChange(item: any) {
|
||||||
@@ -304,41 +352,80 @@ function selectedTipChange(item: any) {
|
|||||||
void appMerchantOrderCalculatePriceCart()
|
void appMerchantOrderCalculatePriceCart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量订单:选择某个店铺的小费
|
|
||||||
function selectedTipChangeForMerchant(merchantId: string, item: any) {
|
|
||||||
if(item.value === merchantTipIndexMap.value[merchantId]) return
|
|
||||||
merchantDiyTipValueMap.value[merchantId] = ''
|
|
||||||
merchantTipIndexMap.value[merchantId] = item.value
|
|
||||||
void appMerchantOrderCalculatePriceCart()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 单个订单的自定义小费确认(金额,单位:美元)
|
|
||||||
function handleConfirmTip() {
|
function handleConfirmTip() {
|
||||||
// 如果未填写其他小费金额,失去焦点时默认填充为 0
|
|
||||||
const val = String(diyTipValue.value || '').trim()
|
const val = String(diyTipValue.value || '').trim()
|
||||||
if (!val) {
|
if (!val) {
|
||||||
diyTipValue.value = '0'
|
diyTipValue.value = '0'
|
||||||
}
|
}
|
||||||
// 使用自定义小费时,选中"其他"这一项
|
|
||||||
if (selectedTipIndex.value !== 0) {
|
if (selectedTipIndex.value !== 0) {
|
||||||
selectedTipIndex.value = 0
|
selectedTipIndex.value = 0
|
||||||
}
|
}
|
||||||
void appMerchantOrderCalculatePriceCart()
|
void appMerchantOrderCalculatePriceCart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量订单:确认某个店铺的自定义小费(金额,单位:美元)
|
// 批量订单:按配送日期选择小费
|
||||||
function handleConfirmTipForMerchant(merchantId: string) {
|
function selectedTipChangeForDeliveryDate(dateKey: string, item: any) {
|
||||||
const val = String(merchantDiyTipValueMap.value[merchantId] || '').trim()
|
if (item.value === deliveryDateTipIndexMap.value[dateKey]) return
|
||||||
|
deliveryDateDiyTipValueMap.value[dateKey] = ''
|
||||||
|
deliveryDateTipIndexMap.value[dateKey] = item.value
|
||||||
|
void appMerchantOrderCalculatePriceCart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirmTipForDeliveryDate(dateKey: string) {
|
||||||
|
const val = String(deliveryDateDiyTipValueMap.value[dateKey] || '').trim()
|
||||||
if (!val) {
|
if (!val) {
|
||||||
merchantDiyTipValueMap.value[merchantId] = '0'
|
deliveryDateDiyTipValueMap.value[dateKey] = '0'
|
||||||
}
|
}
|
||||||
// 使用自定义小费时,选中"其他"这一项
|
if (deliveryDateTipIndexMap.value[dateKey] !== 0) {
|
||||||
if (merchantTipIndexMap.value[merchantId] !== 0) {
|
deliveryDateTipIndexMap.value[dateKey] = 0
|
||||||
merchantTipIndexMap.value[merchantId] = 0
|
|
||||||
}
|
}
|
||||||
void appMerchantOrderCalculatePriceCart()
|
void appMerchantOrderCalculatePriceCart()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSingleTipAmount() {
|
||||||
|
if (deliveryMethod.value !== 0) return 0
|
||||||
|
return resolveTipAmount(selectedTipIndex.value, diyTipValue.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBatchDeliveryDateTipMap() {
|
||||||
|
if (deliveryMethod.value !== 0) return {}
|
||||||
|
return buildDeliveryDateTipMap(
|
||||||
|
batchDeliveryDates.value,
|
||||||
|
deliveryDateTipIndexMap.value,
|
||||||
|
deliveryDateDiyTipValueMap.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCheckoutRequestBase(isBatch: boolean) {
|
||||||
|
const receiveMethod = buildReceiveMethod(deliveryMethod.value)
|
||||||
|
const targetAddressId = selectedAddressId.value || currentAddressId.value
|
||||||
|
const cartIds = isBatch
|
||||||
|
? stringifyCartIds(batchCartIds.value)
|
||||||
|
: stringifyCartIds(cartDataList.value.map((item: any) => item.id))
|
||||||
|
|
||||||
|
const body: Record<string, any> = {
|
||||||
|
cartIds,
|
||||||
|
receiveMethod,
|
||||||
|
phone: formData.value.phone,
|
||||||
|
areaCode: contact.value.areaCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressId = optionalAddressId(receiveMethod, targetAddressId)
|
||||||
|
if (addressId) body.addressId = addressId
|
||||||
|
|
||||||
|
appendDeliveryScheduleFields(body, deliveryTimeType.value, {
|
||||||
|
isBatch,
|
||||||
|
storeId: storeId.value,
|
||||||
|
diyTime: diyTime.value as any,
|
||||||
|
merchants: cartDataList.value as any[],
|
||||||
|
appointmentMap: merchantAppointmentMap.value,
|
||||||
|
buildScheduledTimePayload,
|
||||||
|
getTimeStamps,
|
||||||
|
})
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
// 是否选择了配送周卡
|
// 是否选择了配送周卡
|
||||||
const isWeeklyDelivery = ref(false);
|
const isWeeklyDelivery = ref(false);
|
||||||
// 切换配送周卡
|
// 切换配送周卡
|
||||||
@@ -464,8 +551,6 @@ const formData = ref<CreateOrderCartBo>({
|
|||||||
receiveMethod: 0,
|
receiveMethod: 0,
|
||||||
startScheduledTime: 0,
|
startScheduledTime: 0,
|
||||||
endScheduledTime: 0,
|
endScheduledTime: 0,
|
||||||
tipDiscount: 0,
|
|
||||||
weeklyDeliveryFee: 0,
|
|
||||||
})
|
})
|
||||||
// 是否需要餐具
|
// 是否需要餐具
|
||||||
const needTableware = ref(false)
|
const needTableware = ref(false)
|
||||||
@@ -478,16 +563,23 @@ async function safeAwait<T>(label: string, task: () => Promise<T>): Promise<T |
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLoad(async (options: any)=> {
|
|
||||||
|
// 页面加载状态
|
||||||
|
const loading = ref(true);
|
||||||
|
let bootstrapToken = 0
|
||||||
|
|
||||||
|
async function initCheckoutPage(options?: Record<string, any>) {
|
||||||
|
const token = ++bootstrapToken
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const query = resolvePageQuery(options)
|
||||||
// 判断是批量下单还是普通下单
|
// 判断是批量下单还是普通下单
|
||||||
if(options.type == 'batch' && options.cartIds) {
|
if (query.type === 'batch' && query.cartIds) {
|
||||||
// 批量下单模式
|
// 批量下单模式
|
||||||
orderType.value = 'batch'
|
orderType.value = 'batch'
|
||||||
batchCartIds.value = options.cartIds.split(',')
|
batchCartIds.value = query.cartIds.split(',').filter(Boolean)
|
||||||
console.log("下单类型",options.type);
|
console.log('下单类型', query.type, 'cartIds', batchCartIds.value)
|
||||||
|
|
||||||
// 默认取用户信息中的手机号作为收货手机号
|
// 默认取用户信息中的手机号作为收货手机号
|
||||||
formData.value.phone = userStore.userInfo.phone || ''
|
formData.value.phone = userStore.userInfo.phone || ''
|
||||||
@@ -510,13 +602,13 @@ onLoad(async (options: any)=> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
await safeAwait('appUserCardSelectDefault', appUserCardSelectDefault)
|
await safeAwait('appUserCardSelectDefault', appUserCardSelectDefault)
|
||||||
} else if(options.storeId) {
|
} else if (query.storeId) {
|
||||||
// 普通下单模式
|
// 普通下单模式
|
||||||
orderType.value = 'normal'
|
orderType.value = 'normal'
|
||||||
storeId.value = options.storeId as string
|
storeId.value = query.storeId
|
||||||
formData.value.orderRemark = options.orderRemark as string
|
formData.value.orderRemark = query.orderRemark
|
||||||
|
|
||||||
needTableware.value = options.needTableware === 'true'
|
needTableware.value = query.needTableware === 'true'
|
||||||
|
|
||||||
// 默认取用户信息中的手机号作为收货手机号
|
// 默认取用户信息中的手机号作为收货手机号
|
||||||
formData.value.phone = userStore.userInfo.phone || ''
|
formData.value.phone = userStore.userInfo.phone || ''
|
||||||
@@ -527,13 +619,26 @@ onLoad(async (options: any)=> {
|
|||||||
await safeAwait('getCartInfo', getCartInfo)
|
await safeAwait('getCartInfo', getCartInfo)
|
||||||
await safeAwait('getStoreDetail', getStoreDetail)
|
await safeAwait('getStoreDetail', getStoreDetail)
|
||||||
await safeAwait('appUserCardSelectDefault', appUserCardSelectDefault)
|
await safeAwait('appUserCardSelectDefault', appUserCardSelectDefault)
|
||||||
|
} else {
|
||||||
|
console.warn('[checkout] missing page query', query)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (token === bootstrapToken) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrapCheckout(options?: Record<string, any>) {
|
||||||
|
void initCheckoutPage(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
bootstrapCheckout(options)
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(async ()=> {
|
onShow(async ()=> {
|
||||||
|
if (!orderType.value) return
|
||||||
// 刷新地址,并在必要时重新计算价格
|
// 刷新地址,并在必要时重新计算价格
|
||||||
await safeAwait('getAddressList', getAddressList)
|
await safeAwait('getAddressList', getAddressList)
|
||||||
const hasCart =
|
const hasCart =
|
||||||
@@ -545,9 +650,10 @@ onShow(async ()=> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 页面加载状态
|
|
||||||
const loading = ref(true);
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (!orderType.value) {
|
||||||
|
bootstrapCheckout(getRouteQuery())
|
||||||
|
}
|
||||||
userStore.getUserInfo()
|
userStore.getUserInfo()
|
||||||
const currentHour = dayjs().hour();
|
const currentHour = dayjs().hour();
|
||||||
if (currentHour >= 11 && currentHour <= 14) {
|
if (currentHour >= 11 && currentHour <= 14) {
|
||||||
@@ -562,6 +668,28 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cartDataList = ref<MerchantCartVo[]>([])
|
const cartDataList = ref<MerchantCartVo[]>([])
|
||||||
|
|
||||||
|
const batchDeliveryDates = computed(() =>
|
||||||
|
collectDeliveryDatesFromMerchants(cartDataList.value as any[], merchantAppointmentMap.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
function ensureDeliveryDateTipDefaults() {
|
||||||
|
batchDeliveryDates.value.forEach((dateKey) => {
|
||||||
|
if (deliveryDateTipIndexMap.value[dateKey] === undefined) {
|
||||||
|
deliveryDateTipIndexMap.value[dateKey] = TIP_NEXT_TIME
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(batchDeliveryDates, ensureDeliveryDateTipDefaults, { immediate: true })
|
||||||
|
|
||||||
|
const tipOptions = computed(() => [
|
||||||
|
{ label: t('pages-store.checkout.tipNextTime'), value: TIP_NEXT_TIME },
|
||||||
|
{ label: '$ 2', value: 2 },
|
||||||
|
{ label: '$ 3', value: 3 },
|
||||||
|
{ label: '$ 4', value: 4 },
|
||||||
|
])
|
||||||
|
|
||||||
async function getCartInfo() {
|
async function getCartInfo() {
|
||||||
const res: any = await appMerchantCartListByMerchantIdPost({
|
const res: any = await appMerchantCartListByMerchantIdPost({
|
||||||
params: {
|
params: {
|
||||||
@@ -631,10 +759,6 @@ async function getBatchCartInfo() {
|
|||||||
cartDataList.value = results.filter(item => item !== null);
|
cartDataList.value = results.filter(item => item !== null);
|
||||||
console.log('批量模式-最终购物车数据', cartDataList.value);
|
console.log('批量模式-最终购物车数据', cartDataList.value);
|
||||||
cartDataList.value.forEach((m: any) => {
|
cartDataList.value.forEach((m: any) => {
|
||||||
const id = String(m.id);
|
|
||||||
if (merchantTipIndexMap.value[id] === undefined) {
|
|
||||||
merchantTipIndexMap.value[id] = 2;
|
|
||||||
}
|
|
||||||
ensureDefaultMerchantAppointment(m);
|
ensureDefaultMerchantAppointment(m);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -729,95 +853,29 @@ async function getStoreDetail() {
|
|||||||
|
|
||||||
const priceData = ref({})
|
const priceData = ref({})
|
||||||
async function appMerchantOrderCalculatePriceCart() {
|
async function appMerchantOrderCalculatePriceCart() {
|
||||||
// 优先使用新选择的地址id
|
|
||||||
const targetAddressId = selectedAddressId.value || currentAddressId.value
|
|
||||||
// if(!targetAddressId) return
|
|
||||||
|
|
||||||
const cartIds = orderType.value == 'batch' ? batchCartIds.value : cartDataList.value.map(item => item.id)
|
const cartIds = orderType.value == 'batch' ? batchCartIds.value : cartDataList.value.map(item => item.id)
|
||||||
|
if (!cartIds.length) return
|
||||||
|
|
||||||
// 批量模式使用批量计算价格接口
|
|
||||||
if(orderType.value == 'batch') {
|
if(orderType.value == 'batch') {
|
||||||
// 构建 merchantCouponMap: { merchantId: couponId }
|
const couponMap: Record<string, string> = {}
|
||||||
const couponMap: Record<string, number> = {}
|
|
||||||
Object.keys(merchantCouponMap.value).forEach(merchantId => {
|
Object.keys(merchantCouponMap.value).forEach(merchantId => {
|
||||||
if(merchantCouponMap.value[merchantId]?.id) {
|
const couponId = merchantCouponMap.value[merchantId]?.id
|
||||||
couponMap[merchantId] = merchantCouponMap.value[merchantId].id
|
if (couponId) couponMap[merchantId] = String(couponId)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 构建 merchantTipMap: { merchantId: tipAmount } (小费金额,单位:美元)
|
const body = buildCheckoutRequestBase(true)
|
||||||
const tipMap: Record<string, number> = {}
|
body.merchantCouponMap = couponMap
|
||||||
cartDataList.value.forEach((merchant: any) => {
|
body.deliveryDateTipMap = buildBatchDeliveryDateTipMap()
|
||||||
const merchantId = String(merchant.id)
|
|
||||||
if(deliveryMethod.value === 1) {
|
|
||||||
// 自取订单不需要小费
|
|
||||||
tipMap[merchantId] = 0
|
|
||||||
} else {
|
|
||||||
// 配送订单需要小费
|
|
||||||
const diyTip = merchantDiyTipValueMap.value[merchantId]
|
|
||||||
const tipIndex = merchantTipIndexMap.value[merchantId] || 0
|
|
||||||
if(diyTip) {
|
|
||||||
// 自定义小费金额(美元)
|
|
||||||
tipMap[merchantId] = Number(diyTip) || 0
|
|
||||||
} else if(tipIndex > 0) {
|
|
||||||
// 选择固定金额选项
|
|
||||||
tipMap[merchantId] = tipIndex
|
|
||||||
} else {
|
|
||||||
tipMap[merchantId] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const startMap: Record<string, number> = {}
|
const res: any = await appMerchantOrderCalculatePriceCartBatchPost({ body })
|
||||||
const endMap: Record<string, number> = {}
|
|
||||||
cartDataList.value.forEach((m: any) => {
|
|
||||||
const ap = merchantAppointmentMap.value[String(m.id)]
|
|
||||||
if (ap?.startTime && ap?.endTime) {
|
|
||||||
startMap[String(m.id)] = ap.startTime
|
|
||||||
endMap[String(m.id)] = ap.endTime
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const res: any = await appMerchantOrderCalculatePriceCartBatchPost({
|
|
||||||
body: {
|
|
||||||
addressId: targetAddressId,
|
|
||||||
cartIds: cartIds,
|
|
||||||
receiveMethod: deliveryMethod.value === 0 ? 1 : 2,
|
|
||||||
merchantCouponMap: couponMap,
|
|
||||||
merchantTipMap: tipMap,
|
|
||||||
merchantStartScheduledTimeMap: startMap,
|
|
||||||
merchantEndScheduledTimeMap: endMap,
|
|
||||||
phone: formData.value.phone,
|
|
||||||
areaCode: contact.value.areaCode,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log('批量购物车下单价格', res)
|
console.log('批量购物车下单价格', res)
|
||||||
priceData.value = res.data
|
priceData.value = res.data
|
||||||
} else {
|
} else {
|
||||||
// 普通模式使用单店铺计算价格接口
|
const body = buildCheckoutRequestBase(false)
|
||||||
// 计算小费金额(美元)
|
body.couponId = couponInfo.value?.id ? String(couponInfo.value.id) : undefined
|
||||||
let tipAmount = 0
|
body.tip = buildSingleTipAmount()
|
||||||
if(deliveryMethod.value === 0) {
|
|
||||||
// 配送订单需要小费
|
|
||||||
if(diyTipValue.value) {
|
|
||||||
tipAmount = Number(diyTipValue.value) || 0
|
|
||||||
} else if(selectedTipIndex.value > 0) {
|
|
||||||
tipAmount = selectedTipIndex.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const res: any = await appMerchantOrderCalculatePriceCartPost({
|
const res: any = await appMerchantOrderCalculatePriceCartPost({ body })
|
||||||
body: {
|
|
||||||
addressId: targetAddressId,
|
|
||||||
cartIds: cartIds,
|
|
||||||
receiveMethod: deliveryMethod.value === 0 ? 1 : 2,
|
|
||||||
couponId: couponInfo.value ? couponInfo.value.id : '',
|
|
||||||
tipDiscount: tipAmount, // 小费金额(美元),自取订单不需要小费
|
|
||||||
// weeklyDeliveryFee: isWeeklyDelivery.value ? 1 : 2, // 是否支付周配送费(1-是 2-否)
|
|
||||||
phone: formData.value.phone,
|
|
||||||
areaCode: contact.value.areaCode,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log('购物车下单价格', res)
|
console.log('购物车下单价格', res)
|
||||||
priceData.value = res.data
|
priceData.value = res.data
|
||||||
}
|
}
|
||||||
@@ -896,14 +954,15 @@ const passwordInputRef = ref(null)
|
|||||||
const resOrderId = ref('')
|
const resOrderId = ref('')
|
||||||
const resOrderIds = ref([]) // 批量下单返回的订单ID列表
|
const resOrderIds = ref([]) // 批量下单返回的订单ID列表
|
||||||
function handleGoSettle() {
|
function handleGoSettle() {
|
||||||
// 优先使用新选择的地址id
|
if (deliveryMethod.value === 0) {
|
||||||
const targetAddressId = selectedAddressId.value || currentAddressId.value
|
const targetAddressId = selectedAddressId.value || currentAddressId.value
|
||||||
if(!targetAddressId) {
|
if (!targetAddressId) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('pages-store.checkout.pleaseSelectAddress'),
|
title: t('pages-store.checkout.pleaseSelectAddress'),
|
||||||
icon: 'none'
|
icon: 'none',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(payMethodOptions.value.payMethod === 1 && !payMethodOptions.value.cardId) {
|
if(payMethodOptions.value.payMethod === 1 && !payMethodOptions.value.cardId) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -940,65 +999,21 @@ function handleGoSettle() {
|
|||||||
// 批量下单
|
// 批量下单
|
||||||
if(orderType.value == 'batch') {
|
if(orderType.value == 'batch') {
|
||||||
if(resOrderIds.value.length === 0) {
|
if(resOrderIds.value.length === 0) {
|
||||||
// 构建 merchantCouponMap: { merchantId: couponId }
|
const couponMap: Record<string, string> = {}
|
||||||
const couponMap: Record<string, number> = {}
|
|
||||||
Object.keys(merchantCouponMap.value).forEach(merchantId => {
|
Object.keys(merchantCouponMap.value).forEach(merchantId => {
|
||||||
if(merchantCouponMap.value[merchantId]?.id) {
|
const couponId = merchantCouponMap.value[merchantId]?.id
|
||||||
couponMap[merchantId] = merchantCouponMap.value[merchantId].id
|
if (couponId) couponMap[merchantId] = String(couponId)
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 构建 merchantTipMap: { merchantId: tipAmount } (小费金额,单位:美元)
|
|
||||||
const tipMap: Record<string, number> = {}
|
|
||||||
cartDataList.value.forEach((merchant: any) => {
|
|
||||||
const merchantId = String(merchant.id)
|
|
||||||
if(deliveryMethod.value === 1) {
|
|
||||||
// 自取订单不需要小费
|
|
||||||
tipMap[merchantId] = 0
|
|
||||||
} else {
|
|
||||||
// 配送订单需要小费
|
|
||||||
const diyTip = merchantDiyTipValueMap.value[merchantId]
|
|
||||||
const tipIndex = merchantTipIndexMap.value[merchantId] || 0
|
|
||||||
if(diyTip) {
|
|
||||||
// 自定义小费金额(美元)
|
|
||||||
tipMap[merchantId] = Number(diyTip) || 0
|
|
||||||
} else if(tipIndex > 0) {
|
|
||||||
// 选择固定金额选项
|
|
||||||
tipMap[merchantId] = tipIndex
|
|
||||||
} else {
|
|
||||||
tipMap[merchantId] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const data: Record<string, any> = {
|
const data: Record<string, any> = {
|
||||||
addressId: targetAddressId,
|
...buildCheckoutRequestBase(true),
|
||||||
phone: formData.value.phone,
|
|
||||||
areaCode: contact.value.areaCode,
|
|
||||||
cartIds: batchCartIds.value,
|
|
||||||
merchantCouponMap: couponMap,
|
merchantCouponMap: couponMap,
|
||||||
merchantTipMap: tipMap,
|
deliveryDateTipMap: buildBatchDeliveryDateTipMap(),
|
||||||
deliveryMethod: visitMethod.value.label, // 派送方式(如放门口或者交到顾客手中)
|
deliveryMethod: visitMethod.value.label,
|
||||||
deliveryType: deliveryTimeType.value, // 1-立即交付 2-预约交付
|
|
||||||
orderRemark: formData.value.orderRemark,
|
orderRemark: formData.value.orderRemark,
|
||||||
receiveMethod: deliveryMethod.value === 0 ? 1 : 2, // 收货方式(1-派送 2-自取)
|
needTableware: needTableware.value ? 1 : 2,
|
||||||
needTableware: needTableware.value ? 1 : 2, // 餐具 1是 2否
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deliveryTimeType.value === 1) {
|
|
||||||
const result = getTimeStamps(showDeliveryTime.value);
|
|
||||||
data.startScheduledTime = result.startTimestamp
|
|
||||||
data.endScheduledTime = result.endTimestamp
|
|
||||||
} else {
|
|
||||||
const scheduled = buildScheduledTimePayload(
|
|
||||||
cartDataList.value as any[],
|
|
||||||
merchantAppointmentMap.value
|
|
||||||
)
|
|
||||||
data.startScheduledTime = scheduled.startScheduledTime
|
|
||||||
data.endScheduledTime = scheduled.endScheduledTime
|
|
||||||
data.merchantStartScheduledTimeMap = scheduled.startMap
|
|
||||||
data.merchantEndScheduledTimeMap = scheduled.endMap
|
|
||||||
}
|
|
||||||
console.log('批量下单参数', data)
|
console.log('批量下单参数', data)
|
||||||
appMerchantOrderCreateOrderCartBatchPost({
|
appMerchantOrderCreateOrderCartBatchPost({
|
||||||
body: data
|
body: data
|
||||||
@@ -1034,41 +1049,15 @@ function handleGoSettle() {
|
|||||||
} else {
|
} else {
|
||||||
// 普通下单
|
// 普通下单
|
||||||
if(!resOrderId.value) {
|
if(!resOrderId.value) {
|
||||||
// 计算小费金额(美元)
|
|
||||||
let tipAmount = 0
|
|
||||||
if(deliveryMethod.value === 0) {
|
|
||||||
// 配送订单需要小费
|
|
||||||
if(diyTipValue.value) {
|
|
||||||
tipAmount = Number(diyTipValue.value) || 0
|
|
||||||
} else if(selectedTipIndex.value > 0) {
|
|
||||||
tipAmount = selectedTipIndex.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: Record<string, any> = {
|
const data: Record<string, any> = {
|
||||||
addressId: targetAddressId,
|
...buildCheckoutRequestBase(false),
|
||||||
phone: formData.value.phone,
|
couponId: couponInfo.value?.id ? String(couponInfo.value.id) : undefined,
|
||||||
areaCode: contact.value.areaCode,
|
deliveryMethod: visitMethod.value.label,
|
||||||
cartIds: cartDataList.value.map(item => item.id),
|
|
||||||
couponId: couponInfo.value ? couponInfo.value.id : '',
|
|
||||||
deliveryMethod: visitMethod.value.label, // 派送方式(如放门口或者交到顾客手中)
|
|
||||||
deliveryType: deliveryTimeType.value, // 1-立即交付 2-预约交付
|
|
||||||
orderRemark: formData.value.orderRemark,
|
orderRemark: formData.value.orderRemark,
|
||||||
receiveMethod: deliveryMethod.value === 0 ? 1 : 2, // 收货方式(1-派送 2-自取)
|
tip: buildSingleTipAmount(),
|
||||||
tipDiscount: tipAmount, // 小费金额(美元),自取订单不需要小费
|
needTableware: needTableware.value ? 1 : 2,
|
||||||
// weeklyDeliveryFee: isWeeklyDelivery.value ? 1 : 2, // 是否支付周配送费(1-是 2-否)
|
|
||||||
needTableware: needTableware.value ? 1 : 2, // 餐具 1是 2否
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deliveryTimeType.value === 1) {
|
|
||||||
const result = getTimeStamps(showDeliveryTime.value);
|
|
||||||
data.startScheduledTime = result.startTimestamp
|
|
||||||
data.endScheduledTime = result.endTimestamp
|
|
||||||
} else {
|
|
||||||
const ap = merchantAppointmentMap.value[String(storeId.value)]
|
|
||||||
data.startScheduledTime = ap?.startTime ?? (diyTime.value as any).startTime
|
|
||||||
data.endScheduledTime = ap?.endTime ?? (diyTime.value as any).endTime
|
|
||||||
}
|
|
||||||
console.log('下单参数', data)
|
console.log('下单参数', data)
|
||||||
appMerchantOrderCreateOrderCartPost({
|
appMerchantOrderCreateOrderCartPost({
|
||||||
body: data
|
body: data
|
||||||
@@ -1294,9 +1283,9 @@ export interface CreateOrderCartBo {
|
|||||||
*/
|
*/
|
||||||
endScheduledTime?: number;
|
endScheduledTime?: number;
|
||||||
/**
|
/**
|
||||||
* 小费比例
|
* 配送单小费(固定金额)
|
||||||
*/
|
*/
|
||||||
tipDiscount?: number;
|
tip?: number;
|
||||||
/**
|
/**
|
||||||
* 是否支付周配送费(1-是 2-否)
|
* 是否支付周配送费(1-是 2-否)
|
||||||
*/
|
*/
|
||||||
@@ -1308,10 +1297,6 @@ export interface CreateOrderCartBo {
|
|||||||
const couponInfo = ref(null)
|
const couponInfo = ref(null)
|
||||||
// 批量订单:每个店铺的优惠券映射 merchantId -> couponInfo
|
// 批量订单:每个店铺的优惠券映射 merchantId -> couponInfo
|
||||||
const merchantCouponMap = ref<Record<string, any>>({})
|
const merchantCouponMap = ref<Record<string, any>>({})
|
||||||
// 批量订单:每个店铺的小费映射 merchantId -> tipAmount (小费金额,单位:美元)
|
|
||||||
const merchantTipMap = ref<Record<string, number>>({})
|
|
||||||
// 批量订单:每个店铺的小费比例映射 merchantId -> tipPercent (小费比例,0-100)
|
|
||||||
const merchantTipPercentMap = ref<Record<string, number>>({})
|
|
||||||
|
|
||||||
function navigateToCoupon(merchantId?: string) {
|
function navigateToCoupon(merchantId?: string) {
|
||||||
const targetMerchantId = merchantId || storeId.value
|
const targetMerchantId = merchantId || storeId.value
|
||||||
@@ -1386,48 +1371,23 @@ function safeMoneyStr(val: unknown, fallback = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displayDeliveryFeeStr = computed(() => {
|
const displayDeliveryFeeStr = computed(() => {
|
||||||
const p: any = priceData.value;
|
return safeMoneyStr(pickCheckoutDeliveryFee(priceData.value as any, orderType.value === 'batch'), 0);
|
||||||
if (!p) return '0.00';
|
|
||||||
if (orderType.value === 'batch') {
|
|
||||||
return safeMoneyStr(p.totalDeliveryFee, 0);
|
|
||||||
}
|
|
||||||
return safeMoneyStr(p.deliveryFee, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayGoodsAmountStr = computed(() => {
|
const displayGoodsAmountStr = computed(() => {
|
||||||
const p: any = priceData.value;
|
return safeMoneyStr(pickCheckoutGoodsAmount(priceData.value as any, orderType.value === 'batch'), 0);
|
||||||
if (!p) return '0.00';
|
|
||||||
if (orderType.value === 'batch') {
|
|
||||||
return safeMoneyStr(p.totalActualAmount, 0);
|
|
||||||
}
|
|
||||||
return safeMoneyStr(p.actualAmount, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayTaxStr = computed(() => {
|
const displayTaxStr = computed(() => {
|
||||||
const p: any = priceData.value;
|
return safeMoneyStr(pickCheckoutTax(priceData.value as any, orderType.value === 'batch'), 0);
|
||||||
if (!p) return '0.00';
|
|
||||||
if (orderType.value === 'batch') {
|
|
||||||
return safeMoneyStr(p.totalTax, 0);
|
|
||||||
}
|
|
||||||
return safeMoneyStr(p.tax, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayTipStr = computed(() => {
|
const displayTipStr = computed(() => {
|
||||||
const p: any = priceData.value;
|
return safeMoneyStr(pickCheckoutTip(priceData.value as any, orderType.value === 'batch'), 0);
|
||||||
if (!p) return '0.00';
|
|
||||||
if (orderType.value === 'batch') {
|
|
||||||
return safeMoneyStr(p.totalTip, 0);
|
|
||||||
}
|
|
||||||
return safeMoneyStr(p.tip, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayPaidStr = computed(() => {
|
const displayPaidStr = computed(() => {
|
||||||
const p: any = priceData.value;
|
return safeMoneyStr(pickCheckoutPaidAmount(priceData.value as any, orderType.value === 'batch'), 0);
|
||||||
if (!p) return '0.00';
|
|
||||||
if (orderType.value === 'batch') {
|
|
||||||
return safeMoneyStr(p.totalPaidAmount, 0);
|
|
||||||
}
|
|
||||||
return safeMoneyStr(p.paidAmount, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const scheduledServiceEndLabel = computed(() => {
|
const scheduledServiceEndLabel = computed(() => {
|
||||||
@@ -1486,12 +1446,7 @@ const listDeliveryFeeAmount = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const actualDeliveryFeeNum = computed(() => {
|
const actualDeliveryFeeNum = computed(() => {
|
||||||
const p: any = priceData.value;
|
return pickCheckoutDeliveryFee(priceData.value as any, orderType.value === 'batch');
|
||||||
if (!p) return 0;
|
|
||||||
if (orderType.value === 'batch') {
|
|
||||||
return Number(p.totalDeliveryFee ?? 0);
|
|
||||||
}
|
|
||||||
return Number(p.deliveryFee ?? 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const showListDeliveryStrike = computed(() => {
|
const showListDeliveryStrike = computed(() => {
|
||||||
@@ -1525,17 +1480,10 @@ function handleClose(merchantId?: string) {
|
|||||||
circle-back
|
circle-back
|
||||||
custom-class="checkout-page-navbar"
|
custom-class="checkout-page-navbar"
|
||||||
/>
|
/>
|
||||||
<view
|
<view v-show="loading" class="checkout-loading-wrap">
|
||||||
class="animate-in fade-in animate-ease-out animate-duration-300"
|
<CheckoutSkeleton />
|
||||||
v-show="loading"
|
</view>
|
||||||
>
|
<view v-if="!loading" class="checkout-page">
|
||||||
<!-- 骨架屏 -->
|
|
||||||
<CheckoutSkeleton
|
|
||||||
/></view>
|
|
||||||
<view
|
|
||||||
class="checkout-page animate-in fade-in animate-ease-in animate-duration-300"
|
|
||||||
v-if="!loading"
|
|
||||||
>
|
|
||||||
<view class="checkout-page-stack">
|
<view class="checkout-page-stack">
|
||||||
<!-- 配送信息:主行(地址)+ 底部分栏(偏好 | 电话) -->
|
<!-- 配送信息:主行(地址)+ 底部分栏(偏好 | 电话) -->
|
||||||
<view class="checkout-block">
|
<view class="checkout-block">
|
||||||
@@ -1582,6 +1530,22 @@ function handleClose(merchantId?: string) {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 参数缺失时的兜底提示 -->
|
||||||
|
<view v-if="!orderType" class="checkout-block">
|
||||||
|
<view class="checkout-section-label">{{ t('pages-store.checkout.confirmOrder') }}</view>
|
||||||
|
<view class="checkout-card checkout-gutter py-48rpx">
|
||||||
|
<text class="block text-center text-28rpx text-#6d6d6d">{{ t('pages-store.checkout.missingParamsHint') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 单店模式且购物车为空 -->
|
||||||
|
<view class="checkout-block" v-if="orderType === 'normal' && cartDataList.length === 0">
|
||||||
|
<view class="checkout-section-label">{{ t('pages-store.checkout.confirmOrder') }}</view>
|
||||||
|
<view class="checkout-card checkout-gutter py-48rpx">
|
||||||
|
<text class="block text-center text-28rpx text-#6d6d6d">{{ t('pages-store.checkout.emptyCartHint') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 确认订单(单店) -->
|
<!-- 确认订单(单店) -->
|
||||||
<view v-if="orderType === 'normal' && cartDataList.length > 0" class="checkout-block">
|
<view v-if="orderType === 'normal' && cartDataList.length > 0" class="checkout-block">
|
||||||
<view class="checkout-section-label">{{ t('pages-store.checkout.confirmOrder') }}</view>
|
<view class="checkout-section-label">{{ t('pages-store.checkout.confirmOrder') }}</view>
|
||||||
@@ -1847,63 +1811,67 @@ function handleClose(merchantId?: string) {
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 小费(仅配送时显示) -->
|
</view>
|
||||||
<view v-if="deliveryMethod === 0" class="checkout-merchant-tip-block">
|
</view>
|
||||||
<text class="checkout-driver-tip-desc">{{
|
</view>
|
||||||
t('pages-store.checkout.driverTipNote')
|
</view>
|
||||||
}}</text>
|
|
||||||
<view class="flex items-center mb-20rpx">
|
|
||||||
<image
|
|
||||||
src="@img/chef/1335.png"
|
|
||||||
mode="aspectFill"
|
|
||||||
class="w-36rpx h-36rpx relative z-1"
|
|
||||||
/>
|
|
||||||
<view class="ml-20rpx text-26rpx lh-26rpx font-500 text-#333">
|
|
||||||
{{ t('pages-store.checkout.driverTip') }}
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="checkout-tip-grid checkout-tip-grid--merchant">
|
<!-- 批量模式:按配送日期选择小费 -->
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in tipOptions"
|
v-if="orderType === 'batch' && deliveryMethod === 0 && batchDeliveryDates.length > 0"
|
||||||
:key="index"
|
class="checkout-block"
|
||||||
@click="selectedTipChangeForMerchant(String(merchant.id), item)"
|
>
|
||||||
:class="[
|
<view class="checkout-section-label">{{ t('pages-store.checkout.driverTip') }}</view>
|
||||||
merchantTipIndexMap[String(merchant.id)] === item.value
|
<view
|
||||||
? 'checkout-tip-pill checkout-tip-pill--on'
|
v-for="dateKey in batchDeliveryDates"
|
||||||
: 'checkout-tip-pill',
|
:key="dateKey"
|
||||||
]"
|
class="checkout-card checkout-gutter checkout-tip-card mb-24rpx"
|
||||||
class="checkout-tip-cell checkout-tip-cell--merchant"
|
>
|
||||||
>
|
<text class="checkout-driver-tip-desc">
|
||||||
{{ item.label }}
|
{{ t('pages-store.checkout.tipByDeliveryDatePrefix') }}{{ formatDeliveryDateLabel(dateKey) }}
|
||||||
</view>
|
</text>
|
||||||
<view
|
<text class="checkout-driver-tip-desc checkout-driver-tip-desc--sub">{{
|
||||||
@click="selectedTipChangeForMerchant(String(merchant.id), { value: 0 })"
|
t('pages-store.checkout.driverTipNote')
|
||||||
:class="[
|
}}</text>
|
||||||
merchantTipIndexMap[String(merchant.id)] === 0
|
<view class="checkout-tip-grid checkout-tip-grid--merchant mt-20rpx">
|
||||||
? 'checkout-tip-pill checkout-tip-pill--on'
|
<view
|
||||||
: 'checkout-tip-pill',
|
v-for="(item, index) in tipOptions"
|
||||||
]"
|
:key="`${dateKey}-${index}`"
|
||||||
class="checkout-tip-cell checkout-tip-cell--merchant checkout-tip-cell--input"
|
@click="selectedTipChangeForDeliveryDate(dateKey, item)"
|
||||||
>
|
:class="[
|
||||||
<view class="px-10rpx center h-full w-full">
|
deliveryDateTipIndexMap[dateKey] === item.value
|
||||||
<wd-input
|
? 'checkout-tip-pill checkout-tip-pill--on'
|
||||||
no-border
|
: 'checkout-tip-pill',
|
||||||
use-prefix-slot
|
item.value === TIP_NEXT_TIME ? 'checkout-tip-pill--phrase' : '',
|
||||||
@blur="handleConfirmTipForMerchant(String(merchant.id))"
|
]"
|
||||||
custom-class="!center !text-24rpx !bg-transparent flex-1 !text-center"
|
class="checkout-tip-cell checkout-tip-cell--merchant"
|
||||||
placeholderStyle="font-size: 24rpx;color: #333; text-align: center;"
|
>
|
||||||
:placeholder="t('pages-store.checkout.other')"
|
{{ item.label }}
|
||||||
v-model="merchantDiyTipValueMap[String(merchant.id)]"
|
</view>
|
||||||
@confirm="handleConfirmTipForMerchant(String(merchant.id))"
|
<view
|
||||||
>
|
@click="selectedTipChangeForDeliveryDate(dateKey, { value: 0 })"
|
||||||
<template #suffix>
|
:class="[
|
||||||
<text v-if="merchantDiyTipValueMap[String(merchant.id)] && merchantDiyTipValueMap[String(merchant.id)].length > 0">$</text>
|
deliveryDateTipIndexMap[dateKey] === 0
|
||||||
</template>
|
? 'checkout-tip-pill checkout-tip-pill--on'
|
||||||
</wd-input>
|
: 'checkout-tip-pill',
|
||||||
</view>
|
]"
|
||||||
</view>
|
class="checkout-tip-cell checkout-tip-cell--merchant checkout-tip-cell--input checkout-tip-cell--input-span2"
|
||||||
</view>
|
>
|
||||||
|
<view class="px-10rpx center h-full w-full">
|
||||||
|
<wd-input
|
||||||
|
no-border
|
||||||
|
use-prefix-slot
|
||||||
|
@blur="handleConfirmTipForDeliveryDate(dateKey)"
|
||||||
|
custom-class="!center !text-24rpx !bg-transparent flex-1 !text-center"
|
||||||
|
placeholderStyle="font-size: 24rpx;color: #333; text-align: center;"
|
||||||
|
:placeholder="t('pages-store.checkout.other')"
|
||||||
|
v-model="deliveryDateDiyTipValueMap[dateKey]"
|
||||||
|
@confirm="handleConfirmTipForDeliveryDate(dateKey)"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<text v-if="deliveryDateDiyTipValueMap[dateKey] && deliveryDateDiyTipValueMap[dateKey].length > 0">$</text>
|
||||||
|
</template>
|
||||||
|
</wd-input>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -1955,6 +1923,7 @@ function handleClose(merchantId?: string) {
|
|||||||
selectedTipIndex === item.value
|
selectedTipIndex === item.value
|
||||||
? 'checkout-tip-pill checkout-tip-pill--on'
|
? 'checkout-tip-pill checkout-tip-pill--on'
|
||||||
: 'checkout-tip-pill',
|
: 'checkout-tip-pill',
|
||||||
|
item.value === TIP_NEXT_TIME ? 'checkout-tip-pill--phrase' : '',
|
||||||
]"
|
]"
|
||||||
class="checkout-tip-cell"
|
class="checkout-tip-cell"
|
||||||
>
|
>
|
||||||
@@ -1967,7 +1936,7 @@ function handleClose(merchantId?: string) {
|
|||||||
? 'checkout-tip-pill checkout-tip-pill--on'
|
? 'checkout-tip-pill checkout-tip-pill--on'
|
||||||
: 'checkout-tip-pill',
|
: 'checkout-tip-pill',
|
||||||
]"
|
]"
|
||||||
class="checkout-tip-cell checkout-tip-cell--input"
|
class="checkout-tip-cell checkout-tip-cell--input checkout-tip-cell--input-span2"
|
||||||
>
|
>
|
||||||
<view class="px-10rpx center w-full h-full">
|
<view class="px-10rpx center w-full h-full">
|
||||||
<wd-input
|
<wd-input
|
||||||
@@ -2202,6 +2171,15 @@ $checkout-card-radius: 24rpx;
|
|||||||
$checkout-border: #ebebeb;
|
$checkout-border: #ebebeb;
|
||||||
$checkout-gutter: 32rpx;
|
$checkout-gutter: 32rpx;
|
||||||
|
|
||||||
|
.checkout-root {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $checkout-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-loading-wrap {
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
.checkout-page {
|
.checkout-page {
|
||||||
padding-top: 16rpx;
|
padding-top: 16rpx;
|
||||||
padding-bottom: 32rpx;
|
padding-bottom: 32rpx;
|
||||||
@@ -2945,6 +2923,12 @@ $checkout-gutter: 32rpx;
|
|||||||
margin-bottom: 28rpx;
|
margin-bottom: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkout-driver-tip-desc--sub {
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-top: -16rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.checkout-tip-card {
|
.checkout-tip-card {
|
||||||
padding: 28rpx 28rpx 32rpx;
|
padding: 28rpx 28rpx 32rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -2956,7 +2940,7 @@ $checkout-gutter: 32rpx;
|
|||||||
|
|
||||||
.checkout-tip-grid {
|
.checkout-tip-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -2983,6 +2967,19 @@ $checkout-gutter: 32rpx;
|
|||||||
padding: 0 4rpx;
|
padding: 0 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkout-tip-cell--input-span2 {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-tip-pill--phrase {
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 8rpx 6rpx;
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.checkout-tip-grid .checkout-tip-pill {
|
.checkout-tip-grid .checkout-tip-pill {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,23 @@ const orderStatus = computed(() => {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 税费及其他费用:按订单维度展示,非承载订单 deliveryFee/tip 为 0 */
|
||||||
|
const taxesAndOtherFeesTotal = computed(() => {
|
||||||
|
const d = orderDetail.value
|
||||||
|
return (
|
||||||
|
(Number(d?.tax) || 0) +
|
||||||
|
(Number(d?.tip) || 0) +
|
||||||
|
(Number(d?.deliveryFee) || 0)
|
||||||
|
).toFixed(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
const orderDeliveryDateLabel = computed(() => {
|
||||||
|
const date = String((orderDetail.value as any)?.deliveryDate ?? '').trim()
|
||||||
|
if (!date) return ''
|
||||||
|
const d = dayjs(date)
|
||||||
|
return d.isValid() ? d.format('MM/DD/YYYY') : date
|
||||||
|
})
|
||||||
|
|
||||||
// ====== 为设计稿准备的数据结构(商品缩略卡/总件数/总价等)======
|
// ====== 为设计稿准备的数据结构(商品缩略卡/总件数/总价等)======
|
||||||
const orderDishList = computed(() => {
|
const orderDishList = computed(() => {
|
||||||
const list = orderDetail.value?.merchantOrderDishVoList as unknown as Array<any> | null | undefined
|
const list = orderDetail.value?.merchantOrderDishVoList as unknown as Array<any> | null | undefined
|
||||||
@@ -571,6 +588,12 @@ async function onVoucherImageUploaded(urls: unknown) {
|
|||||||
|
|
||||||
<view v-if="orderDetail?.receiveMethod === 1" class="pt-36rpx bg-white">
|
<view v-if="orderDetail?.receiveMethod === 1" class="pt-36rpx bg-white">
|
||||||
<view class="text-36rpx lh-36rpx text-#333 font-500 pl-30rpx mb-4rpx">{{ t('pages-store.order.deliveryAddress') }}</view>
|
<view class="text-36rpx lh-36rpx text-#333 font-500 pl-30rpx mb-4rpx">{{ t('pages-store.order.deliveryAddress') }}</view>
|
||||||
|
<view
|
||||||
|
v-if="orderDeliveryDateLabel"
|
||||||
|
class="text-28rpx lh-28rpx text-#6D6D6D pl-30rpx mb-16rpx"
|
||||||
|
>
|
||||||
|
{{ t('pages-store.order.deliveryDate') }}: {{ orderDeliveryDateLabel }}
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 收货地址 -->
|
<!-- 收货地址 -->
|
||||||
<view class="flex items-center border-bottom py-36rpx px-30rpx">
|
<view class="flex items-center border-bottom py-36rpx px-30rpx">
|
||||||
@@ -698,7 +721,7 @@ async function onVoucherImageUploaded(urls: unknown) {
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<text>${{ (Number(orderDetail?.tax) + Number(orderDetail?.tip) + Number(orderDetail?.deliveryFee)).toFixed(2) }}</text>
|
<text>${{ taxesAndOtherFeesTotal }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="flex-center-sb">
|
<view class="flex-center-sb">
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import type { MerchantAppointmentSlot } from '@/utils/deliverySchedule'
|
||||||
|
|
||||||
|
/** 小费选项:下次一定($0) */
|
||||||
|
export const TIP_NEXT_TIME = -1
|
||||||
|
|
||||||
|
export function formatDeliveryDateKey(startTime?: number | string | null): string {
|
||||||
|
if (startTime == null || startTime === '') return ''
|
||||||
|
const d = dayjs(Number(startTime))
|
||||||
|
return d.isValid() ? d.format('YYYY-MM-DD') : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDeliveryDateLabel(dateKey: string): string {
|
||||||
|
if (!dateKey) return ''
|
||||||
|
const d = dayjs(dateKey)
|
||||||
|
return d.isValid() ? d.format('MM/DD') : dateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectDeliveryDatesFromMerchants(
|
||||||
|
merchants: Array<{ id?: string | number }>,
|
||||||
|
appointmentMap: Record<string, MerchantAppointmentSlot>,
|
||||||
|
): string[] {
|
||||||
|
const set = new Set<string>()
|
||||||
|
merchants.forEach((m) => {
|
||||||
|
const dateKey = formatDeliveryDateKey(appointmentMap[String(m.id)]?.startTime)
|
||||||
|
if (dateKey) set.add(dateKey)
|
||||||
|
})
|
||||||
|
return Array.from(set).sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTipAmount(tipIndex: number, diyTip?: string): number {
|
||||||
|
const diy = String(diyTip ?? '').trim()
|
||||||
|
if (diy) return Number(diy) || 0
|
||||||
|
if (tipIndex === TIP_NEXT_TIME || tipIndex <= 0) return 0
|
||||||
|
return tipIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDeliveryDateTipMap(
|
||||||
|
dates: string[],
|
||||||
|
indexMap: Record<string, number>,
|
||||||
|
diyMap: Record<string, string>,
|
||||||
|
): Record<string, number> {
|
||||||
|
const map: Record<string, number> = {}
|
||||||
|
dates.forEach((date) => {
|
||||||
|
map[date] = resolveTipAmount(indexMap[date] ?? TIP_NEXT_TIME, diyMap[date])
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildReceiveMethod(deliveryMethodIndex: number): number {
|
||||||
|
return deliveryMethodIndex === 0 ? 1 : 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export function optionalAddressId(
|
||||||
|
receiveMethod: number,
|
||||||
|
addressId: string | number | undefined | null,
|
||||||
|
): string | undefined {
|
||||||
|
if (receiveMethod !== 1) return undefined
|
||||||
|
const id = String(addressId ?? '').trim()
|
||||||
|
return id || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringifyId(id: string | number | undefined | null): string | undefined {
|
||||||
|
const s = String(id ?? '').trim()
|
||||||
|
return s || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringifyCartIds(ids: Array<string | number>): string[] {
|
||||||
|
return ids.map((id) => String(id)).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SchedulePayload = {
|
||||||
|
startScheduledTime?: number
|
||||||
|
endScheduledTime?: number
|
||||||
|
startMap: Record<string, number>
|
||||||
|
endMap: Record<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendDeliveryScheduleFields(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
deliveryTimeType: number,
|
||||||
|
options: {
|
||||||
|
isBatch: boolean
|
||||||
|
storeId?: string
|
||||||
|
showDeliveryTime?: string
|
||||||
|
diyTime?: { startTime?: number; endTime?: number }
|
||||||
|
merchants: Array<{ id?: string | number }>
|
||||||
|
appointmentMap: Record<string, MerchantAppointmentSlot>
|
||||||
|
buildScheduledTimePayload: (
|
||||||
|
merchants: Array<{ id?: string | number }>,
|
||||||
|
map: Record<string, MerchantAppointmentSlot>,
|
||||||
|
) => SchedulePayload
|
||||||
|
getTimeStamps?: (timeStr: string) => { startTimestamp: number; endTimestamp: number }
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
body.deliveryType = deliveryTimeType
|
||||||
|
if (deliveryTimeType !== 2) return
|
||||||
|
|
||||||
|
if (options.isBatch) {
|
||||||
|
const scheduled = options.buildScheduledTimePayload(
|
||||||
|
options.merchants,
|
||||||
|
options.appointmentMap,
|
||||||
|
)
|
||||||
|
if (scheduled.startScheduledTime != null) {
|
||||||
|
body.startScheduledTime = scheduled.startScheduledTime
|
||||||
|
body.endScheduledTime = scheduled.endScheduledTime
|
||||||
|
}
|
||||||
|
if (Object.keys(scheduled.startMap).length > 0) {
|
||||||
|
body.merchantStartScheduledTimeMap = scheduled.startMap
|
||||||
|
body.merchantEndScheduledTimeMap = scheduled.endMap
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ap = options.appointmentMap[String(options.storeId ?? '')]
|
||||||
|
body.startScheduledTime = ap?.startTime ?? options.diyTime?.startTime
|
||||||
|
body.endScheduledTime = ap?.endTime ?? options.diyTime?.endTime
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumUniqueDeliveryDateFees(
|
||||||
|
list: unknown,
|
||||||
|
feeKey: 'groupDeliveryFee' | 'groupTip',
|
||||||
|
): number {
|
||||||
|
if (!Array.isArray(list)) return 0
|
||||||
|
const seen = new Set<string>()
|
||||||
|
let sum = 0
|
||||||
|
list.forEach((item: any) => {
|
||||||
|
const date = String(item?.deliveryDate ?? '').trim()
|
||||||
|
if (!date || seen.has(date)) return
|
||||||
|
seen.add(date)
|
||||||
|
sum += Number(item?.[feeKey] ?? 0) || 0
|
||||||
|
})
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickCheckoutDeliveryFee(price: Record<string, any> | null | undefined, isBatch: boolean): number {
|
||||||
|
if (!price) return 0
|
||||||
|
if (isBatch) {
|
||||||
|
if (price.groupDeliveryFee != null && price.groupDeliveryFee !== '') {
|
||||||
|
return Number(price.groupDeliveryFee) || 0
|
||||||
|
}
|
||||||
|
const fromList = sumUniqueDeliveryDateFees(price.deliveryDateSummaryList, 'groupDeliveryFee')
|
||||||
|
if (fromList > 0) return fromList
|
||||||
|
return Number(price.totalDeliveryFee ?? price.deliveryFee ?? 0) || 0
|
||||||
|
}
|
||||||
|
return Number(price.deliveryFee ?? 0) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickCheckoutTip(price: Record<string, any> | null | undefined, isBatch: boolean): number {
|
||||||
|
if (!price) return 0
|
||||||
|
if (isBatch) {
|
||||||
|
if (price.groupTip != null && price.groupTip !== '') {
|
||||||
|
return Number(price.groupTip) || 0
|
||||||
|
}
|
||||||
|
const fromList = sumUniqueDeliveryDateFees(price.deliveryDateSummaryList, 'groupTip')
|
||||||
|
if (fromList > 0) return fromList
|
||||||
|
return Number(price.totalTip ?? price.tip ?? 0) || 0
|
||||||
|
}
|
||||||
|
return Number(price.tip ?? 0) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickCheckoutPaidAmount(price: Record<string, any> | null | undefined, isBatch: boolean): number {
|
||||||
|
if (!price) return 0
|
||||||
|
if (isBatch) {
|
||||||
|
return Number(price.totalPaidAmount ?? price.paidAmount ?? 0) || 0
|
||||||
|
}
|
||||||
|
return Number(price.paidAmount ?? 0) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickCheckoutGoodsAmount(price: Record<string, any> | null | undefined, isBatch: boolean): number {
|
||||||
|
if (!price) return 0
|
||||||
|
if (isBatch) {
|
||||||
|
return Number(price.totalActualAmount ?? price.actualAmount ?? 0) || 0
|
||||||
|
}
|
||||||
|
return Number(price.actualAmount ?? 0) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickCheckoutTax(price: Record<string, any> | null | undefined, isBatch: boolean): number {
|
||||||
|
if (!price) return 0
|
||||||
|
if (isBatch) {
|
||||||
|
return Number(price.totalTax ?? price.tax ?? 0) || 0
|
||||||
|
}
|
||||||
|
return Number(price.tax ?? 0) || 0
|
||||||
|
}
|
||||||
@@ -112,11 +112,20 @@ function handleSubmit() {
|
|||||||
sideDishItemId: item.selectedIds![0],
|
sideDishItemId: item.selectedIds![0],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const count = Math.max(1, Math.floor(Number(addCartCount.value) || 1))
|
||||||
|
if (count > maxAddCartCount.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('common.prompt.stockInsufficient'),
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
appMerchantCartAddCartPost({
|
appMerchantCartAddCartPost({
|
||||||
body: {
|
body: {
|
||||||
merchantId: storeId.value,
|
merchantId: storeId.value,
|
||||||
dishId: dish.id,
|
dishId: dish.id,
|
||||||
count: 1,
|
count,
|
||||||
merchantCartSideDishBoList,
|
merchantCartSideDishBoList,
|
||||||
} as any,
|
} as any,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -147,11 +156,7 @@ function onAddCartClick() {
|
|||||||
}, 400);
|
}, 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hasSideDishes.value) {
|
openSpecPopup();
|
||||||
openSpecPopup();
|
|
||||||
} else {
|
|
||||||
addCart();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageScroll((e) => {
|
onPageScroll((e) => {
|
||||||
@@ -308,6 +313,20 @@ function handleShare() {
|
|||||||
const isSoldOut = computed(() => isSoldOutStock(dishDetailData.value?.stock))
|
const isSoldOut = computed(() => isSoldOutStock(dishDetailData.value?.stock))
|
||||||
|
|
||||||
const specPopupVisible = ref(false)
|
const specPopupVisible = ref(false)
|
||||||
|
const addCartCount = ref(1)
|
||||||
|
|
||||||
|
const maxAddCartCount = computed(() => {
|
||||||
|
const stock = dishDetailData.value?.stock
|
||||||
|
if (stock == null || stock === '') return 99
|
||||||
|
if (isSoldOutStock(stock)) return 1
|
||||||
|
const n = Number(stock)
|
||||||
|
if (!Number.isFinite(n) || n <= 0) return 99
|
||||||
|
return Math.floor(n)
|
||||||
|
})
|
||||||
|
|
||||||
|
function resetAddCartCount() {
|
||||||
|
addCartCount.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
const hasSideDishes = computed(() => {
|
const hasSideDishes = computed(() => {
|
||||||
const list = dishDetailData.value?.merchantSideDishVoList ?? [];
|
const list = dishDetailData.value?.merchantSideDishVoList ?? [];
|
||||||
@@ -386,6 +405,7 @@ const detailDisplayPrice = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function openSpecPopup() {
|
function openSpecPopup() {
|
||||||
|
resetAddCartCount()
|
||||||
const sideList = dishDetailData.value?.merchantSideDishVoList ?? [];
|
const sideList = dishDetailData.value?.merchantSideDishVoList ?? [];
|
||||||
for (const item of sideList) {
|
for (const item of sideList) {
|
||||||
if (
|
if (
|
||||||
@@ -1018,6 +1038,20 @@ function getStoreDetail() {
|
|||||||
</template>
|
</template>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="spec-popup__quantity">
|
||||||
|
<text class="spec-popup__quantity-label">{{ t('pages-store.store.quantity') }}</text>
|
||||||
|
<wd-input-number
|
||||||
|
v-model="addCartCount"
|
||||||
|
long-press
|
||||||
|
:min="1"
|
||||||
|
:max="maxAddCartCount"
|
||||||
|
:step="1"
|
||||||
|
:input-width="80"
|
||||||
|
button-size="56"
|
||||||
|
custom-class="!bg-transparent"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 底部确认按钮 -->
|
<!-- 底部确认按钮 -->
|
||||||
<view class="spec-popup__footer">
|
<view class="spec-popup__footer">
|
||||||
<view class="spec-popup__confirm-btn" @click="onSpecConfirm">
|
<view class="spec-popup__confirm-btn" @click="onSpecConfirm">
|
||||||
@@ -1712,6 +1746,20 @@ function getStoreDetail() {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spec-popup__quantity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 30rpx 8rpx;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-popup__quantity-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
.spec-popup__footer {
|
.spec-popup__footer {
|
||||||
padding: 20rpx 30rpx;
|
padding: 20rpx 30rpx;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ function onStoreBannerChange(e: { detail?: { current?: number } }) {
|
|||||||
storeBannerCurrent.value = e.detail?.current ?? 0
|
storeBannerCurrent.value = e.detail?.current ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 店铺简介(接口字段 description,入驻申请同名字段) */
|
||||||
|
const storeIntroText = computed(() => {
|
||||||
|
const detail = storeDetail.value as Record<string, unknown>
|
||||||
|
return String(detail?.description ?? '').trim()
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => storeDetail.value?.id,
|
() => storeDetail.value?.id,
|
||||||
() => {
|
() => {
|
||||||
@@ -707,9 +713,16 @@ function handleShare() {
|
|||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
<view v-else class="store-banner__img store-banner__img--empty" />
|
<view v-else class="store-banner__img store-banner__img--empty" />
|
||||||
|
<view
|
||||||
|
v-if="storeIntroText"
|
||||||
|
class="store-banner__overlay"
|
||||||
|
>
|
||||||
|
<text class="store-banner__intro">{{ storeIntroText }}</text>
|
||||||
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="storeBannerImages.length > 1"
|
v-if="storeBannerImages.length > 1"
|
||||||
class="store-banner__counter"
|
class="store-banner__counter"
|
||||||
|
:class="{ 'store-banner__counter--with-intro': storeIntroText }"
|
||||||
>
|
>
|
||||||
{{ storeBannerCurrent + 1 }}/{{ storeBannerImages.length }}
|
{{ storeBannerCurrent + 1 }}/{{ storeBannerImages.length }}
|
||||||
</view>
|
</view>
|
||||||
@@ -842,10 +855,10 @@ function handleShare() {
|
|||||||
<text class="dish-member-inner">{{ t('pages-store.store.members') }}: $ {{ item.memberPrice }}</text>
|
<text class="dish-member-inner">{{ t('pages-store.store.members') }}: $ {{ item.memberPrice }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="flex-1 min-w-0"></view>
|
<view v-else class="flex-1 min-w-0"></view>
|
||||||
<view class="dish-add-btn center shrink-0">
|
<view class="dish-add-btn shrink-0">
|
||||||
<image
|
<image
|
||||||
src="@img/chef/1285.png"
|
src="/static/app/images/add_cart.png"
|
||||||
class="w-20rpx h-20rpx"
|
class="dish-add-btn__icon"
|
||||||
></image>
|
></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -1083,11 +1096,39 @@ page {
|
|||||||
background: #e8e8e8;
|
background: #e8e8e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-banner__overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 56rpx 24rpx 24rpx;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(0, 0, 0, 0) 0%,
|
||||||
|
rgba(0, 0, 0, 0.55) 45%,
|
||||||
|
rgba(0, 0, 0, 0.78) 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-banner__intro {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 36rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
word-break: break-word;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.store-banner__counter {
|
.store-banner__counter {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 24rpx;
|
right: 24rpx;
|
||||||
bottom: 20rpx;
|
bottom: 20rpx;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
padding: 6rpx 16rpx;
|
padding: 6rpx 16rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
background: rgba(0, 0, 0, 0.45);
|
background: rgba(0, 0, 0, 0.45);
|
||||||
@@ -1096,6 +1137,11 @@ page {
|
|||||||
line-height: 28rpx;
|
line-height: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-banner__counter--with-intro {
|
||||||
|
bottom: auto;
|
||||||
|
top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.store-delivery-notice {
|
.store-delivery-notice {
|
||||||
background: #fff7ed;
|
background: #fff7ed;
|
||||||
border-bottom: 1rpx solid #ffe8cc;
|
border-bottom: 1rpx solid #ffe8cc;
|
||||||
@@ -1298,11 +1344,13 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dish-add-btn {
|
.dish-add-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dish-add-btn__icon {
|
||||||
width: 48rpx;
|
width: 48rpx;
|
||||||
height: 48rpx;
|
height: 48rpx;
|
||||||
border-radius: 50%;
|
display: block;
|
||||||
background: #14181b;
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dish-promo {
|
.dish-promo {
|
||||||
|
|||||||
@@ -70,28 +70,34 @@ onShow(()=> {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="bg-#F6F6F6">
|
<view class="balance-page bg-#F6F6F6">
|
||||||
<image src="@img/chef/107.png" class="w-full h-472rpx"></image>
|
|
||||||
<z-paging ref="paging" v-model="dataList" @query="queryList">
|
<z-paging ref="paging" v-model="dataList" @query="queryList">
|
||||||
<template #top>
|
<template #top>
|
||||||
<wd-navbar
|
<view class="balance-hero">
|
||||||
safeAreaInsetTop
|
<image
|
||||||
:fixed="true"
|
src="@img/chef/107.png"
|
||||||
:placeholder="true"
|
class="balance-hero__bg"
|
||||||
:bordered="false"
|
mode="aspectFill"
|
||||||
custom-class="!bg-transparent"
|
/>
|
||||||
@click-left="handleClickLeft"
|
<view class="balance-hero__content">
|
||||||
>
|
<wd-navbar
|
||||||
<template #title>
|
safeAreaInsetTop
|
||||||
<text class="text-34rpx text-#fff !font-400">{{ t('pages-user.balance.title') }}</text>
|
:fixed="true"
|
||||||
</template>
|
:placeholder="true"
|
||||||
<template #left>
|
:bordered="false"
|
||||||
<view class="shrink-0">
|
custom-class="balance-navbar !bg-transparent"
|
||||||
<view class="i-carbon:chevron-left text-50rpx text-#fff ml-[-10rpx]"></view>
|
@click-left="handleClickLeft"
|
||||||
</view>
|
>
|
||||||
</template>
|
<template #title>
|
||||||
</wd-navbar>
|
<text class="balance-navbar__title">{{ t('pages-user.balance.title') }}</text>
|
||||||
<view class="h-254rpx box z-9 mx-18rpx mt-18rpx px-40rpx py-52rpx">
|
</template>
|
||||||
|
<template #left>
|
||||||
|
<view class="shrink-0">
|
||||||
|
<view class="i-carbon:chevron-left balance-navbar__back ml-[-10rpx]" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</wd-navbar>
|
||||||
|
<view class="h-254rpx box z-9 mx-18rpx mt-18rpx px-40rpx py-52rpx">
|
||||||
<view @click="navigateTo('/pages/agreement/index?code=' + Agreement.BALANCE_EXPLANATION)" class="flex items-center">
|
<view @click="navigateTo('/pages/agreement/index?code=' + Agreement.BALANCE_EXPLANATION)" class="flex items-center">
|
||||||
<text class="text-28rpx text-#333">{{ t('pages-user.balance.my-balance') }}:</text>
|
<text class="text-28rpx text-#333">{{ t('pages-user.balance.my-balance') }}:</text>
|
||||||
<image src="@img/chef/108.png" class="w-28rpx h-28rpx shrink-0"></image>
|
<image src="@img/chef/108.png" class="w-28rpx h-28rpx shrink-0"></image>
|
||||||
@@ -101,6 +107,8 @@ onShow(()=> {
|
|||||||
<wd-button @click="navigateTo('/pages-user/pages/recharge/index')" custom-class="!min-w-160rpx !h-64rpx !rounded-20rpx !bg-#00A76D">{{ t('common.recharge') }}</wd-button>
|
<wd-button @click="navigateTo('/pages-user/pages/recharge/index')" custom-class="!min-w-160rpx !h-64rpx !rounded-20rpx !bg-#00A76D">{{ t('common.recharge') }}</wd-button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
<view class="px-18rpx">
|
<view class="px-18rpx">
|
||||||
<view class="bg-white rounded-36rpx overflow-hidden mt-18rpx py-30rpx">
|
<view class="bg-white rounded-36rpx overflow-hidden mt-18rpx py-30rpx">
|
||||||
@@ -142,6 +150,41 @@ onShow(()=> {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.balance-hero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-hero__bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 472rpx;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-hero__content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-navbar__title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
line-height: 34rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-navbar__back {
|
||||||
|
font-size: 50rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.balance-navbar .wd-navbar__title) {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
background: linear-gradient(198deg, #D0F4E8 -8%, #FFFFFF 37%, #FFFFFF 56%, #FFFFFF 76%);
|
background: linear-gradient(198deg, #D0F4E8 -8%, #FFFFFF 37%, #FFFFFF 56%, #FFFFFF 76%);
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import {
|
import {
|
||||||
appMerchantCartDeleteByMerchantIdPost,
|
appMerchantCartDeleteByMerchantIdPost,
|
||||||
appMerchantCartListMerchantPost,
|
appMerchantCartListMerchantPost,
|
||||||
appMerchantCartDeleteCartPost,
|
|
||||||
appMerchantCartCalculateSavingsPost,
|
|
||||||
appMerchantCartListByMerchantIdPost,
|
appMerchantCartListByMerchantIdPost,
|
||||||
|
appMerchantCartDeleteCartPost,
|
||||||
appMerchantCartAddCartByIdPost,
|
appMerchantCartAddCartByIdPost,
|
||||||
appMerchantCartAddCartPost,
|
appMerchantCartAddCartPost,
|
||||||
appMerchantDishDishIdGet,
|
appMerchantDishDishIdGet,
|
||||||
appMerchantDetailMerchantIdGet,
|
appMerchantDetailMerchantIdGet,
|
||||||
|
appMerchantOrderCalculatePriceCartBatchPost,
|
||||||
|
appUserAddressListPost,
|
||||||
} from '@/service';
|
} from '@/service';
|
||||||
|
import {
|
||||||
|
pickCheckoutDeliveryFee,
|
||||||
|
pickCheckoutGoodsAmount,
|
||||||
|
pickCheckoutPaidAmount,
|
||||||
|
stringifyCartIds,
|
||||||
|
} from '@/pages-store/pages/order/utils/checkout-order';
|
||||||
import {
|
import {
|
||||||
buildReservationTimeUrl,
|
buildReservationTimeUrl,
|
||||||
buildDayAppointmentSlot,
|
buildDayAppointmentSlot,
|
||||||
@@ -141,32 +148,97 @@ const selectedItemQty = computed(() =>
|
|||||||
selectedCartItems.value.reduce((n, it) => n + (Number(it.count) || 0), 0)
|
selectedCartItems.value.reduce((n, it) => n + (Number(it.count) || 0), 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 已选商品商品金额小计(不含运费等),用于与设计图「n件商品总共」对齐 */
|
function getCartLineUnitPrice(item: CartItem) {
|
||||||
|
return Number(getCartLineDisplayPrice(item)) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已选商品金额小计(不含运费/税费),与列表行价口径一致 */
|
||||||
const selectedGoodsSubtotal = computed(() => {
|
const selectedGoodsSubtotal = computed(() => {
|
||||||
let total = 0;
|
let total = 0
|
||||||
selectedCartItems.value.forEach((item) => {
|
selectedCartItems.value.forEach((item) => {
|
||||||
const base = Number(item.merchantDishVo?.discountPrice) || 0;
|
total += getCartLineUnitPrice(item) * (Number(item.count) || 0)
|
||||||
let side = 0;
|
})
|
||||||
item.sideDishList?.forEach((d: any) => {
|
return total.toFixed(2)
|
||||||
side += Number(d?.merchantSideDishItemVo?.price) || 0;
|
})
|
||||||
});
|
|
||||||
const line = base + side;
|
/** 优先使用批量算价返回的商品金额,与底部实付口径对齐 */
|
||||||
total += line * (Number(item.count) || 0);
|
const displayGoodsAmountStr = computed(() => {
|
||||||
});
|
const fromApi = pickCheckoutGoodsAmount(priceData.value as any, true)
|
||||||
return total.toFixed(2);
|
if (fromApi > 0) return fromApi.toFixed(2)
|
||||||
});
|
return selectedGoodsSubtotal.value
|
||||||
|
})
|
||||||
|
|
||||||
const deliveryUnifiedText = computed(() =>
|
const deliveryUnifiedText = computed(() =>
|
||||||
`${t("pages-user.cart.deliveryUnifiedPrefix")}${appDisplayName.value}${t("pages-user.cart.deliveryUnifiedSuffix")}`
|
`${t("pages-user.cart.deliveryUnifiedPrefix")}${appDisplayName.value}${t("pages-user.cart.deliveryUnifiedSuffix")}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemsTotalWithPriceText = computed(() =>
|
const displayDeliveryFeeStr = computed(() => {
|
||||||
`${priceData.value.totalQuantity ?? selectedItemQty.value}${t("pages-user.cart.itemsTotalWithPriceMiddle")}${selectedGoodsSubtotal.value}`
|
const fee = pickCheckoutDeliveryFee(priceData.value as any, true)
|
||||||
);
|
return fee > 0 ? fee.toFixed(2) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemsTotalWithPriceText = computed(() => {
|
||||||
|
const goodsPart = `${selectedItemQty.value}${t('pages-user.cart.itemsTotalWithPriceMiddle')}${displayGoodsAmountStr.value}`
|
||||||
|
const delivery = displayDeliveryFeeStr.value
|
||||||
|
if (!delivery) return goodsPart
|
||||||
|
return `${goodsPart},${t('pages-user.cart.deliveryFeeLine')} $${delivery}`
|
||||||
|
})
|
||||||
|
|
||||||
const merchantAppointmentMap = ref<Record<string, MerchantAppointmentSlot>>({});
|
const merchantAppointmentMap = ref<Record<string, MerchantAppointmentSlot>>({});
|
||||||
const merchantAppointmentDisplay = ref<Record<string, string>>({});
|
const merchantAppointmentDisplay = ref<Record<string, string>>({});
|
||||||
const selectingMerchantId = ref<string | null>(null);
|
const selectingMerchantId = ref<string | null>(null);
|
||||||
|
const defaultAddressId = ref('');
|
||||||
|
|
||||||
|
async function loadDefaultAddress() {
|
||||||
|
if (!userStore.isLogin) return;
|
||||||
|
try {
|
||||||
|
const res: any = await appUserAddressListPost({
|
||||||
|
params: { pageNum: 1, pageSize: 100 },
|
||||||
|
});
|
||||||
|
const rows = res?.rows ?? [];
|
||||||
|
if (rows.length > 0) {
|
||||||
|
defaultAddressId.value = String(rows[0].id);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCartBatchPriceBody(cartIds: string[]) {
|
||||||
|
const selectedMerchants = dataList.value.filter((m) =>
|
||||||
|
m.merchantCartVoList.some((item) => item.checked),
|
||||||
|
);
|
||||||
|
|
||||||
|
const startMap: Record<string, number> = {};
|
||||||
|
const endMap: Record<string, number> = {};
|
||||||
|
selectedMerchants.forEach((m) => {
|
||||||
|
const ap = merchantAppointmentMap.value[String(m.id)];
|
||||||
|
if (!ap?.startTime || !ap?.endTime) return;
|
||||||
|
startMap[String(m.id)] = Number(ap.startTime);
|
||||||
|
endMap[String(m.id)] = Number(ap.endTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
const body: Record<string, any> = {
|
||||||
|
cartIds: stringifyCartIds(cartIds),
|
||||||
|
receiveMethod: 1,
|
||||||
|
deliveryType: Object.keys(startMap).length > 0 ? 2 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const addressId = String(defaultAddressId.value || '').trim();
|
||||||
|
if (addressId) body.addressId = addressId;
|
||||||
|
|
||||||
|
if (body.deliveryType === 2) {
|
||||||
|
body.merchantStartScheduledTimeMap = startMap;
|
||||||
|
body.merchantEndScheduledTimeMap = endMap;
|
||||||
|
const firstMerchantId = Object.keys(startMap)[0];
|
||||||
|
if (firstMerchantId) {
|
||||||
|
body.startScheduledTime = startMap[firstMerchantId];
|
||||||
|
body.endScheduledTime = endMap[firstMerchantId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
function ensureDefaultMerchantAppointment(merchant: CartMerchant) {
|
function ensureDefaultMerchantAppointment(merchant: CartMerchant) {
|
||||||
const key = String(merchant.id);
|
const key = String(merchant.id);
|
||||||
@@ -211,6 +283,9 @@ useEventEmit(EventEnum.CHOOSE_APPOINTMENT_TIME, (data: any) => {
|
|||||||
"ddd, MM/DD"
|
"ddd, MM/DD"
|
||||||
);
|
);
|
||||||
selectingMerchantId.value = null;
|
selectingMerchantId.value = null;
|
||||||
|
if (selectedCartItems.value.length > 0) {
|
||||||
|
void calculatePrice();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -229,21 +304,36 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const calculatePrice = () => {
|
const calculatePrice = async () => {
|
||||||
calculatingPrice.value = true;
|
calculatingPrice.value = true;
|
||||||
const cartIds = selectedCartItems.value.map((item) => item.id);
|
const cartIds = selectedCartItems.value.map((item) => String(item.id));
|
||||||
appMerchantCartCalculateSavingsPost({
|
if (!cartIds.length) {
|
||||||
body: cartIds as any,
|
priceData.value = { totalPayPrice: 0, savings: 0 };
|
||||||
})
|
calculatingPrice.value = false;
|
||||||
.then((res) => {
|
return;
|
||||||
priceData.value = (res as any).data ?? {};
|
}
|
||||||
})
|
|
||||||
.catch((err) => {
|
try {
|
||||||
console.error("价格统计失败", err);
|
if (!defaultAddressId.value) {
|
||||||
})
|
await loadDefaultAddress();
|
||||||
.finally(() => {
|
}
|
||||||
calculatingPrice.value = false;
|
|
||||||
|
const res: any = await appMerchantOrderCalculatePriceCartBatchPost({
|
||||||
|
body: buildCartBatchPriceBody(cartIds) as any,
|
||||||
});
|
});
|
||||||
|
const data = res?.data ?? {};
|
||||||
|
priceData.value = {
|
||||||
|
...data,
|
||||||
|
totalPayPrice: pickCheckoutPaidAmount(data, true),
|
||||||
|
goodsAmount: pickCheckoutGoodsAmount(data, true),
|
||||||
|
deliveryFee: pickCheckoutDeliveryFee(data, true),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error("价格统计失败", err);
|
||||||
|
priceData.value = { totalPayPrice: 0, savings: 0 };
|
||||||
|
} finally {
|
||||||
|
calculatingPrice.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAllCheck = () => {
|
const toggleAllCheck = () => {
|
||||||
@@ -591,18 +681,6 @@ function openRecommendDish(dish: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 接口若返回运费/服务费(扩展字段)则展示 */
|
|
||||||
function feeLine(
|
|
||||||
origKey: string,
|
|
||||||
curKey: string
|
|
||||||
): { show: boolean; orig?: any; cur?: any } {
|
|
||||||
const d = priceData.value as any;
|
|
||||||
const orig = d[origKey];
|
|
||||||
const cur = d[curKey];
|
|
||||||
if (cur == null && orig == null) return { show: false };
|
|
||||||
return { show: true, orig, cur };
|
|
||||||
}
|
|
||||||
|
|
||||||
// onMounted(() => {
|
// onMounted(() => {
|
||||||
// loading.value = true;
|
// loading.value = true;
|
||||||
// getCartList();
|
// getCartList();
|
||||||
@@ -611,6 +689,7 @@ function feeLine(
|
|||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (userStore.isLogin) {
|
if (userStore.isLogin) {
|
||||||
userStore.getAppointmentTime();
|
userStore.getAppointmentTime();
|
||||||
|
void loadDefaultAddress();
|
||||||
// 从结算页返回时强制刷新购物车,避免已结算商品残留
|
// 从结算页返回时强制刷新购物车,避免已结算商品残留
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
getCartList({ showMultiStoreNotice: true });
|
getCartList({ showMultiStoreNotice: true });
|
||||||
@@ -727,45 +806,6 @@ async function getCartList(options?: { showMultiStoreNotice?: boolean }) {
|
|||||||
<text class="cart-summary-line1">{{
|
<text class="cart-summary-line1">{{
|
||||||
itemsTotalWithPriceText
|
itemsTotalWithPriceText
|
||||||
}}</text>
|
}}</text>
|
||||||
<view
|
|
||||||
v-if="
|
|
||||||
feeLine('originalDeliveryFee', 'deliveryFee').show
|
|
||||||
"
|
|
||||||
class="cart-fee-line"
|
|
||||||
>
|
|
||||||
<text>{{ t("pages-user.cart.deliveryFeeLine") }}</text>
|
|
||||||
<text
|
|
||||||
v-if="priceData.originalDeliveryFee != null"
|
|
||||||
class="cart-fee-strike"
|
|
||||||
>${{ priceData.originalDeliveryFee }}</text
|
|
||||||
>
|
|
||||||
<text class="cart-fee-cur"
|
|
||||||
>${{ priceData.deliveryFee }}</text
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
v-if="
|
|
||||||
feeLine('originalServiceFee', 'serviceFee').show
|
|
||||||
"
|
|
||||||
class="cart-fee-line"
|
|
||||||
>
|
|
||||||
<text>{{ t("pages-user.cart.serviceFeeLine") }}</text>
|
|
||||||
<text
|
|
||||||
v-if="priceData.originalServiceFee != null"
|
|
||||||
class="cart-fee-strike"
|
|
||||||
>${{ priceData.originalServiceFee }}</text
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
v-if="priceData.serviceFeeFree"
|
|
||||||
class="cart-fee-free"
|
|
||||||
>{{ t("pages-user.member.free") }}</text
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
v-else
|
|
||||||
class="cart-fee-cur"
|
|
||||||
>${{ priceData.serviceFee }}</text
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ function navigateTo(url: string) {
|
|||||||
<view @click="goBack" class="h-148rpx flex items-center justify-end pr-30rpx">
|
<view @click="goBack" class="h-148rpx flex items-center justify-end pr-30rpx">
|
||||||
<view class="w-212rpx h-64rpx bg-#F2F2F2 rounded-64rpx center">
|
<view class="w-212rpx h-64rpx bg-#F2F2F2 rounded-64rpx center">
|
||||||
<image
|
<image
|
||||||
src="@img/chef/1285.png"
|
src="/static/app/images/add_cart.png"
|
||||||
class="mr-16rpx w-24rpx h-24rpx shrink-0"
|
class="mr-16rpx w-24rpx h-24rpx shrink-0"
|
||||||
></image>
|
></image>
|
||||||
<text class="text-#333 text-28rpx lh-28rpx font-500">{{ t('pages-user.cart.addItems') }}</text>
|
<text class="text-#333 text-28rpx lh-28rpx font-500">{{ t('pages-user.cart.addItems') }}</text>
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { appMarketActivityListPost } from '@/service'
|
||||||
|
import { useConfigStore } from '@/store'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
const activityId = ref('')
|
||||||
|
const loading = ref(true)
|
||||||
|
const activity = ref<Record<string, any>>({})
|
||||||
|
|
||||||
|
const bannerSrc = computed(() => {
|
||||||
|
return activity.value.activityImageUrl || activity.value.activityImage || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const activityTitle = computed(() => {
|
||||||
|
return activity.value.activityName || t('pages-user.recharge.activityDetailTitle')
|
||||||
|
})
|
||||||
|
|
||||||
|
const activityContent = computed(() => {
|
||||||
|
const content = activity.value.activityContent
|
||||||
|
if (typeof content === 'string' && content.trim()) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
return `<p>${t('pages-user.recharge.activityPlaceholderDesc')}</p>`
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadActivityDetail() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await appMarketActivityListPost({})
|
||||||
|
const list = Array.isArray(res?.data) ? res.data : []
|
||||||
|
const matched = list.find((item) => String(item?.id) === activityId.value)
|
||||||
|
activity.value = matched || {}
|
||||||
|
} catch {
|
||||||
|
activity.value = {}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRecharge() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages-user/pages/recharge/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((query?: Record<string, string | undefined>) => {
|
||||||
|
activityId.value = String(query?.id ?? '')
|
||||||
|
loadActivityDetail()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="recharge-activity-page">
|
||||||
|
<navbar :title="t('pages-user.recharge.activityDetailTitle')" />
|
||||||
|
|
||||||
|
<view v-if="loading" class="recharge-activity-page__loading center py-120rpx text-28rpx text-#999">
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="recharge-activity-page__banner">
|
||||||
|
<image
|
||||||
|
v-if="bannerSrc"
|
||||||
|
:src="bannerSrc"
|
||||||
|
class="recharge-activity-page__banner-img"
|
||||||
|
mode="scaleToFill"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="recharge-activity-page__banner-placeholder"
|
||||||
|
>
|
||||||
|
{{ t('pages-user.recharge.activityDetailTitle') }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="px-30rpx pt-32rpx">
|
||||||
|
<view class="mt-32rpx mb-24rpx text-40rpx lh-48rpx text-#333 font-bold tracking-[.04em]">
|
||||||
|
{{ activityTitle }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="recharge-activity-page__card rounded-16rpx bg-white px-28rpx py-32rpx">
|
||||||
|
<view class="text-30rpx lh-30rpx text-#333 font-500 mb-24rpx">
|
||||||
|
{{ t('pages-user.recharge.activityRulesTitle') }}
|
||||||
|
</view>
|
||||||
|
<mp-html
|
||||||
|
selectable
|
||||||
|
:preview-img="false"
|
||||||
|
:show-img-menu="false"
|
||||||
|
:tag-style="{
|
||||||
|
div: 'white-space: pre-wrap;',
|
||||||
|
p: 'white-space: pre-wrap;color:#666;font-size:14px;line-height:1.7;',
|
||||||
|
img: 'width:100%;max-width:100%;height:auto;',
|
||||||
|
}"
|
||||||
|
:content="activityContent"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="h-160rpx" />
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]" />
|
||||||
|
|
||||||
|
<view class="recharge-activity-page__footer fixed bottom-0 left-0 right-0 px-30rpx pt-16rpx bg-#fff">
|
||||||
|
<wd-button
|
||||||
|
custom-class="!h-98rpx !text-30rpx !text-#fff !lh-98rpx !rounded-16rpx"
|
||||||
|
block
|
||||||
|
@click="handleRecharge"
|
||||||
|
>
|
||||||
|
{{ t('pages-user.recharge.activityRechargeBtn') }}
|
||||||
|
</wd-button>
|
||||||
|
<view :style="[configStore.iosSafeBottomPlaceholder]" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
page {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.recharge-activity-page__banner {
|
||||||
|
width: 100%;
|
||||||
|
height: 420rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-activity-page__banner-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-activity-page__banner-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-activity-page__footer {
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,155 +1,464 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {appUserCardSelectDefaultPost} from "@/service";
|
import Config from '@/config/index'
|
||||||
import useEventEmit from "@/hooks/useEventEmit";
|
import ChooseImage from '@/components/choose-image/choose-image.vue'
|
||||||
import {EventEnum} from "@/constant/enums";
|
import { appMarketActivityRechargePromotionGet, appUserCardSelectDefaultPost } from '@/service'
|
||||||
import { rechargeBalanceApi } from "@/pages-user/service";
|
import useEventEmit from '@/hooks/useEventEmit'
|
||||||
const { t } = useI18n();
|
import { EventEnum } from '@/constant/enums'
|
||||||
|
import { rechargeBalanceApi } from '@/pages-user/service'
|
||||||
|
|
||||||
const amount = ref()
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
const payMethodOptions = ref({
|
const PAY_METHOD_STRIPE = 1
|
||||||
payMethod: 1,
|
const PAY_METHOD_ZIP = 3
|
||||||
cardId: '',
|
|
||||||
cardNumber: '',
|
const amount = ref<string | number>('')
|
||||||
|
const payMethod = ref(PAY_METHOD_STRIPE)
|
||||||
|
const cardId = ref('')
|
||||||
|
const cardNumber = ref('')
|
||||||
|
const zipPayVoucher = ref('')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const rechargePromotion = ref<Record<string, any> | null>(null)
|
||||||
|
const voucherChooseRef = ref<InstanceType<typeof ChooseImage>>()
|
||||||
|
const promotionNoticeRef = ref<{ reset?: () => void }>()
|
||||||
|
const zellePayPath = Config.zellePayPath
|
||||||
|
|
||||||
|
const promotionTiers = computed(() => {
|
||||||
|
const list = rechargePromotion.value?.rechargeTierList
|
||||||
|
if (!Array.isArray(list)) return []
|
||||||
|
return [...list].sort(
|
||||||
|
(a, b) => (Number(a?.sort) || 0) - (Number(b?.sort) || 0),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleSubmit() {
|
function formatPromotionTier(tier: Record<string, any>) {
|
||||||
if(!amount.value) {
|
const recharge = String(tier.rechargeAmount ?? '0')
|
||||||
|
const gift = String(tier.giftAmount ?? '0')
|
||||||
|
if (locale.value === 'zh-Hans') {
|
||||||
|
return `充${recharge}送${gift}`
|
||||||
|
}
|
||||||
|
return `Deposit ${recharge}, get ${gift} bonus`
|
||||||
|
}
|
||||||
|
|
||||||
|
const promotionActivityName = computed(() => {
|
||||||
|
return (
|
||||||
|
rechargePromotion.value?.activityName ||
|
||||||
|
t('pages-user.recharge.activityDetailTitle')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const promotionNoticeTexts = computed(() => {
|
||||||
|
if (!promotionTiers.value.length) return []
|
||||||
|
return promotionTiers.value.map((tier) =>
|
||||||
|
`${promotionActivityName.value}:${formatPromotionTier(tier)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 横向无缝滚动:多条拼接,重复一遍避免循环跳变 */
|
||||||
|
const promotionNoticeScrollText = computed(() => {
|
||||||
|
const items = promotionNoticeTexts.value.filter(Boolean)
|
||||||
|
if (!items.length) return ''
|
||||||
|
const once = items.join(' ')
|
||||||
|
if (items.length === 1) return once
|
||||||
|
return `${once} ${once}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const promotionNoticeShouldScroll = computed(
|
||||||
|
() => promotionNoticeTexts.value.length > 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
function selectPayMethod(method: number) {
|
||||||
|
if (payMethod.value === method) return
|
||||||
|
payMethod.value = method
|
||||||
|
if (method === PAY_METHOD_STRIPE) {
|
||||||
|
zipPayVoucher.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVoucherUrl(payload: unknown): string {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
const first = payload[0]
|
||||||
|
return typeof first === 'string' ? first : ''
|
||||||
|
}
|
||||||
|
return typeof payload === 'string' ? payload : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUploadVoucher() {
|
||||||
|
voucherChooseRef.value?.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVoucherUploaded(urls: unknown) {
|
||||||
|
const url = normalizeVoucherUrl(urls)
|
||||||
|
if (!url) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('pages-store.order.voucherUploadFailed'),
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zipPayVoucher.value = url
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAmount(): number | null {
|
||||||
|
const val = Number(amount.value)
|
||||||
|
if (!amount.value && amount.value !== 0) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('pages-user.recharge.description'),
|
title: t('pages-user.recharge.description'),
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
})
|
})
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
if(amount.value <= 0) {
|
if (!Number.isFinite(val) || val <= 0) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('pages-user.recharge.amount-invalid'),
|
title: t('pages-user.recharge.amount-invalid'),
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
})
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const payAmount = validateAmount()
|
||||||
|
if (payAmount == null || submitting.value) return
|
||||||
|
|
||||||
|
if (payMethod.value === PAY_METHOD_STRIPE) {
|
||||||
|
if (!cardId.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('pages-user.recharge.pay-method'),
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await rechargeBalanceApi({
|
||||||
|
payAmount,
|
||||||
|
payMethod: PAY_METHOD_STRIPE,
|
||||||
|
cardId: cardId.value,
|
||||||
|
cardType: 1,
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: t('pages-user.recharge.success'),
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
setTimeout(() => uni.navigateBack(), 1000)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(!payMethodOptions.value.cardId) {
|
|
||||||
|
if (!zipPayVoucher.value) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('pages-user.recharge.pay-method'),
|
title: t('pages-user.recharge.voucherRequired'),
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rechargeBalanceApi({
|
|
||||||
payAmount: Number(amount.value),
|
submitting.value = true
|
||||||
cardId: payMethodOptions.value.cardId,
|
try {
|
||||||
cardType: 1
|
await rechargeBalanceApi({
|
||||||
}).then(res => {
|
payAmount,
|
||||||
console.log('充值余额', res)
|
payMethod: PAY_METHOD_ZIP,
|
||||||
|
zipPayVoucher: zipPayVoucher.value,
|
||||||
|
})
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('pages-user.recharge.success'),
|
title: t('pages-user.recharge.zipSubmitSuccess'),
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => uni.navigateBack(), 1200)
|
||||||
uni.navigateBack()
|
} finally {
|
||||||
}, 1000)
|
submitting.value = false
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateTo(url: string) {
|
function navigateTo(url: string) {
|
||||||
uni.navigateTo({ url })
|
uni.navigateTo({ url })
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(()=> {
|
async function loadRechargePromotion() {
|
||||||
// 查询用户默认信用卡
|
try {
|
||||||
appUserCardSelectDefault()
|
const res: any = await appMarketActivityRechargePromotionGet({})
|
||||||
})
|
rechargePromotion.value = res?.data ?? null
|
||||||
|
} catch {
|
||||||
|
rechargePromotion.value = null
|
||||||
|
} finally {
|
||||||
|
await nextTick()
|
||||||
|
promotionNoticeRef.value?.reset?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function appUserCardSelectDefault() {
|
function appUserCardSelectDefault() {
|
||||||
appUserCardSelectDefaultPost({}).then((res: any)=> {
|
appUserCardSelectDefaultPost({}).then((res: any) => {
|
||||||
console.log('查询用户默认信用卡', res)
|
cardId.value = res.data?.cardId || ''
|
||||||
payMethodOptions.value.cardId = res.data?.cardId || ''
|
cardNumber.value = res.data?.cardNumber || ''
|
||||||
payMethodOptions.value.cardNumber = res.data?.cardNumber || ''
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useEventEmit(EventEnum.CHOOSE_PAYMENT_METHOD, (data) => {
|
useEventEmit(EventEnum.CHOOSE_PAYMENT_METHOD, (data) => {
|
||||||
if(data) {
|
if (!data) return
|
||||||
payMethodOptions.value.cardId = data.cardId
|
cardId.value = data.cardId || ''
|
||||||
payMethodOptions.value.cardNumber = data.cardNumber
|
cardNumber.value = data.cardNumber || ''
|
||||||
payMethodOptions.value.payMethod = 1
|
payMethod.value = PAY_METHOD_STRIPE
|
||||||
}
|
zipPayVoucher.value = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
appUserCardSelectDefault()
|
||||||
|
void loadRechargePromotion()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="">
|
<view class="recharge-page">
|
||||||
<navbar :title="t('pages-user.recharge.title')"/>
|
<navbar :title="t('pages-user.recharge.title')" />
|
||||||
|
|
||||||
<view class="px-20rpx py-22rpx">
|
<view class="recharge-page__body">
|
||||||
<view class="bg-white rounded-12rpx p-28rpx">
|
<view v-if="promotionNoticeScrollText" class="recharge-promo-notice mx-30rpx mb-30rpx">
|
||||||
|
<wd-notice-bar
|
||||||
|
ref="promotionNoticeRef"
|
||||||
|
prefix="sound"
|
||||||
|
:text="promotionNoticeScrollText"
|
||||||
|
direction="horizontal"
|
||||||
|
:scrollable="promotionNoticeShouldScroll"
|
||||||
|
:delay="1"
|
||||||
|
:speed="60"
|
||||||
|
color="#00A76D"
|
||||||
|
background-color="#E8F8F1"
|
||||||
|
custom-class="recharge-promo-notice__bar"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bg-white rounded-12rpx p-28rpx mx-30rpx">
|
||||||
<view class="text-30rpx lh-30rpx text-#333 font-500 mb-30rpx">
|
<view class="text-30rpx lh-30rpx text-#333 font-500 mb-30rpx">
|
||||||
{{ t('pages-user.recharge.amount') }}
|
{{ t('pages-user.recharge.amount') }}
|
||||||
</view>
|
</view>
|
||||||
<view class="flex items-center bg-#F7F7F7 h-108rpx rounded-12rpx px-26rpx">
|
<view class="flex items-center bg-#F7F7F7 h-108rpx rounded-12rpx px-26rpx">
|
||||||
|
<text class="text-30rpx text-#333 mr-8rpx">$</text>
|
||||||
<wd-input
|
<wd-input
|
||||||
v-model="amount"
|
v-model="amount"
|
||||||
:focus-when-clear="false"
|
:focus-when-clear="false"
|
||||||
:placeholder="t('pages-user.recharge.description')"
|
:placeholder="t('pages-user.recharge.description')"
|
||||||
clearable
|
clearable
|
||||||
confirm-type="search"
|
custom-class="!text-30rpx !bg-transparent flex-1"
|
||||||
custom-class="!text-30rpx !bg-transparent flex-1"
|
no-border
|
||||||
no-border
|
placeholderStyle="font-size: 30rpx;color: #999;"
|
||||||
placeholderStyle="font-size: 30rpx;color: #999;"
|
type="digit"
|
||||||
type="number"
|
use-prefix-slot
|
||||||
use-prefix-slot
|
/>
|
||||||
>
|
|
||||||
</wd-input>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view class="bg-white rounded-12rpx p-28rpx mt-30rpx mx-30rpx">
|
||||||
@click="navigateTo('/pages-user/pages/choose-paymethod/index?hideWallet=true')"
|
<view class="text-30rpx lh-30rpx text-#333 font-500 mb-24rpx">
|
||||||
class="bg-white rounded-12rpx p-28rpx mt-30rpx flex-center-sb text-32rpx lh-32rpx font-500 text-#333"
|
{{ t('pages-user.choosePaymethod.title') }}
|
||||||
>
|
|
||||||
<view class="flex items-center">
|
|
||||||
<image
|
|
||||||
src="@img/chef/138.png"
|
|
||||||
class="w-44rpx h-44rpx mr-28rpx shrink-0"
|
|
||||||
mode="aspectFit"
|
|
||||||
/>
|
|
||||||
<text class="">
|
|
||||||
{{ t('pages-user.choosePaymethod.creditCard') }}
|
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="flex items-center">
|
|
||||||
<view class="mr-12px">
|
<view
|
||||||
<!--判断用户是否有信用卡-->
|
class="recharge-pay-option"
|
||||||
<text v-if="!payMethodOptions.cardId">{{ t("pages-user.member.creditCard") }}</text>
|
:class="{ 'recharge-pay-option--on': payMethod === PAY_METHOD_STRIPE }"
|
||||||
<text v-else>{{ payMethodOptions.cardNumber }}</text>
|
@click="selectPayMethod(PAY_METHOD_STRIPE)"
|
||||||
|
>
|
||||||
|
<view class="recharge-pay-option-dot">
|
||||||
|
<view v-if="payMethod === PAY_METHOD_STRIPE" class="recharge-pay-option-dot-inner" />
|
||||||
</view>
|
</view>
|
||||||
|
<image src="@img/chef/138.png" class="w-44rpx h-44rpx shrink-0" mode="aspectFit" />
|
||||||
|
<text class="flex-1 text-30rpx text-#333">{{ t('pages-user.recharge.payMethodStripe') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="payMethod === PAY_METHOD_STRIPE"
|
||||||
|
class="recharge-card-row"
|
||||||
|
@click="navigateTo('/pages-user/pages/choose-paymethod/index?hideWallet=true')"
|
||||||
|
>
|
||||||
|
<text class="text-28rpx text-#666">{{ t('pages-user.choosePaymethod.creditCard') }}</text>
|
||||||
|
<view class="flex items-center">
|
||||||
|
<text class="text-28rpx text-#333 mr-12rpx">
|
||||||
|
<template v-if="!cardId">{{ t('pages-user.member.creditCard') }}</template>
|
||||||
|
<template v-else>{{ cardNumber }}</template>
|
||||||
|
</text>
|
||||||
|
<image src="@img/chef/142.png" class="w-32rpx h-32rpx shrink-0" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="recharge-pay-option mt-20rpx"
|
||||||
|
:class="{ 'recharge-pay-option--on': payMethod === PAY_METHOD_ZIP }"
|
||||||
|
@click="selectPayMethod(PAY_METHOD_ZIP)"
|
||||||
|
>
|
||||||
|
<view class="recharge-pay-option-dot">
|
||||||
|
<view v-if="payMethod === PAY_METHOD_ZIP" class="recharge-pay-option-dot-inner" />
|
||||||
|
</view>
|
||||||
|
<text class="flex-1 text-30rpx text-#333">{{ t('pages-user.recharge.payMethodZip') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="payMethod === PAY_METHOD_ZIP" class="recharge-zip-block">
|
||||||
|
<text class="recharge-zip-hint">{{ t('pages-user.recharge.zipPayHint') }}</text>
|
||||||
<image
|
<image
|
||||||
src="@img/chef/142.png"
|
v-if="zellePayPath"
|
||||||
class="w-32rpx h-32rpx shrink-0"
|
:src="zellePayPath"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
|
class="recharge-zip-qr"
|
||||||
/>
|
/>
|
||||||
|
<view v-else class="recharge-zip-qr recharge-zip-qr--empty center text-24rpx text-#999">
|
||||||
|
QR
|
||||||
|
</view>
|
||||||
|
<wd-button
|
||||||
|
custom-class="!h-88rpx !rounded-16rpx !text-28rpx !bg-#14181B !mt-24rpx"
|
||||||
|
block
|
||||||
|
@click="openUploadVoucher"
|
||||||
|
>
|
||||||
|
{{ t('pages-user.recharge.uploadVoucher') }}
|
||||||
|
</wd-button>
|
||||||
|
<view v-if="zipPayVoucher" class="recharge-voucher-preview mt-24rpx">
|
||||||
|
<text class="text-26rpx text-#00A76D mb-12rpx block">{{
|
||||||
|
t('pages-user.recharge.voucherUploaded')
|
||||||
|
}}</text>
|
||||||
|
<image :src="zipPayVoucher" mode="aspectFill" class="recharge-voucher-img" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<fixed-bottom-large-btn
|
<fixed-bottom-large-btn
|
||||||
:text="t('common.recharge')"
|
:text="submitting ? t('common.loading') : t('common.recharge')"
|
||||||
class="z-100"
|
class="z-100"
|
||||||
:fixed="true"
|
:fixed="true"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<choose-image ref="voucherChooseRef" :count="1" @change="onVoucherUploaded" />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
page {
|
page {
|
||||||
background-color: #F5F5F5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
:deep(.wd-input__clear) {
|
.recharge-page {
|
||||||
background-color: transparent !important;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recharge-page__body {
|
||||||
|
padding-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-promo-notice {
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.recharge-promo-notice__bar) {
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 36rpx;
|
||||||
|
padding: 16rpx 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.recharge-promo-notice__bar .wd-notice-bar__prefix) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
line-height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.recharge-promo-notice__bar .wd-notice-bar__wrap) {
|
||||||
|
height: 36rpx;
|
||||||
|
line-height: 36rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.recharge-promo-notice__bar .wd-notice-bar__content) {
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 36rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-pay-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 24rpx 20rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
border: 2rpx solid #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-pay-option--on {
|
||||||
|
border-color: #14181b;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-pay-option-dot {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid #14181b;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-pay-option-dot-inner {
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #14181b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-card-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
margin-left: 56rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-zip-block {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-zip-hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-zip-qr {
|
||||||
|
width: 100%;
|
||||||
|
height: 420rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-zip-qr--empty {
|
||||||
|
height: 220rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-voucher-preview {
|
||||||
|
padding-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-voucher-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 280rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.wd-input__clear),
|
||||||
:deep(.wd-input__icon) {
|
:deep(.wd-input__icon) {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,32 +21,33 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
<view :change:prop="mapRenderjs.searchPlace" :prop="querySearch" class="bg-#f5f5f5">
|
<view class="bg-#f5f5f5">
|
||||||
<view class="px-22rpx py-20rpx">
|
<view class="px-22rpx py-20rpx">
|
||||||
<template v-if="placesLength === 0">
|
<view v-if="logicStore.searchLoading" class="center py-100rpx text-28rpx text-#9B9CA0">
|
||||||
<view class="center py-100rpx">
|
Loading...
|
||||||
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
|
</view>
|
||||||
|
|
||||||
|
<template v-else-if="placesLength > 0">
|
||||||
|
<view class="rounded-36rpx bg-white">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in logicStore.placesList"
|
||||||
|
:key="(item as any).id || index"
|
||||||
|
:class="[index === logicStore.placesList.length - 1 ? '' : 'border-bottom']"
|
||||||
|
class="px-22rpx pb-30rpx pt-34rpx"
|
||||||
|
@click="handleClickLocation(item as AddressPlaceItem)"
|
||||||
|
>
|
||||||
|
<view class="text-#000 text-26rpx font-bold">{{ (item as any).displayName }}</view>
|
||||||
|
<view class="text-#9B9CA0 text-24rpx flex-center-sb">
|
||||||
|
<view>{{ (item as any).formattedAddress }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<view v-else-if="hasSearched" class="center py-100rpx">
|
||||||
<view class="rounded-36rpx bg-white">
|
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
|
||||||
<template v-for="(item,index) in logicStore.placesList" :key="index">
|
</view>
|
||||||
<view :class="[index === logicStore.placesList.length - 1 ? '' : 'border-bottom']"
|
|
||||||
class="px-22rpx pb-30rpx pt-34rpx"
|
|
||||||
@click="handleClickLocation(item)">
|
|
||||||
<!-- <view >{{ item }}</view> -->
|
|
||||||
<view class="text-#000 text-26rpx font-bold">{{ item.displayName }}</view>
|
|
||||||
<view class="text-#9B9CA0 text-24rpx flex-center-sb">
|
|
||||||
<view>{{ item.formattedAddress }}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</view>
|
</view>
|
||||||
<view id="map" :change:prop="mapRenderjs.initMap" :prop="mapDataComputed" :user-location="userLocation"
|
|
||||||
class="absolute left-0 bottom-0 w-0 h-0"></view>
|
|
||||||
</view>
|
</view>
|
||||||
</z-paging>
|
</z-paging>
|
||||||
</template>
|
</template>
|
||||||
@@ -54,84 +55,245 @@
|
|||||||
import Config from "@/config";
|
import Config from "@/config";
|
||||||
import {useLogicStore} from "@/pages-user/store/logic";
|
import {useLogicStore} from "@/pages-user/store/logic";
|
||||||
import {useUserStore} from "@/store";
|
import {useUserStore} from "@/store";
|
||||||
import { getCurrentInstance } from "vue";
|
import {debounce} from "throttle-debounce";
|
||||||
|
|
||||||
const {t, locale} = useI18n();
|
const {t, locale} = useI18n();
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const logicStore = useLogicStore()
|
const logicStore = useLogicStore()
|
||||||
|
|
||||||
|
export interface AddressPlaceItem {
|
||||||
|
id: string
|
||||||
|
displayName: string
|
||||||
|
formattedAddress: string
|
||||||
|
location: { lat: number; lng: number } | null
|
||||||
|
}
|
||||||
|
|
||||||
let openerEventChannel: any = null
|
let openerEventChannel: any = null
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
// 统一通过 getOpenerEventChannel 获取导航传入的 eventChannel
|
|
||||||
const pages = getCurrentPages() as any[]
|
const pages = getCurrentPages() as any[]
|
||||||
const page = pages[pages.length - 1]
|
const page = pages[pages.length - 1]
|
||||||
openerEventChannel = page?.getOpenerEventChannel?.() || null
|
openerEventChannel = page?.getOpenerEventChannel?.() || null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 生命周期:清空地址列表
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
logicStore.clearPlacesList()
|
logicStore.clearPlacesList()
|
||||||
})
|
})
|
||||||
|
|
||||||
const placesLength = computed(() => {
|
const placesLength = computed(() => logicStore.placesList.length)
|
||||||
return logicStore.placesList.length;
|
const mapSearchKeyword = ref<string>('')
|
||||||
});
|
const hasSearched = ref(false)
|
||||||
|
|
||||||
const userLocation = computed(() => ({
|
|
||||||
longitude: userStore.location.longitude,
|
|
||||||
latitude: userStore.location.latitude
|
|
||||||
}));
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => logicStore.searchLoading,
|
() => logicStore.searchLoading,
|
||||||
(newValue) => {
|
(loading) => {
|
||||||
if (newValue) {
|
if (loading) {
|
||||||
uni.showLoading({
|
uni.showLoading({ title: 'Loading...', mask: true })
|
||||||
title: 'Loading...',
|
} else {
|
||||||
mask: true,
|
uni.hideLoading()
|
||||||
});
|
}
|
||||||
} else {
|
},
|
||||||
uni.hideLoading();
|
{ immediate: true },
|
||||||
}
|
)
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 地图搜索关键词
|
function resolveLanguageCode() {
|
||||||
const mapSearchKeyword = ref<string>('');
|
if (locale.value === 'zh-Hans') return 'zh-CN'
|
||||||
const querySearch = computed(() => {
|
if (locale.value === 'en') return 'en'
|
||||||
return {
|
return String(locale.value || 'en')
|
||||||
keyword: mapSearchKeyword.value,
|
}
|
||||||
|
|
||||||
|
function resolveRegionCode() {
|
||||||
|
return locale.value === 'zh-Hans' ? 'CN' : 'US'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBiasCenter() {
|
||||||
|
const lat = Number(userStore.location.latitude)
|
||||||
|
const lng = Number(userStore.location.longitude)
|
||||||
|
if (Number.isFinite(lat) && Number.isFinite(lng) && (lat !== 0 || lng !== 0)) {
|
||||||
|
return { latitude: lat, longitude: lng }
|
||||||
}
|
}
|
||||||
|
if (locale.value === 'zh-Hans') {
|
||||||
|
return { latitude: 39.9042, longitude: 116.4074 }
|
||||||
|
}
|
||||||
|
return { latitude: 38.8977, longitude: -77.0365 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePlacesApiPlace(place: any): AddressPlaceItem | null {
|
||||||
|
if (!place) return null
|
||||||
|
|
||||||
|
const lat = place.location?.latitude
|
||||||
|
const lng = place.location?.longitude
|
||||||
|
const formattedAddress = String(place.formattedAddress || '').trim()
|
||||||
|
const displayName = String(
|
||||||
|
typeof place.displayName === 'string'
|
||||||
|
? place.displayName
|
||||||
|
: place.displayName?.text || formattedAddress,
|
||||||
|
).trim()
|
||||||
|
|
||||||
|
if (!displayName && !formattedAddress) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(place.id || ''),
|
||||||
|
displayName: displayName || formattedAddress,
|
||||||
|
formattedAddress,
|
||||||
|
location:
|
||||||
|
Number.isFinite(Number(lat)) && Number.isFinite(Number(lng))
|
||||||
|
? { lat: Number(lat), lng: Number(lng) }
|
||||||
|
: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGeocodeResult(result: any): AddressPlaceItem | null {
|
||||||
|
if (!result) return null
|
||||||
|
|
||||||
|
const formattedAddress = String(result.formatted_address || '').trim()
|
||||||
|
const lat = result.geometry?.location?.lat
|
||||||
|
const lng = result.geometry?.location?.lng
|
||||||
|
const displayName =
|
||||||
|
formattedAddress.split(',')[0]?.trim() || formattedAddress
|
||||||
|
|
||||||
|
if (!formattedAddress) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(result.place_id || ''),
|
||||||
|
displayName,
|
||||||
|
formattedAddress,
|
||||||
|
location:
|
||||||
|
Number.isFinite(Number(lat)) && Number.isFinite(Number(lng))
|
||||||
|
? { lat: Number(lat), lng: Number(lng) }
|
||||||
|
: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestJson<T = any>(options: UniApp.RequestOptions) {
|
||||||
|
return new Promise<{ statusCode: number; data: T }>((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
timeout: 10000,
|
||||||
|
...options,
|
||||||
|
success: (res) => resolve(res as any),
|
||||||
|
fail: reject,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchByPlacesTextSearch(keyword: string) {
|
||||||
|
const languageCode = resolveLanguageCode()
|
||||||
|
const regionCode = resolveRegionCode()
|
||||||
|
const center = resolveBiasCenter()
|
||||||
|
|
||||||
|
const res = await requestJson<{ places?: any[]; error?: any }>({
|
||||||
|
url: 'https://places.googleapis.com/v1/places:searchText',
|
||||||
|
method: 'POST',
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Goog-Api-Key': Config.googleMapKey,
|
||||||
|
'X-Goog-FieldMask': 'places.id,places.displayName,places.formattedAddress,places.location',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
textQuery: keyword,
|
||||||
|
languageCode,
|
||||||
|
regionCode,
|
||||||
|
pageSize: 20,
|
||||||
|
locationBias: {
|
||||||
|
circle: {
|
||||||
|
center,
|
||||||
|
radius: 50000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
const message = (res.data as any)?.error?.message || `HTTP ${res.statusCode}`
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const places = Array.isArray(res.data?.places) ? res.data.places : []
|
||||||
|
return places.map(parsePlacesApiPlace).filter(Boolean) as AddressPlaceItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchByGeocodeFallback(keyword: string) {
|
||||||
|
const languageCode = resolveLanguageCode()
|
||||||
|
const regionCode = resolveRegionCode()
|
||||||
|
const query = encodeURIComponent(keyword)
|
||||||
|
const url =
|
||||||
|
`https://maps.googleapis.com/maps/api/geocode/json?address=${query}` +
|
||||||
|
`&key=${Config.googleMapKey}&language=${languageCode}®ion=${regionCode}`
|
||||||
|
|
||||||
|
const res = await requestJson<{ results?: any[]; status?: string }>({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode !== 200 || res.data?.status !== 'OK') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = Array.isArray(res.data.results) ? res.data.results : []
|
||||||
|
return results.map(parseGeocodeResult).filter(Boolean) as AddressPlaceItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchPlaces(keyword: string) {
|
||||||
|
logicStore.searchLoading = true
|
||||||
|
hasSearched.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
let list = await searchByPlacesTextSearch(keyword)
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
|
list = await searchByGeocodeFallback(keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
logicStore.setPlacesList(list)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('地址搜索失败', error)
|
||||||
|
try {
|
||||||
|
const fallback = await searchByGeocodeFallback(keyword)
|
||||||
|
logicStore.setPlacesList(fallback)
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error('Geocode 回退失败', fallbackError)
|
||||||
|
logicStore.setPlacesList([])
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
logicStore.searchLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedSearch = debounce(300, (keyword: string) => {
|
||||||
|
void searchPlaces(keyword)
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleClickLocation(item: any) {
|
watch(mapSearchKeyword, (keyword) => {
|
||||||
console.log('item', item)
|
const text = String(keyword || '').trim()
|
||||||
|
if (!text) {
|
||||||
|
hasSearched.value = false
|
||||||
|
logicStore.clearPlacesList()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
debouncedSearch(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleClickLocation(item: AddressPlaceItem) {
|
||||||
openerEventChannel?.emit?.('chooseAddress', item)
|
openerEventChannel?.emit?.('chooseAddress', item)
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用当前位置
|
|
||||||
function handleUseLocation() {
|
function handleUseLocation() {
|
||||||
// 检查是否获取到了当前定位
|
|
||||||
if (!userStore.location.longitude || !userStore.location.latitude) {
|
if (!userStore.location.longitude || !userStore.location.latitude) {
|
||||||
// 尝试再次获取定位
|
|
||||||
uni.getLocation({
|
uni.getLocation({
|
||||||
isHighAccuracy: true,
|
isHighAccuracy: true,
|
||||||
type: 'gcj02',
|
type: 'gcj02',
|
||||||
geocode: true,
|
geocode: true,
|
||||||
success: async (res) => {
|
success: (res) => {
|
||||||
getCityName(res.latitude, res.longitude)
|
getCityName(res.latitude, res.longitude)
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: () => {
|
||||||
const settings = uni.getAppAuthorizeSetting()
|
const settings = uni.getAppAuthorizeSetting()
|
||||||
console.log(settings)
|
|
||||||
if (settings.locationAuthorized === 'denied') {
|
if (settings.locationAuthorized === 'denied') {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('common.prompt.please-authorize-location-information'),
|
title: t('common.prompt.please-authorize-location-information'),
|
||||||
icon: 'none'
|
icon: 'none',
|
||||||
})
|
})
|
||||||
setTimeout(()=> {
|
setTimeout(() => {
|
||||||
uni.openAppAuthorizeSetting()
|
uni.openAppAuthorizeSetting()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -143,311 +305,75 @@ function handleUseLocation() {
|
|||||||
formattedAddress: userStore.location.formattedAddress || '',
|
formattedAddress: userStore.location.formattedAddress || '',
|
||||||
location: {
|
location: {
|
||||||
lng: userStore.location.longitude,
|
lng: userStore.location.longitude,
|
||||||
lat: userStore.location.latitude
|
lat: userStore.location.latitude,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCityName(latitude: number, longitude: number) {
|
function getCityName(latitude: number, longitude: number) {
|
||||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${Config.googleMapKey}&language=${locale.value === 'zh-Hans' ? 'zh-CN' : locale.value}`;
|
const languageCode = resolveLanguageCode()
|
||||||
|
const url =
|
||||||
|
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}` +
|
||||||
|
`&key=${Config.googleMapKey}&language=${languageCode}`
|
||||||
|
|
||||||
uni.request({
|
uni.request({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
success: (res: any) => {
|
success: (res: any) => {
|
||||||
const results = res.data?.results || [];
|
const results = res.data?.results || []
|
||||||
console.log('geocode results:', results);
|
|
||||||
|
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
return handleFail();
|
return handleFail()
|
||||||
}
|
}
|
||||||
|
|
||||||
const addr = results[0]; // 最高匹配度
|
const addr = results[0]
|
||||||
const components = addr.address_components || [];
|
const components = addr.address_components || []
|
||||||
|
|
||||||
// 提取城市名的工具函数
|
|
||||||
const pickAddress = (types: string[]) => {
|
const pickAddress = (types: string[]) => {
|
||||||
return components.find((item) => item.types.some((t) => types.includes(t)));
|
return components.find((item: any) => item.types.some((type: string) => types.includes(type)))
|
||||||
};
|
}
|
||||||
|
|
||||||
// 寻找城市名(多层兜底)
|
const cityObj =
|
||||||
let cityObj =
|
pickAddress(['locality']) ||
|
||||||
pickAddress(["locality"]) ||
|
pickAddress(['administrative_area_level_2']) ||
|
||||||
pickAddress(["administrative_area_level_2"]) ||
|
pickAddress(['administrative_area_level_1']) ||
|
||||||
pickAddress(["administrative_area_level_1"]) ||
|
pickAddress(['political'])
|
||||||
pickAddress(["political"]);
|
|
||||||
|
|
||||||
const cityName = cityObj?.long_name || null;
|
const cityName = cityObj?.long_name || null
|
||||||
|
const formattedAddress = addr.formatted_address || cityName || ''
|
||||||
// 从 Google 获取完整地址
|
|
||||||
const formattedAddress = addr.formatted_address || cityName || "";
|
|
||||||
|
|
||||||
console.log("city:", cityObj);
|
|
||||||
console.log("formattedAddress:", formattedAddress);
|
|
||||||
|
|
||||||
if (!cityName) {
|
if (!cityName) {
|
||||||
return handleFail();
|
return handleFail()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存入 store
|
|
||||||
userStore.location = {
|
userStore.location = {
|
||||||
location: cityName,
|
location: cityName,
|
||||||
formattedAddress,
|
formattedAddress,
|
||||||
longitude,
|
longitude,
|
||||||
latitude
|
latitude,
|
||||||
};
|
}
|
||||||
|
|
||||||
// 回传上一页(eventChannel)
|
|
||||||
openerEventChannel?.emit?.('chooseAddress', {
|
openerEventChannel?.emit?.('chooseAddress', {
|
||||||
displayName: cityName,
|
displayName: cityName,
|
||||||
formattedAddress,
|
formattedAddress,
|
||||||
location: { lng: longitude, lat: latitude }
|
location: { lng: longitude, lat: latitude },
|
||||||
});
|
})
|
||||||
|
|
||||||
uni.navigateBack();
|
uni.navigateBack()
|
||||||
},
|
},
|
||||||
|
fail: () => handleFail(),
|
||||||
fail: () => handleFail()
|
})
|
||||||
});
|
|
||||||
|
|
||||||
function handleFail() {
|
function handleFail() {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('common.prompt.getLocationFailed'),
|
title: t('common.prompt.getLocationFailed'),
|
||||||
icon: 'none'
|
icon: 'none',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDataComputed = computed(() => {
|
|
||||||
return {
|
|
||||||
language: locale.value,
|
|
||||||
lng: userStore.location.longitude || 174.7633,
|
|
||||||
lat: userStore.location.latitude || -36.8485,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
import { useLogicStore } from "@/pages-user/store/logic";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
methods: {
|
|
||||||
onMapSearchResult(places: any) {
|
|
||||||
const logicStore = useLogicStore()
|
|
||||||
console.log(places, '接收到的搜索结果')
|
|
||||||
|
|
||||||
if (places && places.length > 0) {
|
|
||||||
// 保持与原先一致的解析逻辑
|
|
||||||
const parsedPlaces = places
|
|
||||||
.map((placeArray: any) => {
|
|
||||||
const placeData = placeArray[0]?.[0]
|
|
||||||
if (!placeData) return null
|
|
||||||
|
|
||||||
const placeId = placeData[1]
|
|
||||||
const formattedAddress = placeData[8] || ''
|
|
||||||
const locationArray = placeData[11]
|
|
||||||
const displayNameArray = placeData[27]
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: placeId,
|
|
||||||
displayName: displayNameArray?.[0] || formattedAddress,
|
|
||||||
formattedAddress: formattedAddress,
|
|
||||||
location: locationArray
|
|
||||||
? {
|
|
||||||
lat: locationArray[0],
|
|
||||||
lng: locationArray[1],
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
console.log('解析后的地址列表', parsedPlaces)
|
|
||||||
logicStore.setPlacesList(parsedPlaces)
|
|
||||||
} else {
|
|
||||||
logicStore.setPlacesList([])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMapNotLoaded() {
|
|
||||||
uni.showToast({
|
|
||||||
title: 'Map is not loaded yet, please wait',
|
|
||||||
icon: 'none',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onMapSearchTimeout() {
|
|
||||||
uni.showToast({
|
|
||||||
title: 'Search timeout, please try again',
|
|
||||||
icon: 'none',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onMapSearchLoading(_isLoading: boolean) {
|
|
||||||
// 如需联动 loading,可在此处驱动 store
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<script lang="renderjs" module="mapRenderjs">
|
|
||||||
import {Loader} from "@googlemaps/js-api-loader"
|
|
||||||
import * as R from 'ramda'
|
|
||||||
import Config from '@/config'
|
|
||||||
import {debounce} from "throttle-debounce";
|
|
||||||
|
|
||||||
let map = null
|
|
||||||
let mapLoaded = false; // 地图加载完成标志
|
|
||||||
// @ts-ignore
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
region: "US",
|
|
||||||
lan: 'en',
|
|
||||||
google: null,
|
|
||||||
canvas: null,
|
|
||||||
center: null,
|
|
||||||
dotLottie: null,
|
|
||||||
marker: null,
|
|
||||||
markerViewList: [],
|
|
||||||
AdvancedMarkerElement: null,
|
|
||||||
lng: -77.0365,
|
|
||||||
lat: 38.8977,
|
|
||||||
userLocation: {
|
|
||||||
longitude: 0,
|
|
||||||
latitude: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
userLocation: Object
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
console.log('mounted mapRenderjs')
|
|
||||||
mapLoaded = false; // 重置地图加载完成标志
|
|
||||||
// !map&&this.initMap()
|
|
||||||
this.searchPlace = debounce(200, this.searchPlace)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
initMap({lng, lat, language}) {
|
|
||||||
console.log('initMap', lng, lat)
|
|
||||||
console.log('当前系统语言', language)
|
|
||||||
|
|
||||||
this.lan = language === 'zh-Hans' ? 'zh-CN' : 'en'
|
|
||||||
this.region = language === 'zh-Hans' ? 'CN' : 'US'
|
|
||||||
this.lng = language === 'zh-Hans' ? 116.4074 : -77.0365
|
|
||||||
this.lat = language === 'zh-Hans' ? 39.9042 : 38.8977
|
|
||||||
console.log('当前系统语言', this.lan)
|
|
||||||
console.log('当前系统区域', this.region)
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
const loader = new Loader({
|
|
||||||
apiKey: Config.googleMapKey,
|
|
||||||
version: "weekly",
|
|
||||||
region: this.region, // 设置为美国
|
|
||||||
language: this.lan,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
loader.load().then(async (google) => {
|
|
||||||
const {Map} = await google.maps.importLibrary("maps")
|
|
||||||
this.google = google
|
|
||||||
console.log('google', google.maps)
|
|
||||||
// 地图相关配置
|
|
||||||
// 指定自定义地图样式的 ID。
|
|
||||||
// center: 地图初始中心点的经纬度(lat, lng)。
|
|
||||||
// zoom: 地图初始缩放级别。
|
|
||||||
// fullscreenControl: 是否显示全屏按钮(false 表示不显示)。
|
|
||||||
// cameraControl: 是否显示相机控制(false 表示不显示)。
|
|
||||||
// disableDefaultUI: 是否禁用所有默认 UI 控件(true 表示全部禁用)。
|
|
||||||
// gestureHandling: 设置手势操作方式("greedy" 表示允许所有手势操作)
|
|
||||||
const mapOptions = {
|
|
||||||
mapId: 'ff2268c265ce7a40',
|
|
||||||
center: {
|
|
||||||
lat: this.lat,
|
|
||||||
lng: this.lng,
|
|
||||||
},
|
|
||||||
zoom: 12,
|
|
||||||
fullscreenControl: false,
|
|
||||||
cameraControl: false,
|
|
||||||
disableDefaultUI: true,
|
|
||||||
gestureHandling: "greedy",
|
|
||||||
};
|
|
||||||
|
|
||||||
map = new Map(document.getElementById("map"), mapOptions);
|
|
||||||
mapLoaded = true; // 地图加载完成
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async searchPlace({keyword}) {
|
|
||||||
console.log('搜索关键词', keyword);
|
|
||||||
if (!keyword) {
|
|
||||||
this.$ownerInstance.callMethod('onMapSearchResult', [])
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!mapLoaded) {
|
|
||||||
console.log('地图未加载完成,无法搜索');
|
|
||||||
this.$ownerInstance.callMethod('onMapNotLoaded');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!map || !this.google) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$ownerInstance.callMethod('onMapSearchLoading', true);
|
|
||||||
let timeoutId;
|
|
||||||
try {
|
|
||||||
// 设置搜索超时
|
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
this.$ownerInstance.callMethod('onMapSearchLoading', false);
|
|
||||||
reject(new Error('Search timeout'));
|
|
||||||
}, 10000); // 10 秒超时
|
|
||||||
});
|
|
||||||
|
|
||||||
const {Place, SearchByTextRankPreference} = await this.google.maps.importLibrary("places");
|
|
||||||
const request = {
|
|
||||||
textQuery: keyword,
|
|
||||||
fields: ['id', 'displayName', 'location', 'formattedAddress', 'addressComponents'],
|
|
||||||
locationBias: {lat: this.lat, lng: this.lng},
|
|
||||||
language: this.lan,
|
|
||||||
maxResultCount: 20,
|
|
||||||
region: this.region,
|
|
||||||
rankPreference: SearchByTextRankPreference.RELEVANCE
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchPromise = Place.searchByText(request);
|
|
||||||
const {places} = await Promise.race([searchPromise, timeoutPromise]);
|
|
||||||
console.log('地图搜索结果原始数据', places);
|
|
||||||
|
|
||||||
// 将 Google Places API 返回的对象转换为可序列化的数组格式
|
|
||||||
const serializedPlaces = places.map(place => {
|
|
||||||
// 提取需要的字段并转换为普通对象
|
|
||||||
return [[{
|
|
||||||
1: place.id,
|
|
||||||
8: place.formattedAddress || '',
|
|
||||||
9: place.addressComponents || [],
|
|
||||||
11: place.location ? [place.location.lat(), place.location.lng()] : null,
|
|
||||||
27: place.displayName ? [
|
|
||||||
typeof place.displayName === 'string' ? place.displayName : place.displayName.text,
|
|
||||||
place.displayName.languageCode || this.lan
|
|
||||||
] : null
|
|
||||||
}]];
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('序列化后的搜索结果', serializedPlaces);
|
|
||||||
this.$ownerInstance.callMethod('onMapSearchResult', serializedPlaces);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('搜索错误原因', e);
|
|
||||||
if (e.message === 'Search timeout') {
|
|
||||||
this.$ownerInstance.callMethod('onMapSearchTimeout');
|
|
||||||
}
|
|
||||||
this.$ownerInstance.callMethod('onMapSearchResult', []);
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
this.$ownerInstance.callMethod('onMapSearchLoading', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
page {
|
page {
|
||||||
|
|||||||
@@ -234,6 +234,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/recharge/index"
|
"path": "pages/recharge/index"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/recharge/activity-detail"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -290,6 +293,12 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"onReachBottomDistance": 80
|
"onReachBottomDistance": 80
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/group-catering/index"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/group-catering/detail"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ function subtitleLine(item: any): string {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 660rpx;
|
width: 560rpx;
|
||||||
height: 306rpx;
|
height: 306rpx;
|
||||||
min-height: 306rpx;
|
min-height: 306rpx;
|
||||||
margin-left: 16rpx;
|
margin-left: 16rpx;
|
||||||
@@ -162,7 +162,7 @@ function subtitleLine(item: any): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.featured-card__media {
|
.featured-card__media {
|
||||||
width: 323rpx;
|
width: 383rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -206,8 +206,8 @@ function subtitleLine(item: any): string {
|
|||||||
.featured-card__name {
|
.featured-card__name {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 28rpx;
|
font-size: 24rpx;
|
||||||
line-height: 34rpx;
|
line-height: 30rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
@@ -224,8 +224,8 @@ function subtitleLine(item: any): string {
|
|||||||
|
|
||||||
.featured-card__fee {
|
.featured-card__fee {
|
||||||
margin-top: 10rpx;
|
margin-top: 10rpx;
|
||||||
font-size: 24rpx;
|
font-size: 18rpx;
|
||||||
line-height: 30rpx;
|
line-height: 24rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #d48806;
|
color: #d48806;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { computed } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/** 外部列表(菜谱页等);首页不传则使用固定六项 */
|
/** 外部列表(菜谱页等);首页不传则使用固定七项 */
|
||||||
list: {
|
list: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -31,19 +31,9 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
const fixedTabs = [
|
const fixedTabs = [
|
||||||
{
|
{
|
||||||
id: 'member-zone',
|
id: 'fresh-seafood-today',
|
||||||
nameKey: 'pages.home.quickTabs.memberZone',
|
nameKey: 'pages.home.quickTabs.freshSeafoodToday',
|
||||||
logoUrl: '/static/app/images/home/huiyuanzhuanqu.png',
|
logoUrl: '/static/app/images/home/xiandahaixian.png',
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'live-seafood-air',
|
|
||||||
nameKey: 'pages.home.quickTabs.liveSeafoodAir',
|
|
||||||
logoUrl: '/static/app/images/home/kongyunhaixian.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'must-eat-list',
|
|
||||||
nameKey: 'pages.home.quickTabs.mustEatList',
|
|
||||||
logoUrl: '/static/app/images/home/bichibang.png',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new-calendar',
|
id: 'new-calendar',
|
||||||
@@ -51,9 +41,24 @@ const fixedTabs = [
|
|||||||
logoUrl: '/static/app/images/home/shangxinrili.png',
|
logoUrl: '/static/app/images/home/shangxinrili.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fresh-seafood-today',
|
id: 'member-zone',
|
||||||
nameKey: 'pages.home.quickTabs.freshSeafoodToday',
|
nameKey: 'pages.home.quickTabs.memberZone',
|
||||||
logoUrl: '/static/app/images/home/xiandahaixian.png',
|
logoUrl: '/static/app/images/home/huiyuanzhuanqu.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'must-eat-list',
|
||||||
|
nameKey: 'pages.home.quickTabs.mustEatList',
|
||||||
|
logoUrl: '/static/app/images/home/bichibang.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group-catering',
|
||||||
|
nameKey: 'pages.home.quickTabs.groupCatering',
|
||||||
|
logoUrl: '/static/app/images/home/tancanyuding.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'live-seafood-air',
|
||||||
|
nameKey: 'pages.home.quickTabs.liveSeafoodAir',
|
||||||
|
logoUrl: '/static/app/images/home/kongyunhaixian.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'energy-meal',
|
id: 'energy-meal',
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ function tabsTypeChange(id: string | number) {
|
|||||||
navigateTo('/pages-store/pages/energy-meal/index')
|
navigateTo('/pages-store/pages/energy-meal/index')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (topic === 'group-catering') {
|
||||||
|
navigateTo('/pages-store/pages/group-catering/index')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!isQuickTopicSlug(topic)) {
|
if (!isQuickTopicSlug(topic)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -314,8 +318,8 @@ function handleClickSwiper(item: any) {
|
|||||||
case 3: // 会员
|
case 3: // 会员
|
||||||
navigateTo('/pages-user/pages/member/index')
|
navigateTo('/pages-user/pages/member/index')
|
||||||
break
|
break
|
||||||
case 5: // 钱包
|
case 5: // 充值活动
|
||||||
navigateTo('/pages-user/pages/balance/index')
|
navigateTo('/pages-user/pages/recharge/activity-detail?id=' + item.id)
|
||||||
break
|
break
|
||||||
// case 4:
|
// case 4:
|
||||||
// navigateTo('/pages/ai/chat/index')
|
// navigateTo('/pages/ai/chat/index')
|
||||||
@@ -323,8 +327,6 @@ function handleClickSwiper(item: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const walletUrl = "pages-user/pages/balance/index";
|
|
||||||
|
|
||||||
function navigateToDishes(item: any) {
|
function navigateToDishes(item: any) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + item.merchantId,
|
url: '/pages-store/pages/store/dishes?id=' + item.id + '&storeId=' + item.merchantId,
|
||||||
@@ -427,7 +429,7 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
class="home-member-banner__btn"
|
class="home-member-banner__btn"
|
||||||
@click="navigateTo('/pages-user/pages/balance/index')"
|
@click="navigateTo('/pages-user/pages/recharge/index')"
|
||||||
>
|
>
|
||||||
<text class="home-member-banner__btn-text">{{ t('pages.home.recharge-now') }}</text>
|
<text class="home-member-banner__btn-text">{{ t('pages.home.recharge-now') }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -532,10 +534,10 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
<text class="featured-dish-member-inner">{{ t('pages-store.store.members') }}: US${{ item?.memberPrice }}</text>
|
<text class="featured-dish-member-inner">{{ t('pages-store.store.members') }}: US${{ item?.memberPrice }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="flex-1 min-w-0"></view>
|
<view v-else class="flex-1 min-w-0"></view>
|
||||||
<view class="featured-dish-add center shrink-0">
|
<view class="featured-dish-add shrink-0">
|
||||||
<image
|
<image
|
||||||
src="@img/chef/1285.png"
|
src="/static/app/images/add_cart.png"
|
||||||
class="w-28rpx h-28rpx"
|
class="featured-dish-add__icon"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -749,11 +751,13 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
.featured-dish-add {
|
.featured-dish-add {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-dish-add__icon {
|
||||||
width: 56rpx;
|
width: 56rpx;
|
||||||
height: 56rpx;
|
height: 56rpx;
|
||||||
border-radius: 50%;
|
display: block;
|
||||||
background: #14181b;
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 可选营销条(淡红底 + 文案) */
|
/* 可选营销条(淡红底 + 文案) */
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ const cover = computed(() => {
|
|||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="search-dish-item__add center" @click.stop="goDetail">
|
<view class="search-dish-item__add shrink-0" @click.stop="goDetail">
|
||||||
<image src="@img/chef/1285.png" class="search-dish-item__add-icon" mode="aspectFit" />
|
<image src="/static/app/images/add_cart.png" class="search-dish-item__add-icon" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -180,17 +180,13 @@ const cover = computed(() => {
|
|||||||
|
|
||||||
.search-dish-item__add {
|
.search-dish-item__add {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
width: 64rpx;
|
|
||||||
height: 64rpx;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 50%;
|
|
||||||
background: #f2f2f2;
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-dish-item__add-icon {
|
.search-dish-item__add-icon {
|
||||||
width: 30rpx;
|
width: 64rpx;
|
||||||
height: 30rpx;
|
height: 64rpx;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-dish-item__img-wrap {
|
.search-dish-item__img-wrap {
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import request from '@/http/vue-query';
|
||||||
|
import type { CustomRequestOptions } from '@/http/types';
|
||||||
|
|
||||||
|
export interface GroupMealMerchantVo {
|
||||||
|
id?: number | string;
|
||||||
|
merchantName?: string;
|
||||||
|
logo?: string;
|
||||||
|
merchantAddress?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupMealReservationVo {
|
||||||
|
id?: number | string;
|
||||||
|
merchantId?: number | string;
|
||||||
|
userId?: number | string;
|
||||||
|
peopleCount?: number;
|
||||||
|
perCapitaPrice?: number;
|
||||||
|
scene?: string;
|
||||||
|
contactName?: string;
|
||||||
|
contactPhone?: string;
|
||||||
|
expectedTime?: number;
|
||||||
|
status?: number;
|
||||||
|
handleRemark?: string;
|
||||||
|
cancelReason?: string;
|
||||||
|
remark?: string;
|
||||||
|
merchant?: GroupMealMerchantVo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupMealReservationSubmitBo {
|
||||||
|
merchantId: number | string;
|
||||||
|
peopleCount: number;
|
||||||
|
perCapitaPrice: number;
|
||||||
|
scene: string;
|
||||||
|
contactName: string;
|
||||||
|
contactPhone: string;
|
||||||
|
expectedTime: number;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupMealReservationMyListQueryBo {
|
||||||
|
merchantId?: number | string;
|
||||||
|
status?: number;
|
||||||
|
expectedBeginTime?: number;
|
||||||
|
expectedEndTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupMealReservationCancelBo {
|
||||||
|
id: number | string;
|
||||||
|
cancelReason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交团餐预定 POST /app/groupMealReservation/submit */
|
||||||
|
export async function appGroupMealReservationSubmitPost({
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
body: GroupMealReservationSubmitBo;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<{ data?: number | string }>('/app/groupMealReservation/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的团餐预定列表 POST /app/groupMealReservation/myList */
|
||||||
|
export async function appGroupMealReservationMyListPost({
|
||||||
|
params,
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
params: { pageNum: number; pageSize: number };
|
||||||
|
body?: GroupMealReservationMyListQueryBo;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<{ rows?: GroupMealReservationVo[]; total?: number }>(
|
||||||
|
'/app/groupMealReservation/myList',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
data: body ?? {},
|
||||||
|
...(options || {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的团餐预定详情 GET /app/groupMealReservation/{id} */
|
||||||
|
export async function appGroupMealReservationIdGet({
|
||||||
|
id,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
id: number | string;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<{ data?: GroupMealReservationVo }>(
|
||||||
|
`/app/groupMealReservation/${id}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消我的团餐预定 POST /app/groupMealReservation/cancel */
|
||||||
|
export async function appGroupMealReservationCancelPost({
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
body: GroupMealReservationCancelBo;
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
}) {
|
||||||
|
return request<Record<string, unknown>>('/app/groupMealReservation/cancel', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -45,3 +45,4 @@ export * from './userCoupon';
|
|||||||
export * from './marketingActivity';
|
export * from './marketingActivity';
|
||||||
export * from './marketActivity';
|
export * from './marketActivity';
|
||||||
export * from './energyMeal';
|
export * from './energyMeal';
|
||||||
|
export * from './groupMealReservation';
|
||||||
|
|||||||
@@ -16,3 +16,15 @@ export async function appMarketActivityListPost({
|
|||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 充值赠送活动 GET /app/marketActivity/rechargePromotion */
|
||||||
|
export async function appMarketActivityRechargePromotionGet({
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
options?: CustomRequestOptions;
|
||||||
|
} = {}) {
|
||||||
|
return request<API.R>('/app/marketActivity/rechargePromotion', {
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,15 +112,28 @@ export interface CalculatePriceCartBatchBo {
|
|||||||
*/
|
*/
|
||||||
receiveMethod?: number;
|
receiveMethod?: number;
|
||||||
/**
|
/**
|
||||||
* 小费比例
|
* 配送单小费(单商户算价/创单,固定金额)
|
||||||
*/
|
*/
|
||||||
tipDiscount?: number;
|
tip?: number;
|
||||||
|
/**
|
||||||
|
* 交付时间类型(1-立即交付 2-预约交付)
|
||||||
|
*/
|
||||||
|
deliveryType?: number;
|
||||||
|
/**
|
||||||
|
* 预约开始/结束时间(13 位毫秒时间戳,deliveryType=2 时)
|
||||||
|
*/
|
||||||
|
startScheduledTime?: number;
|
||||||
|
endScheduledTime?: number;
|
||||||
/**
|
/**
|
||||||
* 商户优惠券映射 { merchantId: couponId }
|
* 商户优惠券映射 { merchantId: couponId }
|
||||||
*/
|
*/
|
||||||
merchantCouponMap?: Record<string, number>;
|
merchantCouponMap?: Record<string, number>;
|
||||||
/**
|
/**
|
||||||
* 商户小费映射 { merchantId: tipAmount } (小费金额,单位:美元)
|
* 按配送日期的小费映射 { yyyy-MM-dd: tipAmount }
|
||||||
|
*/
|
||||||
|
deliveryDateTipMap?: Record<string, number>;
|
||||||
|
/**
|
||||||
|
* @deprecated 使用 deliveryDateTipMap
|
||||||
*/
|
*/
|
||||||
merchantTipMap?: Record<string, number>;
|
merchantTipMap?: Record<string, number>;
|
||||||
/**
|
/**
|
||||||
@@ -279,9 +292,13 @@ export interface CreateOrderCartBatchBo {
|
|||||||
*/
|
*/
|
||||||
endScheduledTime?: number | string;
|
endScheduledTime?: number | string;
|
||||||
/**
|
/**
|
||||||
* 小费比例
|
* 配送单小费(单商户算价/创单,固定金额)
|
||||||
*/
|
*/
|
||||||
tipDiscount?: number;
|
tip?: number;
|
||||||
|
/**
|
||||||
|
* 按配送日期的小费映射 { yyyy-MM-dd: tipAmount }
|
||||||
|
*/
|
||||||
|
deliveryDateTipMap?: Record<string, number>;
|
||||||
/**
|
/**
|
||||||
* 是否需要餐具(1-是 2-否)
|
* 是否需要餐具(1-是 2-否)
|
||||||
*/
|
*/
|
||||||
@@ -291,7 +308,7 @@ export interface CreateOrderCartBatchBo {
|
|||||||
*/
|
*/
|
||||||
merchantCouponMap?: Record<string, number>;
|
merchantCouponMap?: Record<string, number>;
|
||||||
/**
|
/**
|
||||||
* 商户小费映射 { merchantId: tipAmount } (小费金额,单位:美元)
|
* @deprecated 使用 deliveryDateTipMap
|
||||||
*/
|
*/
|
||||||
merchantTipMap?: Record<string, number>;
|
merchantTipMap?: Record<string, number>;
|
||||||
/**
|
/**
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 553 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Reference in New Issue
Block a user