feat(ticket): 实现基于模板配置的动态起送数量功能

- 引入 gltTicketTemplate API 获取模板配置
- 将固定起送数量改为动态可配置的最小起送数量
- 添加基于商品ID或票据模板ID获取起送配置的功能
- 实现页面初始化时从票据模板加载起送数量配置
- 更新用户界面显示实际的动态起送数量要求
- 添加异步加载和取消请求的安全处理机制
This commit is contained in:
2026-03-10 12:11:48 +08:00
parent 0c9a03d656
commit 00f3954012

View File

@@ -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}