diff --git a/src/locale/en.json b/src/locale/en.json
index ea2b2b3..13b241d 100644
--- a/src/locale/en.json
+++ b/src/locale/en.json
@@ -234,14 +234,15 @@
"featured-dishes": "Featured Dishes",
"nearby-merchants": "Nearby Merchants",
"open-member": "Become a Member",
- "recharge-now": "Recharge Now",
+ "recharge-now": "Deposit Now",
"quickTabs": {
- "memberZone": "Member Zone",
- "liveSeafoodAir": "Limited Live Seafood",
- "mustEatList": "CHEFLINK Must-Eat",
- "newCalendar": "New Arrival Calendar",
+ "memberZone": "Members' Picks",
+ "liveSeafoodAir": "Live Seafood – Limited Supply",
+ "mustEatList": "Must-Try List",
+ "newCalendar": "New Arrival",
"newCalendarNav": "Today's New",
- "freshSeafoodToday": "Fresh Seafood Today",
+ "freshSeafoodToday": "Day-Boat Fresh Catch",
+ "groupCatering": "Catering & Group Orders",
"energyMeal": "Energy Meals"
},
"mustEatListTabs": {
@@ -481,6 +482,8 @@
"deliveryInfo": "Delivery",
"driverTip": "Driver tip",
"driverTipNote": "100% of your tip goes to the driver.",
+ "tipByDeliveryDatePrefix": "Delivery date ",
+ "tipNextTime": "Maybe next time",
"fillAddressHint": "Add delivery address and contact",
"freeTag": "Free",
"localDelivery": "Local delivery",
@@ -488,6 +491,8 @@
"localDeliverySubtitleSuffix": ".",
"memberThanksPrefix": "Thanks for being a ",
"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",
"orderBreakdown": "Order details",
"pay": "Pay",
@@ -551,6 +556,7 @@
"cancellationTitle": "Merchant processing in progress",
"cancelled": "Cancelled",
"deliveryAddress": "Delivery address",
+ "deliveryDate": "Delivery date",
"deliveryPhotos": "Delivery of photos",
"deliveryTime": "Delivery time",
"beforeDeadline": " before",
@@ -596,8 +602,71 @@
"empty": "No energy meals yet",
"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": {
"addToCart": "Add to cart",
+ "quantity": "Quantity",
"appetizers": "Appetizers",
"merchantDiscounts": "Merchant discounts",
"claimNow": "Claim now",
@@ -845,10 +914,22 @@
"password-length-limit": "Password must be 8-16 characters long"
},
"recharge": {
+ "activityDetailTitle": "Recharge Promotion",
+ "activityPlaceholderDesc": "Promotion details are coming soon.",
+ "activityRechargeBtn": "Deposit Now",
+ "activityRulesTitle": "Promotion Rules",
"amount": "Recharge amount",
"amount-invalid": "The recharge amount cannot be less than 0",
"description": "Please enter your recharge amount",
"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",
"title": "Recharge Balance"
},
diff --git a/src/locale/zh-Hans.json b/src/locale/zh-Hans.json
index f845d35..60040c5 100644
--- a/src/locale/zh-Hans.json
+++ b/src/locale/zh-Hans.json
@@ -236,13 +236,14 @@
"open-member": "开通会员",
"recharge-now": "立即充值",
"quickTabs": {
- "memberZone": "会员专区",
- "liveSeafoodAir": "限量空运活海鲜",
- "mustEatList": "CHEFLINK必吃榜",
+ "memberZone": "会员精选",
+ "liveSeafoodAir": "限量活海鲜",
+ "mustEatList": "必吃榜单",
"newCalendar": "上新日历",
"newCalendarNav": "今日上新",
"freshSeafoodToday": "今日现打海鲜",
- "energyMeal": "能量餐"
+ "groupCatering": "团餐预定",
+ "energyMeal": "能量餐食"
},
"mustEatListTabs": {
"merchant": "商家榜单",
@@ -481,6 +482,8 @@
"deliveryInfo": "配送信息",
"driverTip": "司机小费",
"driverTipNote": "您所支付的小费 100% 归司机所有",
+ "tipByDeliveryDatePrefix": "配送日期 ",
+ "tipNextTime": "下次再说吧",
"fillAddressHint": "请填写您的送货地址及联系方式",
"freeTag": "免费",
"localDelivery": "本地配送",
@@ -488,6 +491,8 @@
"localDeliverySubtitleSuffix": " 统一配送!",
"memberThanksPrefix": "感谢您成为 ",
"memberThanksSuffix": " 会员。",
+ "missingParamsHint": "页面参数缺失,请返回购物车重新结算",
+ "emptyCartHint": "购物车暂无商品,请返回添加后再结算",
"orderTotalLine": "订单总计",
"orderBreakdown": "订单明细",
"pay": "付款",
@@ -551,6 +556,7 @@
"cancellationTitle": "商户处理中",
"cancelled": "已取消",
"deliveryAddress": "配送地址",
+ "deliveryDate": "配送日期",
"deliveryPhotos": "送达照片",
"deliveryTime": "送达时间",
"beforeDeadline": "前",
@@ -596,8 +602,71 @@
"empty": "暂无能量餐",
"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": {
"addToCart": "加入购物车",
+ "quantity": "数量",
"appetizers": "开胃菜",
"merchantDiscounts": "商家折扣",
"claimNow": "立即领取",
@@ -845,10 +914,22 @@
"password-length-limit": "密码长度必须为8-16位"
},
"recharge": {
+ "activityDetailTitle": "充值活动",
+ "activityPlaceholderDesc": "活动详情即将上线,敬请期待。",
+ "activityRechargeBtn": "立即充值",
+ "activityRulesTitle": "活动规则",
"amount": "充值金额",
"amount-invalid": "充值金额不能小于0",
"description": "请输入您的充值金额",
"pay-method": "请选择支付方式",
+ "payMethodStripe": "信用卡(Stripe)",
+ "payMethodZip": "ZIP 凭证支付",
+ "zipPayHint": "请扫码完成转账后,上传支付凭证截图",
+ "uploadVoucher": "上传支付凭证",
+ "voucherRequired": "请先上传支付凭证",
+ "voucherUploaded": "凭证已上传",
+ "zipSubmitSuccess": "已提交凭证,等待后台审核",
+ "promotionTier": "充{recharge}送{gift}",
"success": "充值成功",
"title": "余额充值"
},
diff --git a/src/manifest.json b/src/manifest.json
index 874148c..7ba565a 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -2,8 +2,8 @@
"name" : "CHEFLINK delivery",
"appid" : "__UNI__06509BE",
"description" : "",
- "versionName" : "3.2.5",
- "versionCode" : 325,
+ "versionName" : "3.2.9",
+ "versionCode" : 329,
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
diff --git a/src/pages-store/pages/dishes/components/featured-dish-topic-page.vue b/src/pages-store/pages/dishes/components/featured-dish-topic-page.vue
index 512037e..bf2ad61 100644
--- a/src/pages-store/pages/dishes/components/featured-dish-topic-page.vue
+++ b/src/pages-store/pages/dishes/components/featured-dish-topic-page.vue
@@ -160,8 +160,8 @@ defineExpose({
-
-
+
+
-
-
+
+
+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({})
+const cancelReason = ref('')
+
+const estimatedTotal = computed(() =>
+ formatGroupMealMoney(calcGroupMealEstimatedTotal(detail.value)),
+)
+
+const statusText = computed(() => {
+ const status = Number(detail.value.status)
+ const keyMap: Record = {
+ 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) => {
+ reservationId.value = String(query?.id ?? '')
+ loadDetail()
+})
+
+
+
+
+
+
+
+ {{ t('common.loading') }}
+
+
+
+
+
+
+
+ {{ detail.merchant?.merchantName || '--' }}
+
+ {{ statusText }}
+
+
+
+ {{ t('pages-store.groupCatering.fieldScene') }}
+ {{ detail.scene || '--' }}
+
+
+ {{ t('pages-store.groupCatering.fieldPeopleCount') }}
+ {{ detail.peopleCount ?? '--' }}
+
+
+ {{ t('pages-store.groupCatering.fieldPerCapitaPrice') }}
+ ${{ formatGroupMealMoney(Number(detail.perCapitaPrice) || 0) }}
+
+
+ {{ t('pages-store.groupCatering.fieldEstimatedTotal') }}
+ ${{ estimatedTotal }}
+
+
+ {{ t('pages-store.groupCatering.fieldExpectedTime') }}
+ {{ formatExpectedTime(detail.expectedTime) }}
+
+
+ {{ t('pages-store.groupCatering.fieldContactName') }}
+ {{ detail.contactName || '--' }}
+
+
+ {{ t('pages-store.groupCatering.fieldContactPhone') }}
+ {{ detail.contactPhone || '--' }}
+
+
+ {{ t('pages-store.groupCatering.fieldRemark') }}
+ {{ detail.remark }}
+
+
+ {{ t('pages-store.groupCatering.fieldHandleRemark') }}
+ {{ detail.handleRemark }}
+
+
+ {{ t('pages-store.groupCatering.fieldCancelReason') }}
+ {{ detail.cancelReason }}
+
+
+
+
+
+ {{ t('pages-store.groupCatering.fieldCancelReason') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-store/pages/group-catering/index.vue b/src/pages-store/pages/group-catering/index.vue
new file mode 100644
index 0000000..aa41da1
--- /dev/null
+++ b/src/pages-store/pages/group-catering/index.vue
@@ -0,0 +1,893 @@
+
+
+
+
+
+
+
+
+
+ {{ t('pages-store.groupCatering.tabSubmit') }}
+
+
+ {{ t('pages-store.groupCatering.tabList') }}
+
+
+
+
+
+
+ {{ t('pages-store.groupCatering.intro') }}
+
+
+
+
+ {{ t('pages-store.groupCatering.fieldMerchant') }}
+
+
+ {{ selectedMerchant?.merchantName || t('pages-store.groupCatering.selectMerchant') }}
+
+ ›
+
+
+
+
+ {{ t('pages-store.groupCatering.fieldPeopleCount') }}
+
+
+
+
+ {{ t('pages-store.groupCatering.fieldPerCapitaPrice') }}
+
+
+
+
+ {{ t('pages-store.groupCatering.fieldEstimatedTotal') }}
+ ${{ estimatedTotal }}
+
+
+
+ {{ t('pages-store.groupCatering.fieldScene') }}
+
+
+ {{ scene }}
+
+
+
+
+
+
+ {{ t('pages-store.groupCatering.fieldExpectedTime') }}
+
+
+ {{ expectedDate || t('pages-store.groupCatering.selectDate') }}
+
+
+ {{ expectedTime || t('pages-store.groupCatering.selectTime') }}
+
+
+ {{ expectedTimeText }}
+
+
+
+ {{ t('pages-store.groupCatering.fieldContactName') }}
+
+
+
+
+ {{ t('pages-store.groupCatering.fieldContactPhone') }}
+
+
+
+
+ {{ t('pages-store.groupCatering.fieldRemark') }}
+
+
+
+
+
+ {{ t('pages-store.groupCatering.tips') }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('common.loading') }}
+
+
+
+
+
+ {{ t('pages-store.groupCatering.listEmpty') }}
+
+
+ {{ t('pages-store.groupCatering.emptyAction') }}
+
+
+
+
+
+
+
+ {{ item.merchant?.merchantName || '--' }}
+ {{ getStatusText(item.status) }}
+
+
+ {{ t('pages-store.groupCatering.fieldPeopleCount') }}: {{ item.peopleCount ?? '--' }}
+ ${{ formatGroupMealMoney(Number(item.perCapitaPrice) || 0) }}
+
+
+ {{ t('pages-store.groupCatering.fieldEstimatedTotal') }}: ${{ formatGroupMealMoney(calcGroupMealEstimatedTotal(item)) }}
+
+
+ {{ formatTimestampWithMonthName(Number(item.expectedTime)) }}
+
+
+ {{ t('pages-store.groupCatering.viewAndCancel') }}
+
+
+
+
+
+ {{ t('common.loading') }}
+
+
+
+
+
+
+
+ {{ t('pages-store.groupCatering.selectMerchant') }}
+
+
+ {{ t('common.loading') }}
+
+
+ {{ item.merchantName }}
+
+
+ {{ t('pages-store.groupCatering.merchantEmpty') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages-store/pages/group-catering/utils.ts b/src/pages-store/pages/group-catering/utils.ts
new file mode 100644
index 0000000..1ab9171
--- /dev/null
+++ b/src/pages-store/pages/group-catering/utils.ts
@@ -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)
+}
diff --git a/src/pages-store/pages/list/index.vue b/src/pages-store/pages/list/index.vue
index 8758a27..1811b48 100644
--- a/src/pages-store/pages/list/index.vue
+++ b/src/pages-store/pages/list/index.vue
@@ -432,13 +432,13 @@ onShow(() => {
-
+
{
}
.cat-dish-add {
+ flex-shrink: 0;
+}
+
+.cat-dish-add__icon {
width: 56rpx;
height: 56rpx;
- border-radius: 50%;
- background: #14181b;
- box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
+ display: block;
}
.cat-dish-add--busy {
diff --git a/src/pages-store/pages/order/checkout.vue b/src/pages-store/pages/order/checkout.vue
index 3bb359d..84cf9a8 100644
--- a/src/pages-store/pages/order/checkout.vue
+++ b/src/pages-store/pages/order/checkout.vue
@@ -7,7 +7,7 @@ import CheckoutSkeleton from "./components/checkout-skeleton.vue";
import ChangePhone from "./components/change-phone.vue";
import PriceDetail from "./components/price-detail.vue";
import VisitMethod from "@/components/visit-method/index.vue";
-import {ref} from "vue";
+import { ref, computed, watch } from "vue";
import {
appMerchantCartListByMerchantIdPost,
appMerchantCartCalculateSavingsPost,
@@ -33,6 +33,60 @@ import {
loadCheckoutMerchantAppointments,
type MerchantAppointmentSlot,
} 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 {
+ try {
+ const pages = getCurrentPages()
+ const page = pages[pages.length - 1] as UniApp.PageInstance & {
+ $page?: { options?: Record }
+ options?: Record
+ }
+ 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) {
+ 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 configStore = useConfigStore();
const userStore = useUserStore();
@@ -282,19 +336,13 @@ function handleClickSegmented(index: number) {
// 折叠面板
const collapseValue = ref([""]);
-// 单个订单的小费(与设计稿一致:默认 $2)
-const selectedTipIndex = ref(2);
+// 小费选项:-1 表示「下次一定」(金额为 $0)
+// 单店小费
+const selectedTipIndex = ref(TIP_NEXT_TIME);
const diyTipValue = ref('')
-// 批量订单:每个店铺的小费索引映射 merchantId -> tipIndex
-const merchantTipIndexMap = ref>({})
-// 批量订单:每个店铺的自定义小费值映射 merchantId -> diyTipValue
-const merchantDiyTipValueMap = ref>({})
-
-const tipOptions = ref([
- { label: "$ 2", value: 2 },
- { label: "$ 3", value: 3 },
- { label: "$ 4", value: 4 },
-]);
+// 多店:按配送日期 yyyy-MM-dd 选择小费
+const deliveryDateTipIndexMap = ref>({})
+const deliveryDateDiyTipValueMap = ref>({})
// 单个订单的小费选择
function selectedTipChange(item: any) {
@@ -304,41 +352,80 @@ function selectedTipChange(item: any) {
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() {
- // 如果未填写其他小费金额,失去焦点时默认填充为 0
const val = String(diyTipValue.value || '').trim()
if (!val) {
diyTipValue.value = '0'
}
- // 使用自定义小费时,选中"其他"这一项
if (selectedTipIndex.value !== 0) {
selectedTipIndex.value = 0
}
void appMerchantOrderCalculatePriceCart()
}
-// 批量订单:确认某个店铺的自定义小费(金额,单位:美元)
-function handleConfirmTipForMerchant(merchantId: string) {
- const val = String(merchantDiyTipValueMap.value[merchantId] || '').trim()
+// 批量订单:按配送日期选择小费
+function selectedTipChangeForDeliveryDate(dateKey: string, item: any) {
+ 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) {
- merchantDiyTipValueMap.value[merchantId] = '0'
+ deliveryDateDiyTipValueMap.value[dateKey] = '0'
}
- // 使用自定义小费时,选中"其他"这一项
- if (merchantTipIndexMap.value[merchantId] !== 0) {
- merchantTipIndexMap.value[merchantId] = 0
+ if (deliveryDateTipIndexMap.value[dateKey] !== 0) {
+ deliveryDateTipIndexMap.value[dateKey] = 0
}
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 = {
+ 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);
// 切换配送周卡
@@ -464,8 +551,6 @@ const formData = ref({
receiveMethod: 0,
startScheduledTime: 0,
endScheduledTime: 0,
- tipDiscount: 0,
- weeklyDeliveryFee: 0,
})
// 是否需要餐具
const needTableware = ref(false)
@@ -478,16 +563,23 @@ async function safeAwait(label: string, task: () => Promise): Promise {
+
+// 页面加载状态
+const loading = ref(true);
+let bootstrapToken = 0
+
+async function initCheckoutPage(options?: Record) {
+ const token = ++bootstrapToken
loading.value = true
try {
+ const query = resolvePageQuery(options)
// 判断是批量下单还是普通下单
- if(options.type == 'batch' && options.cartIds) {
+ if (query.type === 'batch' && query.cartIds) {
// 批量下单模式
orderType.value = 'batch'
- batchCartIds.value = options.cartIds.split(',')
- console.log("下单类型",options.type);
+ batchCartIds.value = query.cartIds.split(',').filter(Boolean)
+ console.log('下单类型', query.type, 'cartIds', batchCartIds.value)
// 默认取用户信息中的手机号作为收货手机号
formData.value.phone = userStore.userInfo.phone || ''
@@ -510,13 +602,13 @@ onLoad(async (options: any)=> {
}
});
await safeAwait('appUserCardSelectDefault', appUserCardSelectDefault)
- } else if(options.storeId) {
+ } else if (query.storeId) {
// 普通下单模式
orderType.value = 'normal'
- storeId.value = options.storeId as string
- formData.value.orderRemark = options.orderRemark as string
+ storeId.value = query.storeId
+ formData.value.orderRemark = query.orderRemark
- needTableware.value = options.needTableware === 'true'
+ needTableware.value = query.needTableware === 'true'
// 默认取用户信息中的手机号作为收货手机号
formData.value.phone = userStore.userInfo.phone || ''
@@ -527,13 +619,26 @@ onLoad(async (options: any)=> {
await safeAwait('getCartInfo', getCartInfo)
await safeAwait('getStoreDetail', getStoreDetail)
await safeAwait('appUserCardSelectDefault', appUserCardSelectDefault)
+ } else {
+ console.warn('[checkout] missing page query', query)
}
} finally {
- loading.value = false
+ if (token === bootstrapToken) {
+ loading.value = false
+ }
}
+}
+
+function bootstrapCheckout(options?: Record) {
+ void initCheckoutPage(options)
+}
+
+onLoad((options: any) => {
+ bootstrapCheckout(options)
})
onShow(async ()=> {
+ if (!orderType.value) return
// 刷新地址,并在必要时重新计算价格
await safeAwait('getAddressList', getAddressList)
const hasCart =
@@ -545,9 +650,10 @@ onShow(async ()=> {
}
})
-// 页面加载状态
-const loading = ref(true);
onMounted(() => {
+ if (!orderType.value) {
+ bootstrapCheckout(getRouteQuery())
+ }
userStore.getUserInfo()
const currentHour = dayjs().hour();
if (currentHour >= 11 && currentHour <= 14) {
@@ -562,6 +668,28 @@ onMounted(() => {
});
const cartDataList = ref([])
+
+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() {
const res: any = await appMerchantCartListByMerchantIdPost({
params: {
@@ -631,10 +759,6 @@ async function getBatchCartInfo() {
cartDataList.value = results.filter(item => item !== null);
console.log('批量模式-最终购物车数据', cartDataList.value);
cartDataList.value.forEach((m: any) => {
- const id = String(m.id);
- if (merchantTipIndexMap.value[id] === undefined) {
- merchantTipIndexMap.value[id] = 2;
- }
ensureDefaultMerchantAppointment(m);
});
@@ -729,95 +853,29 @@ async function getStoreDetail() {
const priceData = ref({})
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)
-
- // 批量模式使用批量计算价格接口
+ if (!cartIds.length) return
+
if(orderType.value == 'batch') {
- // 构建 merchantCouponMap: { merchantId: couponId }
- const couponMap: Record = {}
+ const couponMap: Record = {}
Object.keys(merchantCouponMap.value).forEach(merchantId => {
- if(merchantCouponMap.value[merchantId]?.id) {
- couponMap[merchantId] = merchantCouponMap.value[merchantId].id
- }
- })
-
- // 构建 merchantTipMap: { merchantId: tipAmount } (小费金额,单位:美元)
- const tipMap: Record = {}
- 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 startMap: Record = {}
- const endMap: Record = {}
- 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 couponId = merchantCouponMap.value[merchantId]?.id
+ if (couponId) couponMap[merchantId] = String(couponId)
})
- 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,
- }
- })
+ const body = buildCheckoutRequestBase(true)
+ body.merchantCouponMap = couponMap
+ body.deliveryDateTipMap = buildBatchDeliveryDateTipMap()
+
+ const res: any = await appMerchantOrderCalculatePriceCartBatchPost({ body })
console.log('批量购物车下单价格', res)
priceData.value = res.data
} else {
- // 普通模式使用单店铺计算价格接口
- // 计算小费金额(美元)
- let tipAmount = 0
- 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({
- 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,
- }
- })
+ const body = buildCheckoutRequestBase(false)
+ body.couponId = couponInfo.value?.id ? String(couponInfo.value.id) : undefined
+ body.tip = buildSingleTipAmount()
+
+ const res: any = await appMerchantOrderCalculatePriceCartPost({ body })
console.log('购物车下单价格', res)
priceData.value = res.data
}
@@ -896,14 +954,15 @@ const passwordInputRef = ref(null)
const resOrderId = ref('')
const resOrderIds = ref([]) // 批量下单返回的订单ID列表
function handleGoSettle() {
- // 优先使用新选择的地址id
- const targetAddressId = selectedAddressId.value || currentAddressId.value
- if(!targetAddressId) {
- uni.showToast({
- title: t('pages-store.checkout.pleaseSelectAddress'),
- icon: 'none'
- })
- return
+ if (deliveryMethod.value === 0) {
+ const targetAddressId = selectedAddressId.value || currentAddressId.value
+ if (!targetAddressId) {
+ uni.showToast({
+ title: t('pages-store.checkout.pleaseSelectAddress'),
+ icon: 'none',
+ })
+ return
+ }
}
if(payMethodOptions.value.payMethod === 1 && !payMethodOptions.value.cardId) {
uni.showToast({
@@ -940,65 +999,21 @@ function handleGoSettle() {
// 批量下单
if(orderType.value == 'batch') {
if(resOrderIds.value.length === 0) {
- // 构建 merchantCouponMap: { merchantId: couponId }
- const couponMap: Record = {}
+ const couponMap: Record = {}
Object.keys(merchantCouponMap.value).forEach(merchantId => {
- if(merchantCouponMap.value[merchantId]?.id) {
- couponMap[merchantId] = merchantCouponMap.value[merchantId].id
- }
+ const couponId = merchantCouponMap.value[merchantId]?.id
+ if (couponId) couponMap[merchantId] = String(couponId)
})
-
- // 构建 merchantTipMap: { merchantId: tipAmount } (小费金额,单位:美元)
- const tipMap: Record = {}
- 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 = {
- addressId: targetAddressId,
- phone: formData.value.phone,
- areaCode: contact.value.areaCode,
- cartIds: batchCartIds.value,
+ ...buildCheckoutRequestBase(true),
merchantCouponMap: couponMap,
- merchantTipMap: tipMap,
- deliveryMethod: visitMethod.value.label, // 派送方式(如放门口或者交到顾客手中)
- deliveryType: deliveryTimeType.value, // 1-立即交付 2-预约交付
+ deliveryDateTipMap: buildBatchDeliveryDateTipMap(),
+ deliveryMethod: visitMethod.value.label,
orderRemark: formData.value.orderRemark,
- receiveMethod: deliveryMethod.value === 0 ? 1 : 2, // 收货方式(1-派送 2-自取)
- needTableware: needTableware.value ? 1 : 2, // 餐具 1是 2否
+ needTableware: needTableware.value ? 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)
appMerchantOrderCreateOrderCartBatchPost({
body: data
@@ -1034,41 +1049,15 @@ function handleGoSettle() {
} else {
// 普通下单
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 = {
- addressId: targetAddressId,
- phone: formData.value.phone,
- areaCode: contact.value.areaCode,
- cartIds: cartDataList.value.map(item => item.id),
- couponId: couponInfo.value ? couponInfo.value.id : '',
- deliveryMethod: visitMethod.value.label, // 派送方式(如放门口或者交到顾客手中)
- deliveryType: deliveryTimeType.value, // 1-立即交付 2-预约交付
+ ...buildCheckoutRequestBase(false),
+ couponId: couponInfo.value?.id ? String(couponInfo.value.id) : undefined,
+ deliveryMethod: visitMethod.value.label,
orderRemark: formData.value.orderRemark,
- receiveMethod: deliveryMethod.value === 0 ? 1 : 2, // 收货方式(1-派送 2-自取)
- tipDiscount: tipAmount, // 小费金额(美元),自取订单不需要小费
- // weeklyDeliveryFee: isWeeklyDelivery.value ? 1 : 2, // 是否支付周配送费(1-是 2-否)
- needTableware: needTableware.value ? 1 : 2, // 餐具 1是 2否
+ tip: buildSingleTipAmount(),
+ needTableware: needTableware.value ? 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)
appMerchantOrderCreateOrderCartPost({
body: data
@@ -1294,9 +1283,9 @@ export interface CreateOrderCartBo {
*/
endScheduledTime?: number;
/**
- * 小费比例
+ * 配送单小费(固定金额)
*/
- tipDiscount?: number;
+ tip?: number;
/**
* 是否支付周配送费(1-是 2-否)
*/
@@ -1308,10 +1297,6 @@ export interface CreateOrderCartBo {
const couponInfo = ref(null)
// 批量订单:每个店铺的优惠券映射 merchantId -> couponInfo
const merchantCouponMap = ref>({})
-// 批量订单:每个店铺的小费映射 merchantId -> tipAmount (小费金额,单位:美元)
-const merchantTipMap = ref>({})
-// 批量订单:每个店铺的小费比例映射 merchantId -> tipPercent (小费比例,0-100)
-const merchantTipPercentMap = ref>({})
function navigateToCoupon(merchantId?: string) {
const targetMerchantId = merchantId || storeId.value
@@ -1386,48 +1371,23 @@ function safeMoneyStr(val: unknown, fallback = 0) {
}
const displayDeliveryFeeStr = computed(() => {
- const p: any = priceData.value;
- if (!p) return '0.00';
- if (orderType.value === 'batch') {
- return safeMoneyStr(p.totalDeliveryFee, 0);
- }
- return safeMoneyStr(p.deliveryFee, 0);
+ return safeMoneyStr(pickCheckoutDeliveryFee(priceData.value as any, orderType.value === 'batch'), 0);
});
const displayGoodsAmountStr = computed(() => {
- const p: any = priceData.value;
- if (!p) return '0.00';
- if (orderType.value === 'batch') {
- return safeMoneyStr(p.totalActualAmount, 0);
- }
- return safeMoneyStr(p.actualAmount, 0);
+ return safeMoneyStr(pickCheckoutGoodsAmount(priceData.value as any, orderType.value === 'batch'), 0);
});
const displayTaxStr = computed(() => {
- const p: any = priceData.value;
- if (!p) return '0.00';
- if (orderType.value === 'batch') {
- return safeMoneyStr(p.totalTax, 0);
- }
- return safeMoneyStr(p.tax, 0);
+ return safeMoneyStr(pickCheckoutTax(priceData.value as any, orderType.value === 'batch'), 0);
});
const displayTipStr = computed(() => {
- const p: any = priceData.value;
- if (!p) return '0.00';
- if (orderType.value === 'batch') {
- return safeMoneyStr(p.totalTip, 0);
- }
- return safeMoneyStr(p.tip, 0);
+ return safeMoneyStr(pickCheckoutTip(priceData.value as any, orderType.value === 'batch'), 0);
});
const displayPaidStr = computed(() => {
- const p: any = priceData.value;
- if (!p) return '0.00';
- if (orderType.value === 'batch') {
- return safeMoneyStr(p.totalPaidAmount, 0);
- }
- return safeMoneyStr(p.paidAmount, 0);
+ return safeMoneyStr(pickCheckoutPaidAmount(priceData.value as any, orderType.value === 'batch'), 0);
});
const scheduledServiceEndLabel = computed(() => {
@@ -1486,12 +1446,7 @@ const listDeliveryFeeAmount = computed(() => {
});
const actualDeliveryFeeNum = computed(() => {
- const p: any = priceData.value;
- if (!p) return 0;
- if (orderType.value === 'batch') {
- return Number(p.totalDeliveryFee ?? 0);
- }
- return Number(p.deliveryFee ?? 0);
+ return pickCheckoutDeliveryFee(priceData.value as any, orderType.value === 'batch');
});
const showListDeliveryStrike = computed(() => {
@@ -1525,17 +1480,10 @@ function handleClose(merchantId?: string) {
circle-back
custom-class="checkout-page-navbar"
/>
-
-
-
-
+
+
+
+
@@ -1582,6 +1530,22 @@ function handleClose(merchantId?: string) {
+
+
+ {{ t('pages-store.checkout.confirmOrder') }}
+
+ {{ t('pages-store.checkout.missingParamsHint') }}
+
+
+
+
+
+ {{ t('pages-store.checkout.confirmOrder') }}
+
+ {{ t('pages-store.checkout.emptyCartHint') }}
+
+
+
{{ t('pages-store.checkout.confirmOrder') }}
@@ -1847,63 +1811,67 @@ function handleClose(merchantId?: string) {
/>
-
-
- {{
- t('pages-store.checkout.driverTipNote')
- }}
-
-
-
- {{ t('pages-store.checkout.driverTip') }}
-
-
+
+
+
+
-
-
- {{ item.label }}
-
-
-
-
-
- $
-
-
-
-
-
+
+
+ {{ t('pages-store.checkout.driverTip') }}
+
+
+ {{ t('pages-store.checkout.tipByDeliveryDatePrefix') }}{{ formatDeliveryDateLabel(dateKey) }}
+
+ {{
+ t('pages-store.checkout.driverTipNote')
+ }}
+
+
+ {{ item.label }}
+
+
+
+
+
+ $
+
+
@@ -1955,6 +1923,7 @@ function handleClose(merchantId?: string) {
selectedTipIndex === item.value
? 'checkout-tip-pill checkout-tip-pill--on'
: 'checkout-tip-pill',
+ item.value === TIP_NEXT_TIME ? 'checkout-tip-pill--phrase' : '',
]"
class="checkout-tip-cell"
>
@@ -1967,7 +1936,7 @@ function handleClose(merchantId?: string) {
? 'checkout-tip-pill checkout-tip-pill--on'
: 'checkout-tip-pill',
]"
- class="checkout-tip-cell checkout-tip-cell--input"
+ class="checkout-tip-cell checkout-tip-cell--input checkout-tip-cell--input-span2"
>
{
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 list = orderDetail.value?.merchantOrderDishVoList as unknown as Array | null | undefined
@@ -571,6 +588,12 @@ async function onVoucherImageUploaded(urls: unknown) {
{{ t('pages-store.order.deliveryAddress') }}
+
+ {{ t('pages-store.order.deliveryDate') }}: {{ orderDeliveryDateLabel }}
+
@@ -698,7 +721,7 @@ async function onVoucherImageUploaded(urls: unknown) {
/>
- ${{ (Number(orderDetail?.tax) + Number(orderDetail?.tip) + Number(orderDetail?.deliveryFee)).toFixed(2) }}
+ ${{ taxesAndOtherFeesTotal }}
diff --git a/src/pages-store/pages/order/utils/checkout-order.ts b/src/pages-store/pages/order/utils/checkout-order.ts
new file mode 100644
index 0000000..def8484
--- /dev/null
+++ b/src/pages-store/pages/order/utils/checkout-order.ts
@@ -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[] {
+ const set = new Set()
+ 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,
+ diyMap: Record,
+): Record {
+ const map: Record = {}
+ 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[] {
+ return ids.map((id) => String(id)).filter(Boolean)
+}
+
+type SchedulePayload = {
+ startScheduledTime?: number
+ endScheduledTime?: number
+ startMap: Record
+ endMap: Record
+}
+
+export function appendDeliveryScheduleFields(
+ body: Record,
+ deliveryTimeType: number,
+ options: {
+ isBatch: boolean
+ storeId?: string
+ showDeliveryTime?: string
+ diyTime?: { startTime?: number; endTime?: number }
+ merchants: Array<{ id?: string | number }>
+ appointmentMap: Record
+ buildScheduledTimePayload: (
+ merchants: Array<{ id?: string | number }>,
+ map: Record,
+ ) => 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()
+ 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 | 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 | 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 | 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 | 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 | 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
+}
diff --git a/src/pages-store/pages/store/dishes.vue b/src/pages-store/pages/store/dishes.vue
index 4e7acda..dc45813 100644
--- a/src/pages-store/pages/store/dishes.vue
+++ b/src/pages-store/pages/store/dishes.vue
@@ -112,11 +112,20 @@ function handleSubmit() {
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({
body: {
merchantId: storeId.value,
dishId: dish.id,
- count: 1,
+ count,
merchantCartSideDishBoList,
} as any,
}).then(() => {
@@ -147,11 +156,7 @@ function onAddCartClick() {
}, 400);
return;
}
- if (hasSideDishes.value) {
- openSpecPopup();
- } else {
- addCart();
- }
+ openSpecPopup();
}
onPageScroll((e) => {
@@ -308,6 +313,20 @@ function handleShare() {
const isSoldOut = computed(() => isSoldOutStock(dishDetailData.value?.stock))
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 list = dishDetailData.value?.merchantSideDishVoList ?? [];
@@ -386,6 +405,7 @@ const detailDisplayPrice = computed(() => {
});
function openSpecPopup() {
+ resetAddCartCount()
const sideList = dishDetailData.value?.merchantSideDishVoList ?? [];
for (const item of sideList) {
if (
@@ -1018,6 +1038,20 @@ function getStoreDetail() {
+
+
-
+
-
-
-
+
+ Loading...
+
+
+
+
+
+ {{ (item as any).displayName }}
+
+ {{ (item as any).formattedAddress }}
+
+
-
-
-
-
-
- {{ item.displayName }}
-
- {{ item.formattedAddress }}
-
-
-
-
-
+
+
+
-
@@ -54,84 +55,245 @@
import Config from "@/config";
import {useLogicStore} from "@/pages-user/store/logic";
import {useUserStore} from "@/store";
-import { getCurrentInstance } from "vue";
+import {debounce} from "throttle-debounce";
const {t, locale} = useI18n();
const userStore = useUserStore()
const logicStore = useLogicStore()
+export interface AddressPlaceItem {
+ id: string
+ displayName: string
+ formattedAddress: string
+ location: { lat: number; lng: number } | null
+}
+
let openerEventChannel: any = null
onLoad(() => {
- // 统一通过 getOpenerEventChannel 获取导航传入的 eventChannel
const pages = getCurrentPages() as any[]
const page = pages[pages.length - 1]
openerEventChannel = page?.getOpenerEventChannel?.() || null
})
-// 生命周期:清空地址列表
onMounted(() => {
logicStore.clearPlacesList()
})
-const placesLength = computed(() => {
- return logicStore.placesList.length;
-});
-
-const userLocation = computed(() => ({
- longitude: userStore.location.longitude,
- latitude: userStore.location.latitude
-}));
+const placesLength = computed(() => logicStore.placesList.length)
+const mapSearchKeyword = ref('')
+const hasSearched = ref(false)
watch(
- () => logicStore.searchLoading,
- (newValue) => {
- if (newValue) {
- uni.showLoading({
- title: 'Loading...',
- mask: true,
- });
- } else {
- uni.hideLoading();
- }
- },
- {immediate: true}
-);
+ () => logicStore.searchLoading,
+ (loading) => {
+ if (loading) {
+ uni.showLoading({ title: 'Loading...', mask: true })
+ } else {
+ uni.hideLoading()
+ }
+ },
+ { immediate: true },
+)
-// 地图搜索关键词
-const mapSearchKeyword = ref('');
-const querySearch = computed(() => {
- return {
- keyword: mapSearchKeyword.value,
+function resolveLanguageCode() {
+ if (locale.value === 'zh-Hans') return 'zh-CN'
+ if (locale.value === 'en') return 'en'
+ return String(locale.value || 'en')
+}
+
+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(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) {
- console.log('item', item)
+watch(mapSearchKeyword, (keyword) => {
+ const text = String(keyword || '').trim()
+ if (!text) {
+ hasSearched.value = false
+ logicStore.clearPlacesList()
+ return
+ }
+ debouncedSearch(text)
+})
+
+function handleClickLocation(item: AddressPlaceItem) {
openerEventChannel?.emit?.('chooseAddress', item)
uni.navigateBack()
}
-// 使用当前位置
function handleUseLocation() {
- // 检查是否获取到了当前定位
if (!userStore.location.longitude || !userStore.location.latitude) {
- // 尝试再次获取定位
uni.getLocation({
isHighAccuracy: true,
type: 'gcj02',
geocode: true,
- success: async (res) => {
+ success: (res) => {
getCityName(res.latitude, res.longitude)
},
- fail: (err) => {
+ fail: () => {
const settings = uni.getAppAuthorizeSetting()
- console.log(settings)
if (settings.locationAuthorized === 'denied') {
uni.showToast({
title: t('common.prompt.please-authorize-location-information'),
- icon: 'none'
+ icon: 'none',
})
- setTimeout(()=> {
+ setTimeout(() => {
uni.openAppAuthorizeSetting()
})
}
@@ -143,311 +305,75 @@ function handleUseLocation() {
formattedAddress: userStore.location.formattedAddress || '',
location: {
lng: userStore.location.longitude,
- lat: userStore.location.latitude
- }
+ lat: userStore.location.latitude,
+ },
})
uni.navigateBack()
}
}
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({
url,
method: 'GET',
timeout: 10000,
success: (res: any) => {
- const results = res.data?.results || [];
- console.log('geocode results:', results);
+ const results = res.data?.results || []
if (!results.length) {
- return handleFail();
+ return handleFail()
}
- const addr = results[0]; // 最高匹配度
- const components = addr.address_components || [];
+ const addr = results[0]
+ const components = addr.address_components || []
- // 提取城市名的工具函数
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)))
+ }
- // 寻找城市名(多层兜底)
- let cityObj =
- pickAddress(["locality"]) ||
- pickAddress(["administrative_area_level_2"]) ||
- pickAddress(["administrative_area_level_1"]) ||
- pickAddress(["political"]);
+ const cityObj =
+ pickAddress(['locality']) ||
+ pickAddress(['administrative_area_level_2']) ||
+ pickAddress(['administrative_area_level_1']) ||
+ pickAddress(['political'])
- const cityName = cityObj?.long_name || null;
-
- // 从 Google 获取完整地址
- const formattedAddress = addr.formatted_address || cityName || "";
-
- console.log("city:", cityObj);
- console.log("formattedAddress:", formattedAddress);
+ const cityName = cityObj?.long_name || null
+ const formattedAddress = addr.formatted_address || cityName || ''
if (!cityName) {
- return handleFail();
+ return handleFail()
}
- // 存入 store
userStore.location = {
location: cityName,
formattedAddress,
longitude,
- latitude
- };
+ latitude,
+ }
- // 回传上一页(eventChannel)
openerEventChannel?.emit?.('chooseAddress', {
displayName: cityName,
formattedAddress,
- location: { lng: longitude, lat: latitude }
- });
+ location: { lng: longitude, lat: latitude },
+ })
- uni.navigateBack();
+ uni.navigateBack()
},
-
- fail: () => handleFail()
- });
+ fail: () => handleFail(),
+ })
function handleFail() {
uni.showToast({
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,
- }
-})
-
-
-
\ No newline at end of file
+
diff --git a/src/pages.json b/src/pages.json
index d638172..ceddb75 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -234,6 +234,9 @@
},
{
"path": "pages/recharge/index"
+ },
+ {
+ "path": "pages/recharge/activity-detail"
}
]
},
@@ -290,6 +293,12 @@
"style": {
"onReachBottomDistance": 80
}
+ },
+ {
+ "path": "pages/group-catering/index"
+ },
+ {
+ "path": "pages/group-catering/detail"
}
]
}
diff --git a/src/pages/home/components/tabbar-home/components/featured-on/index.vue b/src/pages/home/components/tabbar-home/components/featured-on/index.vue
index 0d4df93..d533056 100644
--- a/src/pages/home/components/tabbar-home/components/featured-on/index.vue
+++ b/src/pages/home/components/tabbar-home/components/featured-on/index.vue
@@ -148,7 +148,7 @@ function subtitleLine(item: any): string {
flex-shrink: 0;
display: flex;
flex-direction: row;
- width: 660rpx;
+ width: 560rpx;
height: 306rpx;
min-height: 306rpx;
margin-left: 16rpx;
@@ -162,7 +162,7 @@ function subtitleLine(item: any): string {
}
.featured-card__media {
- width: 323rpx;
+ width: 383rpx;
flex-shrink: 0;
background: #f0f0f0;
display: flex;
@@ -206,8 +206,8 @@ function subtitleLine(item: any): string {
.featured-card__name {
display: block;
width: 100%;
- font-size: 28rpx;
- line-height: 34rpx;
+ font-size: 24rpx;
+ line-height: 30rpx;
font-weight: 600;
color: #1a1a1a;
white-space: normal;
@@ -224,8 +224,8 @@ function subtitleLine(item: any): string {
.featured-card__fee {
margin-top: 10rpx;
- font-size: 24rpx;
- line-height: 30rpx;
+ font-size: 18rpx;
+ line-height: 24rpx;
font-weight: 500;
color: #d48806;
}
diff --git a/src/pages/home/components/tabbar-home/components/tabs-type.vue b/src/pages/home/components/tabbar-home/components/tabs-type.vue
index 56eb001..cc1e67c 100644
--- a/src/pages/home/components/tabbar-home/components/tabs-type.vue
+++ b/src/pages/home/components/tabbar-home/components/tabs-type.vue
@@ -3,7 +3,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({
- /** 外部列表(菜谱页等);首页不传则使用固定六项 */
+ /** 外部列表(菜谱页等);首页不传则使用固定七项 */
list: {
type: Array,
default: () => [],
@@ -31,19 +31,9 @@ const { t } = useI18n()
const fixedTabs = [
{
- id: 'member-zone',
- nameKey: 'pages.home.quickTabs.memberZone',
- logoUrl: '/static/app/images/home/huiyuanzhuanqu.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: 'fresh-seafood-today',
+ nameKey: 'pages.home.quickTabs.freshSeafoodToday',
+ logoUrl: '/static/app/images/home/xiandahaixian.png',
},
{
id: 'new-calendar',
@@ -51,9 +41,24 @@ const fixedTabs = [
logoUrl: '/static/app/images/home/shangxinrili.png',
},
{
- id: 'fresh-seafood-today',
- nameKey: 'pages.home.quickTabs.freshSeafoodToday',
- logoUrl: '/static/app/images/home/xiandahaixian.png',
+ id: 'member-zone',
+ nameKey: 'pages.home.quickTabs.memberZone',
+ 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',
diff --git a/src/pages/home/components/tabbar-home/tabbar-home.vue b/src/pages/home/components/tabbar-home/tabbar-home.vue
index efee085..eef2c02 100644
--- a/src/pages/home/components/tabbar-home/tabbar-home.vue
+++ b/src/pages/home/components/tabbar-home/tabbar-home.vue
@@ -240,6 +240,10 @@ function tabsTypeChange(id: string | number) {
navigateTo('/pages-store/pages/energy-meal/index')
return
}
+ if (topic === 'group-catering') {
+ navigateTo('/pages-store/pages/group-catering/index')
+ return
+ }
if (!isQuickTopicSlug(topic)) {
return
}
@@ -314,8 +318,8 @@ function handleClickSwiper(item: any) {
case 3: // 会员
navigateTo('/pages-user/pages/member/index')
break
- case 5: // 钱包
- navigateTo('/pages-user/pages/balance/index')
+ case 5: // 充值活动
+ navigateTo('/pages-user/pages/recharge/activity-detail?id=' + item.id)
break
// case 4:
// navigateTo('/pages/ai/chat/index')
@@ -323,8 +327,6 @@ function handleClickSwiper(item: any) {
}
}
-const walletUrl = "pages-user/pages/balance/index";
-
function navigateToDishes(item: any) {
uni.navigateTo({
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
{{ t('pages.home.recharge-now') }}
@@ -532,10 +534,10 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
{{ t('pages-store.store.members') }}: US${{ item?.memberPrice }}
-
+
@@ -749,11 +751,13 @@ const debouncedEmit = debounce(1300, (isCollected: boolean, id: string, type: Co
}
.featured-dish-add {
+ flex-shrink: 0;
+}
+
+.featured-dish-add__icon {
width: 56rpx;
height: 56rpx;
- border-radius: 50%;
- background: #14181b;
- box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
+ display: block;
}
/* 可选营销条(淡红底 + 文案) */
diff --git a/src/pages/search/components/search-dish-result-item.vue b/src/pages/search/components/search-dish-result-item.vue
index f0118d8..df4e400 100644
--- a/src/pages/search/components/search-dish-result-item.vue
+++ b/src/pages/search/components/search-dish-result-item.vue
@@ -78,8 +78,8 @@ const cover = computed(() => {
-
-
+
+
@@ -180,17 +180,13 @@ const cover = computed(() => {
.search-dish-item__add {
align-self: flex-end;
- width: 64rpx;
- height: 64rpx;
flex-shrink: 0;
- border-radius: 50%;
- background: #f2f2f2;
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-dish-item__add-icon {
- width: 30rpx;
- height: 30rpx;
+ width: 64rpx;
+ height: 64rpx;
+ display: block;
}
.search-dish-item__img-wrap {
diff --git a/src/service/groupMealReservation.ts b/src/service/groupMealReservation.ts
new file mode 100644
index 0000000..450175d
--- /dev/null
+++ b/src/service/groupMealReservation.ts
@@ -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>('/app/groupMealReservation/cancel', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ data: body,
+ ...(options || {}),
+ });
+}
diff --git a/src/service/index.ts b/src/service/index.ts
index 91e7371..f12e17d 100644
--- a/src/service/index.ts
+++ b/src/service/index.ts
@@ -45,3 +45,4 @@ export * from './userCoupon';
export * from './marketingActivity';
export * from './marketActivity';
export * from './energyMeal';
+export * from './groupMealReservation';
diff --git a/src/service/marketActivity.ts b/src/service/marketActivity.ts
index 14fa9af..0e3b70d 100644
--- a/src/service/marketActivity.ts
+++ b/src/service/marketActivity.ts
@@ -16,3 +16,15 @@ export async function appMarketActivityListPost({
...(options || {}),
});
}
+
+/** 充值赠送活动 GET /app/marketActivity/rechargePromotion */
+export async function appMarketActivityRechargePromotionGet({
+ options,
+}: {
+ options?: CustomRequestOptions;
+} = {}) {
+ return request('/app/marketActivity/rechargePromotion', {
+ method: 'GET',
+ ...(options || {}),
+ });
+}
diff --git a/src/service/merchantOrder.ts b/src/service/merchantOrder.ts
index 2db108c..a7db851 100644
--- a/src/service/merchantOrder.ts
+++ b/src/service/merchantOrder.ts
@@ -112,15 +112,28 @@ export interface CalculatePriceCartBatchBo {
*/
receiveMethod?: number;
/**
- * 小费比例
+ * 配送单小费(单商户算价/创单,固定金额)
*/
- tipDiscount?: number;
+ tip?: number;
+ /**
+ * 交付时间类型(1-立即交付 2-预约交付)
+ */
+ deliveryType?: number;
+ /**
+ * 预约开始/结束时间(13 位毫秒时间戳,deliveryType=2 时)
+ */
+ startScheduledTime?: number;
+ endScheduledTime?: number;
/**
* 商户优惠券映射 { merchantId: couponId }
*/
merchantCouponMap?: Record;
/**
- * 商户小费映射 { merchantId: tipAmount } (小费金额,单位:美元)
+ * 按配送日期的小费映射 { yyyy-MM-dd: tipAmount }
+ */
+ deliveryDateTipMap?: Record;
+ /**
+ * @deprecated 使用 deliveryDateTipMap
*/
merchantTipMap?: Record;
/**
@@ -279,9 +292,13 @@ export interface CreateOrderCartBatchBo {
*/
endScheduledTime?: number | string;
/**
- * 小费比例
+ * 配送单小费(单商户算价/创单,固定金额)
*/
- tipDiscount?: number;
+ tip?: number;
+ /**
+ * 按配送日期的小费映射 { yyyy-MM-dd: tipAmount }
+ */
+ deliveryDateTipMap?: Record;
/**
* 是否需要餐具(1-是 2-否)
*/
@@ -291,7 +308,7 @@ export interface CreateOrderCartBatchBo {
*/
merchantCouponMap?: Record;
/**
- * 商户小费映射 { merchantId: tipAmount } (小费金额,单位:美元)
+ * @deprecated 使用 deliveryDateTipMap
*/
merchantTipMap?: Record;
/**
diff --git a/src/static/app/images/add_cart.png b/src/static/app/images/add_cart.png
new file mode 100644
index 0000000..025e4b5
Binary files /dev/null and b/src/static/app/images/add_cart.png differ
diff --git a/src/static/app/images/home/tancanyuding.png b/src/static/app/images/home/tancanyuding.png
new file mode 100644
index 0000000..86f1425
Binary files /dev/null and b/src/static/app/images/home/tancanyuding.png differ