```
feat(registration): 优化经销商注册流程并增加地址定位功能 - 修改导航栏标题从“邀请注册”为“注册成为会员” - 修复重复提交问题并移除不必要的submitting状态 - 增加昵称和头像的必填验证提示 - 添加用户角色缺失时的默认角色写入机制 - 集成地图选点功能,支持经纬度获取和地址解析 - 实现微信地址导入功能,自动填充基本信息 - 增加定位权限检查和错误处理机制 - 添加.gitignore规则忽略备份文件夹src__bak - 移除已废弃的银行卡和客户管理页面代码 - 优化表单验证规则和错误提示信息 - 实现经销商注册成功后自动跳转到“我的”页面 - 添加用户信息缓存刷新机制确保角色信息同步 ```
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {
|
||||
Image,
|
||||
Button,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ActionSheet,
|
||||
Popup,
|
||||
InputNumber,
|
||||
DatePicker,
|
||||
ConfigProvider
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {Location, ArrowRight} from '@nutui/icons-react-taro'
|
||||
@@ -27,6 +28,8 @@ import OrderConfirmSkeleton from "@/components/OrderConfirmSkeleton";
|
||||
import CouponList from "@/components/CouponList";
|
||||
import {CouponCardProps} from "@/components/CouponCard";
|
||||
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
||||
import {getGltTicketTemplateByGoodsId} from "@/api/glt/gltTicketTemplate";
|
||||
import type {GltTicketTemplate} from "@/api/glt/gltTicketTemplate/model";
|
||||
import {
|
||||
transformCouponData,
|
||||
calculateCouponDiscount,
|
||||
@@ -36,7 +39,11 @@ import {
|
||||
filterUsableCoupons,
|
||||
filterUnusableCoupons
|
||||
} from "@/utils/couponUtils";
|
||||
import navTo from "@/utils/common";
|
||||
import dayjs from 'dayjs'
|
||||
import type {ShopStore} from "@/api/shop/shopStore/model";
|
||||
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||
import { ensureLoggedIn, isLoggedIn } from '@/utils/auth'
|
||||
|
||||
|
||||
const OrderConfirm = () => {
|
||||
@@ -50,6 +57,21 @@ const OrderConfirm = () => {
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string>('')
|
||||
const [payLoading, setPayLoading] = useState<boolean>(false)
|
||||
// 配送时间(仅水票套票商品需要)
|
||||
// 当日截单时间:超过该时间下单,最早配送日顺延到次日(避免 21:00 下单仍显示“当天配送”)
|
||||
const DELIVERY_CUTOFF_HOUR = 21
|
||||
const getMinSendDate = () => {
|
||||
const now = dayjs()
|
||||
const cutoff = now.hour(DELIVERY_CUTOFF_HOUR).minute(0).second(0).millisecond(0)
|
||||
const startOfToday = now.startOf('day')
|
||||
// >= 截单时间则最早只能选次日
|
||||
return now.isSame(cutoff) || now.isAfter(cutoff) ? startOfToday.add(1, 'day') : startOfToday
|
||||
}
|
||||
const [sendTime, setSendTime] = useState<Date>(() => getMinSendDate().toDate())
|
||||
const [sendTimePickerVisible, setSendTimePickerVisible] = useState(false)
|
||||
|
||||
// 水票套票活动(若存在则按规则限制最小购买量等)
|
||||
const [ticketTemplate, setTicketTemplate] = useState<GltTicketTemplate | null>(null)
|
||||
|
||||
// InputNumber 主题配置
|
||||
const customTheme = {
|
||||
@@ -67,13 +89,92 @@ const OrderConfirm = () => {
|
||||
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
|
||||
const [couponLoading, setCouponLoading] = useState<boolean>(false)
|
||||
|
||||
// 门店选择:用于在下单页展示当前“已选门店”,并允许用户切换(写入 SelectedStore Storage)
|
||||
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
||||
const [stores, setStores] = useState<ShopStore[]>([])
|
||||
const [storeLoading, setStoreLoading] = useState(false)
|
||||
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
||||
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const goodsId = router?.params?.goodsId;
|
||||
|
||||
// 页面级兜底:未登录直接进入下单页时,引导去注册/登录并回跳
|
||||
useEffect(() => {
|
||||
if (!goodsId) {
|
||||
// 也可能是 orderData 模式;这里只做最小兜底
|
||||
if (!ensureLoggedIn('/shop/orderConfirm/index')) return
|
||||
return
|
||||
}
|
||||
if (!ensureLoggedIn(`/shop/orderConfirm/index?goodsId=${goodsId}`)) return
|
||||
}, [goodsId])
|
||||
|
||||
const isTicketTemplateActive =
|
||||
!!ticketTemplate &&
|
||||
ticketTemplate.enabled !== false &&
|
||||
ticketTemplate.status !== 1 &&
|
||||
ticketTemplate.deleted !== 1
|
||||
const hasTicketTemplate = !!ticketTemplate
|
||||
|
||||
// 套票活动最低购买量:优先取模板配置
|
||||
const ticketMinBuyQty = (() => {
|
||||
const n = Number(ticketTemplate?.minBuyQty)
|
||||
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1
|
||||
})()
|
||||
const minBuyQty = isTicketTemplateActive ? ticketMinBuyQty : 1
|
||||
|
||||
const sendTimeText = useMemo(() => {
|
||||
return dayjs(sendTime).format('YYYY-MM-DD')
|
||||
}, [sendTime])
|
||||
|
||||
const getGiftTicketQty = (buyQty: number) => {
|
||||
if (!isTicketTemplateActive) return 0
|
||||
const multiplier = Number(ticketTemplate?.giftMultiplier || 0)
|
||||
const startSend = Number(ticketTemplate?.startSendQty || 0)
|
||||
if (multiplier > 0) return Math.max(0, buyQty) * multiplier
|
||||
return Math.max(0, startSend)
|
||||
}
|
||||
|
||||
const loadStores = async () => {
|
||||
if (storeLoading) return
|
||||
try {
|
||||
setStoreLoading(true)
|
||||
const list = await listShopStore()
|
||||
setStores((list || []).filter(s => s?.isDelete !== 1))
|
||||
} catch (e) {
|
||||
console.error('获取门店列表失败:', e)
|
||||
setStores([])
|
||||
Taro.showToast({title: '获取门店列表失败', icon: 'none'})
|
||||
} finally {
|
||||
setStoreLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const openStorePopup = async () => {
|
||||
setStorePopupVisible(true)
|
||||
if (!stores.length) {
|
||||
await loadStores()
|
||||
}
|
||||
}
|
||||
|
||||
// 计算商品总价
|
||||
const getGoodsTotal = () => {
|
||||
if (!goods) return 0
|
||||
return parseFloat(goods.price || '0') * quantity
|
||||
const price = parseFloat(goods.price || '0')
|
||||
// const total = price * quantity
|
||||
|
||||
// 🔍 详细日志,用于排查数值精度问题
|
||||
// console.log('💵 商品总价计算:', {
|
||||
// goodsPrice: goods.price,
|
||||
// goodsPriceType: typeof goods.price,
|
||||
// parsedPrice: price,
|
||||
// quantity: quantity,
|
||||
// total: total,
|
||||
// totalFixed2: total.toFixed(2),
|
||||
// totalString: total.toString()
|
||||
// })
|
||||
|
||||
return price * quantity
|
||||
}
|
||||
|
||||
// 计算优惠券折扣
|
||||
@@ -98,14 +199,16 @@ const OrderConfirm = () => {
|
||||
|
||||
// 处理数量变化
|
||||
const handleQuantityChange = (value: string | number) => {
|
||||
const newQuantity = typeof value === 'string' ? parseInt(value) || 1 : value
|
||||
const finalQuantity = Math.max(1, Math.min(newQuantity, goods?.stock || 999))
|
||||
const fallback = isTicketTemplateActive ? minBuyQty : 1
|
||||
const newQuantity = typeof value === 'string' ? parseInt(value, 10) || fallback : value
|
||||
const finalQuantity = Math.max(fallback, Math.min(newQuantity, goods?.stock || 999))
|
||||
setQuantity(finalQuantity)
|
||||
|
||||
// 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用
|
||||
if (availableCoupons.length > 0) {
|
||||
const newTotal = parseFloat(goods?.price || '0') * finalQuantity
|
||||
const sortedCoupons = sortCoupons(availableCoupons, newTotal)
|
||||
const usableCoupons = filterUsableCoupons(sortedCoupons, newTotal)
|
||||
setAvailableCoupons(sortedCoupons)
|
||||
|
||||
// 检查当前选中的优惠券是否还可用
|
||||
@@ -115,6 +218,56 @@ const OrderConfirm = () => {
|
||||
title: '当前优惠券不满足使用条件,已自动取消',
|
||||
icon: 'none'
|
||||
})
|
||||
|
||||
// 🎯 自动推荐新的最优优惠券
|
||||
if (usableCoupons.length > 0) {
|
||||
const bestCoupon = usableCoupons[0]
|
||||
const discount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||
|
||||
if (discount > 0) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
Taro.showToast({
|
||||
title: `已为您重新推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||
icon: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (!selectedCoupon && usableCoupons.length > 0) {
|
||||
// 🔔 如果没有选中优惠券但有可用的,推荐最优的
|
||||
const bestCoupon = usableCoupons[0]
|
||||
const discount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||
|
||||
if (discount > 0) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
Taro.showToast({
|
||||
title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||
icon: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
} else if (selectedCoupon && usableCoupons.length > 0) {
|
||||
// 🔍 检查是否有更好的优惠券
|
||||
const bestCoupon = usableCoupons[0]
|
||||
const currentDiscount = calculateCouponDiscount(selectedCoupon, newTotal)
|
||||
const bestDiscount = calculateCouponDiscount(bestCoupon, newTotal)
|
||||
|
||||
// 如果有更好的优惠券(优惠超过0.01元)
|
||||
if (bestDiscount > currentDiscount + 0.01 && bestCoupon.id !== selectedCoupon.id) {
|
||||
Taro.showModal({
|
||||
title: '发现更优惠的优惠券',
|
||||
content: `有更好的优惠券可用,额外节省¥${(bestDiscount - currentDiscount).toFixed(2)},是否更换?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
Taro.showToast({
|
||||
title: '优惠券已更换',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,9 +276,45 @@ const OrderConfirm = () => {
|
||||
const handleCouponSelect = (coupon: CouponCardProps) => {
|
||||
const total = getGoodsTotal()
|
||||
|
||||
// 🔍 详细日志记录,用于排查问题
|
||||
console.log('🎫 手动选择优惠券详细信息:', {
|
||||
coupon: {
|
||||
id: coupon.id,
|
||||
title: coupon.title,
|
||||
type: coupon.type,
|
||||
amount: coupon.amount,
|
||||
minAmount: coupon.minAmount,
|
||||
status: coupon.status
|
||||
},
|
||||
orderInfo: {
|
||||
goodsPrice: goods?.price,
|
||||
quantity: quantity,
|
||||
total: total,
|
||||
totalFixed: total.toFixed(2)
|
||||
},
|
||||
validation: {
|
||||
isUsable: isCouponUsable(coupon, total),
|
||||
discount: calculateCouponDiscount(coupon, total),
|
||||
reason: getCouponUnusableReason(coupon, total)
|
||||
}
|
||||
})
|
||||
|
||||
// 检查是否可用
|
||||
if (!isCouponUsable(coupon, total)) {
|
||||
const reason = getCouponUnusableReason(coupon, total)
|
||||
|
||||
// 🚨 记录手动选择失败的详细信息
|
||||
console.error('🚨 手动选择优惠券失败:', {
|
||||
reason,
|
||||
coupon,
|
||||
total,
|
||||
minAmount: coupon.minAmount,
|
||||
comparison: {
|
||||
totalVsMinAmount: `${total} < ${coupon.minAmount}`,
|
||||
result: total < (coupon.minAmount || 0)
|
||||
}
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: reason || '优惠券不可用',
|
||||
icon: 'none'
|
||||
@@ -151,7 +340,7 @@ const OrderConfirm = () => {
|
||||
}
|
||||
|
||||
// 加载用户优惠券
|
||||
const loadUserCoupons = async () => {
|
||||
const loadUserCoupons = async (totalOverride?: number) => {
|
||||
try {
|
||||
setCouponLoading(true)
|
||||
|
||||
@@ -163,15 +352,63 @@ const OrderConfirm = () => {
|
||||
const transformedCoupons = res.map(transformCouponData)
|
||||
|
||||
// 按优惠金额排序
|
||||
const total = getGoodsTotal()
|
||||
const total = totalOverride ?? getGoodsTotal()
|
||||
const sortedCoupons = sortCoupons(transformedCoupons, total)
|
||||
const usableCoupons = filterUsableCoupons(sortedCoupons, total)
|
||||
|
||||
setAvailableCoupons(sortedCoupons)
|
||||
|
||||
// 🎯 智能推荐:自动应用最优惠的可用优惠券
|
||||
if (usableCoupons.length > 0 && !selectedCoupon) {
|
||||
const bestCoupon = usableCoupons[0] // 已经按优惠金额排序,第一个就是最优的
|
||||
const discount = calculateCouponDiscount(bestCoupon, total)
|
||||
|
||||
// 🔍 详细日志记录自动推荐的信息
|
||||
console.log('🤖 自动推荐优惠券详细信息:', {
|
||||
coupon: {
|
||||
id: bestCoupon.id,
|
||||
title: bestCoupon.title,
|
||||
type: bestCoupon.type,
|
||||
amount: bestCoupon.amount,
|
||||
minAmount: bestCoupon.minAmount,
|
||||
status: bestCoupon.status
|
||||
},
|
||||
orderInfo: {
|
||||
goodsPrice: goods?.price,
|
||||
quantity: quantity,
|
||||
total: total,
|
||||
totalFixed: total.toFixed(2)
|
||||
},
|
||||
validation: {
|
||||
isUsable: isCouponUsable(bestCoupon, total),
|
||||
discount: discount,
|
||||
reason: getCouponUnusableReason(bestCoupon, total)
|
||||
}
|
||||
})
|
||||
|
||||
if (discount > 0) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
|
||||
// 显示智能推荐提示
|
||||
Taro.showToast({
|
||||
title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`,
|
||||
icon: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 🔔 优惠券提示:如果有可用优惠券,显示提示
|
||||
if (usableCoupons.length > 0) {
|
||||
console.log(`发现${usableCoupons.length}张可用优惠券,已为您推荐最优惠券`)
|
||||
}
|
||||
|
||||
console.log('加载优惠券成功:', {
|
||||
originalData: res,
|
||||
transformedData: transformedCoupons,
|
||||
sortedData: sortedCoupons
|
||||
sortedData: sortedCoupons,
|
||||
usableCoupons: usableCoupons,
|
||||
recommendedCoupon: usableCoupons[0] || null
|
||||
})
|
||||
} else {
|
||||
setAvailableCoupons([])
|
||||
@@ -213,6 +450,32 @@ const OrderConfirm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 水票套票商品:保存配送时间到 ShopOrder.sendStartTime
|
||||
if (hasTicketTemplate && !sendTime) {
|
||||
Taro.showToast({ title: '请选择配送时间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (hasTicketTemplate) {
|
||||
const min = getMinSendDate()
|
||||
if (dayjs(sendTime).isBefore(min, 'day')) {
|
||||
setSendTime(min.toDate())
|
||||
Taro.showToast({
|
||||
title: `已过当日${DELIVERY_CUTOFF_HOUR}点截单,最早配送:${min.format('YYYY-MM-DD')}`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 水票套票活动:最小购买量校验
|
||||
if (isTicketTemplateActive && quantity < minBuyQty) {
|
||||
Taro.showToast({
|
||||
title: `最低购买量:${minBuyQty}桶`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 库存校验
|
||||
if (goods.stock !== undefined && quantity > goods.stock) {
|
||||
Taro.showToast({
|
||||
@@ -233,6 +496,56 @@ const OrderConfirm = () => {
|
||||
})
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 🔔 支付前最后一次检查:提醒用户是否有可用优惠券
|
||||
const total = getGoodsTotal()
|
||||
const usableCoupons = filterUsableCoupons(availableCoupons, total)
|
||||
|
||||
if (usableCoupons.length > 0) {
|
||||
const bestCoupon = usableCoupons[0]
|
||||
const discount = calculateCouponDiscount(bestCoupon, total)
|
||||
|
||||
if (discount > 0) {
|
||||
// 用模态框提醒用户
|
||||
const confirmResult = await new Promise<boolean>((resolve) => {
|
||||
Taro.showModal({
|
||||
title: '发现可用优惠券',
|
||||
content: `您有优惠券可使用,可省¥${discount.toFixed(2)},是否使用?`,
|
||||
success: (res) => resolve(res.confirm),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
|
||||
if (confirmResult) {
|
||||
setSelectedCoupon(bestCoupon)
|
||||
// 🔄 使用优惠券后需要重新构建订单数据,这里直接递归调用支付函数
|
||||
// 但要确保传递最新的优惠券信息
|
||||
const currentPaymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
|
||||
const updatedOrderData = buildSingleGoodsOrder(
|
||||
goods.goodsId!,
|
||||
quantity,
|
||||
address.id,
|
||||
{
|
||||
comments: goods.name,
|
||||
deliveryType: 0,
|
||||
buyerRemarks: orderRemark,
|
||||
sendStartTime: hasTicketTemplate
|
||||
? dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||
: undefined,
|
||||
couponId: parseInt(String(bestCoupon.id), 10)
|
||||
}
|
||||
);
|
||||
|
||||
console.log('🎯 使用推荐优惠券的订单数据:', updatedOrderData);
|
||||
|
||||
// 执行支付
|
||||
await PaymentHandler.pay(updatedOrderData, currentPaymentType);
|
||||
return; // 提前返回,避免重复执行支付
|
||||
} else {
|
||||
// 用户选择不使用优惠券,继续支付
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建订单数据
|
||||
@@ -241,26 +554,43 @@ const OrderConfirm = () => {
|
||||
quantity,
|
||||
address.id,
|
||||
{
|
||||
comments: goods.name,
|
||||
comments: '易赊宝',
|
||||
deliveryType: 0,
|
||||
buyerRemarks: orderRemark,
|
||||
// 确保couponId是数字类型
|
||||
couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined
|
||||
sendStartTime: hasTicketTemplate
|
||||
? dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||
: undefined,
|
||||
// 🔧 确保 couponId 是正确的数字类型,且不传递 undefined
|
||||
couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined
|
||||
}
|
||||
);
|
||||
|
||||
// 根据支付方式选择支付类型
|
||||
const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
|
||||
|
||||
console.log('开始支付:', {
|
||||
// 🔍 支付前的详细信息记录
|
||||
console.log('💰 开始支付 - 详细信息:', {
|
||||
orderData,
|
||||
paymentType,
|
||||
selectedCoupon: selectedCoupon ? {
|
||||
id: selectedCoupon.id,
|
||||
title: selectedCoupon.title,
|
||||
type: selectedCoupon.type,
|
||||
amount: selectedCoupon.amount,
|
||||
minAmount: selectedCoupon.minAmount,
|
||||
discount: getCouponDiscount()
|
||||
} : null,
|
||||
finalPrice: getFinalPrice()
|
||||
priceCalculation: {
|
||||
goodsPrice: goods?.price,
|
||||
quantity: quantity,
|
||||
goodsTotal: getGoodsTotal(),
|
||||
couponDiscount: getCouponDiscount(),
|
||||
finalPrice: getFinalPrice()
|
||||
},
|
||||
couponValidation: selectedCoupon ? {
|
||||
isUsable: isCouponUsable(selectedCoupon, getGoodsTotal()),
|
||||
reason: getCouponUnusableReason(selectedCoupon, getGoodsTotal())
|
||||
} : null
|
||||
});
|
||||
|
||||
// 执行支付 - 移除这里的成功提示,让PaymentHandler统一处理
|
||||
@@ -272,31 +602,35 @@ const OrderConfirm = () => {
|
||||
// icon: 'success'
|
||||
// })
|
||||
} catch (error: any) {
|
||||
return navTo('/user/order/order?statusFilter=0', true)
|
||||
// console.error('支付失败:', error)
|
||||
const message = String(error?.message || '')
|
||||
const isOutOfDeliveryRange =
|
||||
message.includes('不在配送范围') ||
|
||||
message.includes('配送范围') ||
|
||||
message.includes('电子围栏') ||
|
||||
message.includes('围栏')
|
||||
|
||||
// 只处理PaymentHandler未处理的错误
|
||||
// if (!error.handled) {
|
||||
// let errorMessage = '支付失败,请重试';
|
||||
//
|
||||
// // 根据错误类型提供具体提示
|
||||
// if (error.message?.includes('余额不足')) {
|
||||
// errorMessage = '账户余额不足,请充值后重试';
|
||||
// } else if (error.message?.includes('优惠券')) {
|
||||
// errorMessage = '优惠券使用失败,请重新选择';
|
||||
// } else if (error.message?.includes('库存')) {
|
||||
// errorMessage = '商品库存不足,请减少购买数量';
|
||||
// } else if (error.message?.includes('地址')) {
|
||||
// errorMessage = '收货地址信息有误,请重新选择';
|
||||
// } else if (error.message) {
|
||||
// errorMessage = error.message;
|
||||
// }
|
||||
// Taro.showToast({
|
||||
// title: errorMessage,
|
||||
// icon: 'error'
|
||||
// })
|
||||
// console.log('跳去未付款的订单列表页面')
|
||||
// }
|
||||
// “配送范围”类错误给出更友好的解释,并提供快捷入口去更换收货地址
|
||||
if (isOutOfDeliveryRange) {
|
||||
try {
|
||||
const res = await Taro.showModal({
|
||||
title: '暂不支持配送',
|
||||
content: '当前收货地址超出配送范围。您可以更换收货地址后再下单,或联系门店确认配送范围。',
|
||||
confirmText: '更换地址',
|
||||
cancelText: '我知道了'
|
||||
})
|
||||
if (res?.confirm) {
|
||||
Taro.navigateTo({ url: '/user/address/index' })
|
||||
}
|
||||
} catch (_e) {
|
||||
// ignore
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 兜底:仅在 PaymentHandler 未弹过提示时再提示一次
|
||||
if (!error?.handled) {
|
||||
Taro.showToast({ title: message || '支付失败,请重试', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
setPayLoading(false)
|
||||
}
|
||||
@@ -304,6 +638,9 @@ const OrderConfirm = () => {
|
||||
|
||||
// 统一的数据加载函数
|
||||
const loadAllData = async () => {
|
||||
// 未登录时不发起接口请求;页面会被登录兜底逻辑引导走注册/登录页
|
||||
if (!isLoggedIn()) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
@@ -320,10 +657,42 @@ const OrderConfirm = () => {
|
||||
])
|
||||
|
||||
// 设置商品信息
|
||||
if (goodsRes) {
|
||||
setGoods(goodsRes)
|
||||
// 查询当前商品是否存在水票套票活动(失败/无数据时不影响正常下单)
|
||||
let tpl: GltTicketTemplate | null = null
|
||||
if (goodsId) {
|
||||
try {
|
||||
tpl = await getGltTicketTemplateByGoodsId(Number(goodsId))
|
||||
} catch (e) {
|
||||
tpl = null
|
||||
}
|
||||
}
|
||||
|
||||
const tplActive =
|
||||
!!tpl &&
|
||||
tpl.enabled !== false &&
|
||||
tpl.status !== 1 &&
|
||||
tpl.deleted !== 1
|
||||
|
||||
const tplMinBuyQty = (() => {
|
||||
const n = Number(tpl?.minBuyQty)
|
||||
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1
|
||||
})()
|
||||
|
||||
// 设置商品信息(若存在套票模板,则默认 canBuyNumber 使用模板最小购买量)
|
||||
if (goodsRes) {
|
||||
const patchedGoods: ShopGoods = { ...goodsRes }
|
||||
if (tplActive && ((patchedGoods.canBuyNumber ?? 0) === 0)) {
|
||||
patchedGoods.canBuyNumber = tplMinBuyQty
|
||||
}
|
||||
setGoods(patchedGoods)
|
||||
|
||||
// 设置默认购买数量:优先使用 canBuyNumber,否则使用 1
|
||||
const initQty = (patchedGoods.canBuyNumber ?? 0) > 0 ? (patchedGoods.canBuyNumber as number) : 1
|
||||
setQuantity(initQty)
|
||||
}
|
||||
|
||||
setTicketTemplate(tpl)
|
||||
|
||||
// 设置默认收货地址
|
||||
if (addressRes && addressRes.length > 0) {
|
||||
setAddress(addressRes[0])
|
||||
@@ -338,9 +707,16 @@ const OrderConfirm = () => {
|
||||
setPayment(paymentRes[0])
|
||||
}
|
||||
|
||||
// 加载优惠券(在商品信息加载完成后)
|
||||
// 加载优惠券:使用“初始数量”对应的总价做推荐,避免默认数量变化导致推荐不准
|
||||
if (goodsRes) {
|
||||
await loadUserCoupons()
|
||||
const initQty = (() => {
|
||||
const n = Number(goodsRes?.canBuyNumber)
|
||||
if (Number.isFinite(n) && n > 0) return Math.floor(n)
|
||||
if (tplActive) return tplMinBuyQty
|
||||
return 1
|
||||
})()
|
||||
const total = parseFloat(goodsRes.price || '0') * initQty
|
||||
await loadUserCoupons(total)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载数据失败:', err)
|
||||
@@ -351,10 +727,17 @@ const OrderConfirm = () => {
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
// 返回/切换到该页面时,刷新一下当前已选门店
|
||||
if (!isLoggedIn()) return
|
||||
setSelectedStore(getSelectedStoreFromStorage())
|
||||
loadAllData()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// 切换商品时重置配送时间,避免沿用上一次选择
|
||||
if (!isLoggedIn()) return
|
||||
setSendTime(getMinSendDate().toDate())
|
||||
setSendTimePickerVisible(false)
|
||||
loadAllData()
|
||||
}, [goodsId]);
|
||||
|
||||
@@ -413,6 +796,48 @@ const OrderConfirm = () => {
|
||||
)}
|
||||
</CellGroup>
|
||||
|
||||
{hasTicketTemplate && (
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title={'配送时间'}
|
||||
extra={(
|
||||
<View className={'flex items-center gap-2'}>
|
||||
<View className={'text-gray-900'}>{sendTimeText}</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)}
|
||||
onClick={() => {
|
||||
// 若页面停留跨过截单时间,打开选择器前再校正一次最早可选日期
|
||||
const min = getMinSendDate()
|
||||
if (dayjs(sendTime).isBefore(min, 'day')) {
|
||||
setSendTime(min.toDate())
|
||||
}
|
||||
setSendTimePickerVisible(true)
|
||||
}}
|
||||
/>
|
||||
</CellGroup>
|
||||
)}
|
||||
|
||||
{/*<CellGroup>*/}
|
||||
{/* <Cell*/}
|
||||
{/* title={(*/}
|
||||
{/* <View className="flex items-center gap-2">*/}
|
||||
{/* <Shop className={'text-gray-500'}/>*/}
|
||||
{/* <Text>门店</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/* extra={(*/}
|
||||
{/* <View className={'flex items-center gap-2'}>*/}
|
||||
{/* <View className={'text-gray-900'}>*/}
|
||||
{/* {selectedStore?.name || '请选择门店'}*/}
|
||||
{/* </View>*/}
|
||||
{/* <ArrowRight className={'text-gray-400'} size={14}/>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/* onClick={openStorePopup}*/}
|
||||
{/* />*/}
|
||||
{/*</CellGroup>*/}
|
||||
|
||||
<CellGroup>
|
||||
<Cell key={goods.goodsId}>
|
||||
<View className={'flex w-full justify-between gap-3'}>
|
||||
@@ -422,26 +847,34 @@ const OrderConfirm = () => {
|
||||
height: '80px',
|
||||
}} lazyLoad={false}/>
|
||||
</View>
|
||||
<View className={'flex flex-col w-full'} style={{width: '100%'}}>
|
||||
<View className={'flex flex-col w-full ml-2'} style={{width: '100%'}}>
|
||||
<Text className={'font-medium w-full'}>{goods.name}</Text>
|
||||
<Text className={'number text-gray-400 text-sm py-2'}>80g/袋</Text>
|
||||
{/*<Text className={'number text-gray-400 text-sm py-2'}>80g/袋</Text>*/}
|
||||
<View className={'flex justify-between items-center'}>
|
||||
<Text className={'text-red-500'}>¥{goods.price}</Text>
|
||||
<View className={'flex flex-col items-end gap-1'}>
|
||||
<ConfigProvider theme={customTheme}>
|
||||
<InputNumber
|
||||
value={quantity}
|
||||
min={1}
|
||||
max={goods.stock || 999}
|
||||
disabled={goods.canBuyNumber != 0}
|
||||
onChange={handleQuantityChange}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
<ConfigProvider theme={customTheme}>
|
||||
<InputNumber
|
||||
value={quantity}
|
||||
min={isTicketTemplateActive ? minBuyQty : 1}
|
||||
max={goods.stock || 999}
|
||||
step={minBuyQty === 1 ? 1 : 10}
|
||||
readOnly
|
||||
disabled={((goods.canBuyNumber ?? 0) !== 0) && !isTicketTemplateActive}
|
||||
onChange={handleQuantityChange}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
{goods.stock !== undefined && (
|
||||
<Text className={'text-xs text-gray-400'}>
|
||||
库存 {goods.stock} 件
|
||||
</Text>
|
||||
)}
|
||||
{isTicketTemplateActive && (
|
||||
<View className={'text-xs text-gray-500'}>
|
||||
<Text>最低购买量:{minBuyQty}桶</Text>
|
||||
<Text className={'ml-2'}>赠送水票:{getGiftTicketQty(quantity)}张</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -474,7 +907,31 @@ const OrderConfirm = () => {
|
||||
<View className={'text-red-500 text-sm mr-1'}>
|
||||
{selectedCoupon ? `-¥${getCouponDiscount().toFixed(2)}` : '暂未使用'}
|
||||
</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
{(() => {
|
||||
const usableCoupons = filterUsableCoupons(availableCoupons, getGoodsTotal())
|
||||
if (usableCoupons.length > 0 && !selectedCoupon) {
|
||||
return (
|
||||
<View className={'flex items-center'}>
|
||||
<View className={'bg-red-500 text-white text-xs px-2 py-1 rounded mr-2'}>
|
||||
{usableCoupons.length}张可用
|
||||
</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)
|
||||
} else if (usableCoupons.length > 0) {
|
||||
return (
|
||||
<View className={'flex items-center'}>
|
||||
<View className={'bg-green-500 text-white text-xs px-2 py-1 rounded mr-2'}>
|
||||
已选择
|
||||
</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
return <ArrowRight className={'text-gray-400'} size={14}/>
|
||||
}
|
||||
})()
|
||||
}
|
||||
</View>
|
||||
)}
|
||||
onClick={() => setCouponVisible(true)}
|
||||
@@ -502,6 +959,20 @@ const OrderConfirm = () => {
|
||||
)}/>
|
||||
</CellGroup>
|
||||
|
||||
{ticketTemplate && (
|
||||
<CellGroup>
|
||||
<Cell extra={(
|
||||
<div className={'text-red-500 text-sm'}>
|
||||
<Text>注意事项:</Text>
|
||||
<Text>最低起送量≥{ticketTemplate.startSendQty}桶;</Text>
|
||||
<Text>配送范围要在电子围栏内;</Text>
|
||||
<Text>上楼费暂不收取,收费另行通知。</Text>
|
||||
</div>
|
||||
)}/>
|
||||
</CellGroup>
|
||||
)}
|
||||
|
||||
|
||||
{/* 支付方式选择 */}
|
||||
<ActionSheet
|
||||
visible={isVisible}
|
||||
@@ -582,8 +1053,82 @@ const OrderConfirm = () => {
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* 门店选择弹窗 */}
|
||||
<Popup
|
||||
visible={storePopupVisible}
|
||||
position="bottom"
|
||||
style={{height: '70vh'}}
|
||||
onClose={() => setStorePopupVisible(false)}
|
||||
>
|
||||
<View className="p-4">
|
||||
<View className="flex justify-between items-center mb-3">
|
||||
<Text className="text-base font-medium">选择门店</Text>
|
||||
<Text
|
||||
className="text-sm text-gray-500"
|
||||
onClick={() => setStorePopupVisible(false)}
|
||||
>
|
||||
关闭
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{storeLoading ? (
|
||||
<View className="py-10 text-center text-gray-500">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<CellGroup>
|
||||
{stores.map((s) => {
|
||||
const isActive = !!selectedStore?.id && selectedStore.id === s.id
|
||||
return (
|
||||
<Cell
|
||||
key={s.id}
|
||||
title={<Text className={isActive ? 'text-green-600' : ''}>{s.name || `门店${s.id}`}</Text>}
|
||||
description={s.address || ''}
|
||||
onClick={async () => {
|
||||
let storeToSave: ShopStore = s
|
||||
if (s?.id) {
|
||||
try {
|
||||
const full = await getShopStore(s.id)
|
||||
if (full) storeToSave = full
|
||||
} catch (_e) {
|
||||
// keep base item
|
||||
}
|
||||
}
|
||||
setSelectedStore(storeToSave)
|
||||
saveSelectedStoreToStorage(storeToSave)
|
||||
setStorePopupVisible(false)
|
||||
Taro.showToast({title: '门店已切换', icon: 'success'})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{!stores.length && (
|
||||
<Cell title={<Text className="text-gray-500">暂无门店数据</Text>} />
|
||||
)}
|
||||
</CellGroup>
|
||||
)}
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
<Gap height={50}/>
|
||||
|
||||
<DatePicker
|
||||
visible={sendTimePickerVisible}
|
||||
title="选择配送时间"
|
||||
type="date"
|
||||
startDate={getMinSendDate().toDate()}
|
||||
endDate={dayjs().add(30, 'day').toDate()}
|
||||
value={sendTime}
|
||||
onClose={() => setSendTimePickerVisible(false)}
|
||||
onCancel={() => setSendTimePickerVisible(false)}
|
||||
onConfirm={(_options, selectedValue) => {
|
||||
const [y, m, d] = (selectedValue || []).map(v => Number(v))
|
||||
const next = new Date(y, (m || 1) - 1, d || 1, 0, 0, 0)
|
||||
setSendTime(next)
|
||||
setSendTimePickerVisible(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
|
||||
<View className={'btn-bar flex justify-between items-center'}>
|
||||
<div className={'flex flex-col justify-center items-start mx-4'}>
|
||||
@@ -602,6 +1147,7 @@ const OrderConfirm = () => {
|
||||
type="success"
|
||||
size="large"
|
||||
loading={payLoading}
|
||||
disabled={isTicketTemplateActive && quantity < minBuyQty}
|
||||
onClick={() => onPay(goods)}
|
||||
>
|
||||
{payLoading ? '支付中...' : '立即付款'}
|
||||
|
||||
Reference in New Issue
Block a user