feat(ticket): 实现基于模板配置的动态起送数量功能
- 引入 gltTicketTemplate API 获取模板配置 - 将固定起送数量改为动态可配置的最小起送数量 - 添加基于商品ID或票据模板ID获取起送配置的功能 - 实现页面初始化时从票据模板加载起送数量配置 - 更新用户界面显示实际的动态起送数量要求 - 添加异步加载和取消请求的安全处理机制
This commit is contained in:
@@ -26,6 +26,7 @@ import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
|||||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
|
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
|
||||||
import { getGltUserTicket, listGltUserTicket } from '@/api/glt/gltUserTicket'
|
import { getGltUserTicket, listGltUserTicket } from '@/api/glt/gltUserTicket'
|
||||||
|
import { getGltTicketTemplate, getGltTicketTemplateByGoodsId } from '@/api/glt/gltTicketTemplate'
|
||||||
import { addGltTicketOrder, getGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
import { addGltTicketOrder, getGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model'
|
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model'
|
||||||
@@ -35,13 +36,14 @@ import type { ShopStoreFence } from '@/api/shop/shopStoreFence/model'
|
|||||||
import { listShopStoreFence } from '@/api/shop/shopStoreFence'
|
import { listShopStoreFence } from '@/api/shop/shopStoreFence'
|
||||||
import { parseFencePoints, parseLngLatFromText, pointInAnyPolygon, pointInPolygon } from '@/utils/geofence'
|
import { parseFencePoints, parseLngLatFromText, pointInAnyPolygon, pointInPolygon } from '@/utils/geofence'
|
||||||
|
|
||||||
const MIN_START_QTY = 10
|
const DEFAULT_MIN_START_QTY = 10
|
||||||
const ADDRESS_CHANGE_COOLDOWN_DAYS = 30
|
const ADDRESS_CHANGE_COOLDOWN_DAYS = 30
|
||||||
|
|
||||||
const OrderConfirm = () => {
|
const OrderConfirm = () => {
|
||||||
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
||||||
const [address, setAddress] = useState<ShopUserAddress>()
|
const [address, setAddress] = useState<ShopUserAddress>()
|
||||||
const [quantity, setQuantity] = useState<number>(MIN_START_QTY)
|
const [minStartQty, setMinStartQty] = useState<number>(DEFAULT_MIN_START_QTY)
|
||||||
|
const [quantity, setQuantity] = useState<number>(DEFAULT_MIN_START_QTY)
|
||||||
const [orderRemark, setOrderRemark] = useState<string>('')
|
const [orderRemark, setOrderRemark] = useState<string>('')
|
||||||
// Delivery date only (no hour/min selection).
|
// Delivery date only (no hour/min selection).
|
||||||
const [sendTime, setSendTime] = useState<Date>(() => dayjs().startOf('day').toDate())
|
const [sendTime, setSendTime] = useState<Date>(() => dayjs().startOf('day').toDate())
|
||||||
@@ -325,13 +327,13 @@ const OrderConfirm = () => {
|
|||||||
}, [availableTicketTotal, editingOrder?.totalNum, editingOrder?.userTicketId, goods?.stock, isEditMode, tickets])
|
}, [availableTicketTotal, editingOrder?.totalNum, editingOrder?.userTicketId, goods?.stock, isEditMode, tickets])
|
||||||
|
|
||||||
const canStartOrder = useMemo(() => {
|
const canStartOrder = useMemo(() => {
|
||||||
return maxQuantity >= MIN_START_QTY
|
return maxQuantity >= minStartQty
|
||||||
}, [maxQuantity])
|
}, [maxQuantity, minStartQty])
|
||||||
|
|
||||||
const displayQty = useMemo(() => {
|
const displayQty = useMemo(() => {
|
||||||
if (!canStartOrder) return 0
|
if (!canStartOrder) return 0
|
||||||
return Math.max(MIN_START_QTY, Math.min(quantity, maxQuantity))
|
return Math.max(minStartQty, Math.min(quantity, maxQuantity))
|
||||||
}, [quantity, maxQuantity, canStartOrder])
|
}, [quantity, maxQuantity, canStartOrder, minStartQty])
|
||||||
|
|
||||||
const sendTimeText = useMemo(() => {
|
const sendTimeText = useMemo(() => {
|
||||||
return dayjs(sendTime).format('YYYY-MM-DD')
|
return dayjs(sendTime).format('YYYY-MM-DD')
|
||||||
@@ -600,7 +602,7 @@ const OrderConfirm = () => {
|
|||||||
setQuantity(0)
|
setQuantity(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setQuantity(Math.max(MIN_START_QTY, Math.min(newQuantity || MIN_START_QTY, upper)))
|
setQuantity(Math.max(minStartQty, Math.min(newQuantity || minStartQty, upper)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadUserTickets = async () => {
|
const loadUserTickets = async () => {
|
||||||
@@ -718,8 +720,8 @@ const OrderConfirm = () => {
|
|||||||
Taro.showToast({ title: '商品库存不足', icon: 'none' })
|
Taro.showToast({ title: '商品库存不足', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (finalQty < MIN_START_QTY) {
|
if (finalQty < minStartQty) {
|
||||||
Taro.showToast({ title: `最低起送 ${MIN_START_QTY} 桶`, icon: 'none' })
|
Taro.showToast({ title: `最低起送 ${minStartQty} 桶`, icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!sendTime) {
|
if (!sendTime) {
|
||||||
@@ -863,8 +865,8 @@ const OrderConfirm = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const initQty = Number(editingOrderRes.totalNum ?? MIN_START_QTY)
|
const initQty = Number(editingOrderRes.totalNum ?? minStartQty)
|
||||||
setQuantity(Number.isFinite(initQty) && initQty > 0 ? initQty : MIN_START_QTY)
|
setQuantity(Number.isFinite(initQty) && initQty > 0 ? initQty : minStartQty)
|
||||||
setOrderRemark(String(editingOrderRes.buyerRemarks || ''))
|
setOrderRemark(String(editingOrderRes.buyerRemarks || ''))
|
||||||
const st = parseTime(editingOrderRes.sendTime)
|
const st = parseTime(editingOrderRes.sendTime)
|
||||||
if (st) setSendTime(st.startOf('day').toDate())
|
if (st) setSendTime(st.startOf('day').toDate())
|
||||||
@@ -1000,11 +1002,54 @@ const OrderConfirm = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQuantity(prev => {
|
setQuantity(prev => {
|
||||||
if (maxQuantity <= 0) return 0
|
if (maxQuantity <= 0) return 0
|
||||||
if (maxQuantity < MIN_START_QTY) return 0
|
if (maxQuantity < minStartQty) return 0
|
||||||
if (!prev || prev < MIN_START_QTY) return MIN_START_QTY
|
if (!prev || prev < minStartQty) return minStartQty
|
||||||
return Math.min(prev, maxQuantity)
|
return Math.min(prev, maxQuantity)
|
||||||
})
|
})
|
||||||
}, [maxQuantity])
|
}, [maxQuantity, minStartQty])
|
||||||
|
|
||||||
|
const minStartQtyKey = useMemo(() => {
|
||||||
|
const gid = Number(goods?.goodsId)
|
||||||
|
if (Number.isFinite(gid) && gid > 0) return `g:${gid}`
|
||||||
|
|
||||||
|
// If there is exactly one ticket template available, infer min start qty from it (covers "稍后再送" without goodsId).
|
||||||
|
const ids = Array.from(
|
||||||
|
new Set(
|
||||||
|
(usableTickets || [])
|
||||||
|
.map(t => Number(t?.templateId))
|
||||||
|
.filter(id => Number.isFinite(id) && id > 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (ids.length === 1) return `t:${ids[0]}`
|
||||||
|
return ''
|
||||||
|
}, [goods?.goodsId, usableTickets])
|
||||||
|
|
||||||
|
// Use configured min start-send qty from ticket template (by goodsId or by user's unique templateId).
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
if (!minStartQtyKey) {
|
||||||
|
setMinStartQty(DEFAULT_MIN_START_QTY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [kind, rawId] = minStartQtyKey.split(':')
|
||||||
|
const id = Number(rawId)
|
||||||
|
const tpl =
|
||||||
|
kind === 'g'
|
||||||
|
? await getGltTicketTemplateByGoodsId(id)
|
||||||
|
: await getGltTicketTemplate(id)
|
||||||
|
const n = Number(tpl?.startSendQty)
|
||||||
|
const safe = Number.isFinite(n) && n > 0 ? n : DEFAULT_MIN_START_QTY
|
||||||
|
if (!cancelled) setMinStartQty(safe)
|
||||||
|
} catch (_e) {
|
||||||
|
if (!cancelled) setMinStartQty(DEFAULT_MIN_START_QTY)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [minStartQtyKey])
|
||||||
|
|
||||||
// If user has no usable tickets, proactively guide them to purchase (only once per page lifecycle).
|
// If user has no usable tickets, proactively guide them to purchase (only once per page lifecycle).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1138,16 +1183,16 @@ const OrderConfirm = () => {
|
|||||||
title={'送水数量'}
|
title={'送水数量'}
|
||||||
description={
|
description={
|
||||||
canStartOrder
|
canStartOrder
|
||||||
? `最低起送 ${MIN_START_QTY} 桶`
|
? `最低起送 ${minStartQty} 桶`
|
||||||
: `最低起送 ${MIN_START_QTY} 桶(当前最多 ${maxQuantity} 桶)`
|
: `最低起送 ${minStartQty} 桶(当前最多 ${maxQuantity} 桶)`
|
||||||
}
|
}
|
||||||
extra={(
|
extra={(
|
||||||
<ConfigProvider theme={customTheme}>
|
<ConfigProvider theme={customTheme}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={displayQty}
|
value={displayQty}
|
||||||
min={canStartOrder ? MIN_START_QTY : 0}
|
min={canStartOrder ? minStartQty : 0}
|
||||||
max={canStartOrder ? maxQuantity : 0}
|
max={canStartOrder ? maxQuantity : 0}
|
||||||
step={10}
|
step={minStartQty >= 10 ? 10 : 1}
|
||||||
readOnly
|
readOnly
|
||||||
disabled={!canStartOrder}
|
disabled={!canStartOrder}
|
||||||
onChange={handleQuantityChange}
|
onChange={handleQuantityChange}
|
||||||
|
|||||||
Reference in New Issue
Block a user