diff --git a/src/api/glt/gltTicketOrder/model/index.ts b/src/api/glt/gltTicketOrder/model/index.ts index 33e5152..8fda597 100644 --- a/src/api/glt/gltTicketOrder/model/index.ts +++ b/src/api/glt/gltTicketOrder/model/index.ts @@ -4,20 +4,36 @@ import type { PageParam } from '@/api/index'; * 送水订单 */ export interface GltTicketOrder { - // + // id?: number; // 用户水票ID userTicketId?: number; // 门店ID storeId?: number; + // 门店名称 + storeName?: string; + // 门店地址 + storeAddress?: string; + // 门店电话 + storePhone?: string; // 配送员 riderId?: number; + // 配送员名称 + riderName?: string; + // 配送员电话 + riderPhone?: string; // 仓库ID warehouseId?: number; + // 仓库名称 + warehouseName?: string; + // 仓库地址 + warehouseAddress?: string; // 关联收货地址 addressId?: number; // 收货地址 address?: string; + // 配送时间 + sendTime?: string; // 买家留言 buyerRemarks?: string; // 用于统计 @@ -26,6 +42,12 @@ export interface GltTicketOrder { totalNum?: number; // 用户ID userId?: number; + // 昵称 + nickname?: string; + // 头像 + avatar?: string; + // 手机号码 + phone?: string; // 排序(数字越小越靠前) sortNumber?: number; // 备注 @@ -48,4 +70,5 @@ export interface GltTicketOrder { export interface GltTicketOrderParam extends PageParam { id?: number; keywords?: string; + userId?: number; } diff --git a/src/user/ticket/index.tsx b/src/user/ticket/index.tsx index caad438..2656986 100644 --- a/src/user/ticket/index.tsx +++ b/src/user/ticket/index.tsx @@ -16,8 +16,8 @@ import { import { View, Text, Image } from '@tarojs/components'; import { pageGltUserTicket } from '@/api/glt/gltUserTicket'; import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'; -import { pageGltUserTicketLog } from '@/api/glt/gltUserTicketLog'; -import type { GltUserTicketLog } from '@/api/glt/gltUserTicketLog/model'; +import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder'; +import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model'; import { BaseUrl } from '@/config/app'; import dayjs from "dayjs"; @@ -31,13 +31,13 @@ const UserTicketList = () => { const [ticketPage, setTicketPage] = useState(1); const [ticketTotal, setTicketTotal] = useState(0); - const [logList, setLogList] = useState([]); - const [logLoading, setLogLoading] = useState(false); - const [logHasMore, setLogHasMore] = useState(true); - const [logPage, setLogPage] = useState(1); - const [logTotal, setLogTotal] = useState(0); + const [orderList, setOrderList] = useState([]); + const [orderLoading, setOrderLoading] = useState(false); + const [orderHasMore, setOrderHasMore] = useState(true); + const [orderPage, setOrderPage] = useState(1); + const [orderTotal, setOrderTotal] = useState(0); - const [activeTab, setActiveTab] = useState<'ticket' | 'log'>('ticket'); + const [activeTab, setActiveTab] = useState<'ticket' | 'order'>('ticket'); const [qrVisible, setQrVisible] = useState(false); const [qrTicket, setQrTicket] = useState(null); @@ -154,27 +154,27 @@ const UserTicketList = () => { } }; - const reloadLogs = async (isRefresh = true, keywords?: string) => { - if (logLoading) return; + const reloadOrders = async (isRefresh = true, keywords?: string) => { + if (orderLoading) return; const userId = getUserId(); if (!userId) { - setLogList([]); - setLogTotal(0); - setLogHasMore(false); + setOrderList([]); + setOrderTotal(0); + setOrderHasMore(false); return; } if (isRefresh) { - setLogPage(1); - setLogList([]); - setLogHasMore(true); + setOrderPage(1); + setOrderList([]); + setOrderHasMore(true); } - setLogLoading(true); + setOrderLoading(true); try { - const currentPage = isRefresh ? 1 : logPage; - const res = await pageGltUserTicketLog({ + const currentPage = isRefresh ? 1 : orderPage; + const res = await pageGltTicketOrder({ page: currentPage, limit: PAGE_SIZE, userId, @@ -182,23 +182,23 @@ const UserTicketList = () => { }); const resList = res?.list || []; - const nextList = isRefresh ? resList : [...logList, ...resList]; - setLogList(nextList); + const nextList = isRefresh ? resList : [...orderList, ...resList]; + setOrderList(nextList); const count = typeof res?.count === 'number' ? res.count : nextList.length; - setLogTotal(count); - setLogHasMore(nextList.length < count); + setOrderTotal(count); + setOrderHasMore(nextList.length < count); if (resList.length > 0) { - setLogPage(currentPage + 1); + setOrderPage(currentPage + 1); } else { - setLogHasMore(false); + setOrderHasMore(false); } } catch (error) { - console.error('获取核销记录失败:', error); - Taro.showToast({ title: '获取核销记录失败', icon: 'error' }); - setLogHasMore(false); + console.error('获取送水订单失败:', error); + Taro.showToast({ title: '获取送水订单失败', icon: 'error' }); + setOrderHasMore(false); } finally { - setLogLoading(false); + setOrderLoading(false); } }; @@ -207,7 +207,7 @@ const UserTicketList = () => { if (activeTab === 'ticket') { reloadTickets(true, value); } else { - reloadLogs(true, value); + reloadOrders(true, value); } }; @@ -215,12 +215,12 @@ const UserTicketList = () => { if (activeTab === 'ticket') { await reloadTickets(true); } else { - await reloadLogs(true); + await reloadOrders(true); } }; const handleTabChange = (value: string | number) => { - const tab = String(value) as 'ticket' | 'log'; + const tab = String(value) as 'ticket' | 'order'; setActiveTab(tab); if (tab === 'ticket') { setTicketPage(1); @@ -228,10 +228,10 @@ const UserTicketList = () => { setTicketHasMore(true); reloadTickets(true); } else { - setLogPage(1); - setLogList([]); - setLogHasMore(true); - reloadLogs(true); + setOrderPage(1); + setOrderList([]); + setOrderHasMore(true); + reloadOrders(true); } }; @@ -241,23 +241,30 @@ const UserTicketList = () => { } }; - const loadMoreLogs = async () => { - if (!logLoading && logHasMore) { - await reloadLogs(false); + const loadMoreOrders = async () => { + if (!orderLoading && orderHasMore) { + await reloadOrders(false); } }; - const formatSigned = (n?: number) => { - const val = Number(n || 0); - if (val === 0) return '0'; - return val > 0 ? `+${val}` : `${val}`; + const formatDateTime = (v?: string) => { + if (!v) return '-'; + const d = dayjs(v); + return d.isValid() ? d.format('YYYY年MM月DD日 HH:mm:ss') : v; + }; + + const getOrderStatusText = (status?: number) => { + // Backend field meaning may vary; page asks for "是否送达". + if (status === 1) return { text: '已送达', type: 'success' as const }; + if (status === 0) return { text: '未送达', type: 'warning' as const }; + return { text: status == null ? '-' : String(status), type: 'primary' as const }; }; useDidShow(() => { if (activeTab === 'ticket') { reloadTickets(true).then(); } else { - reloadLogs(true).then(); + reloadOrders(true).then(); } }); @@ -277,7 +284,7 @@ const UserTicketList = () => { - + @@ -287,9 +294,9 @@ const UserTicketList = () => { )} - {activeTab === 'log' && logTotal > 0 && ( + {activeTab === 'order' && orderTotal > 0 && ( - 共 {logTotal} 条核销记录 + 共 {orderTotal} 条送水订单 )} @@ -306,12 +313,12 @@ const UserTicketList = () => { style={{ backgroundColor: 'transparent' }} /> - ) : activeTab === 'log' && logList.length === 0 && !logLoading ? ( + ) : activeTab === 'order' && orderList.length === 0 && !orderLoading ? ( - + ) : activeTab === 'ticket' ? ( { ) : ( @@ -403,68 +410,35 @@ const UserTicketList = () => { } loadMoreText={ - {logList.length === 0 ? '暂无数据' : '没有更多了'} + {orderList.length === 0 ? '暂无数据' : '没有更多了'} } > - {logList.map((item, index) => ( + {orderList.map((item, index) => ( - 票号:{item.userTicketId} + 票号:{item.userTicketId ?? '-'} - {item.createTime && ( - - 时间:{item.createTime} - - )} - {item.orderNo && ( - - 订单编号:{item.orderNo} - - )} - {item.comments && ( - - {item.comments} - - )} - - 变更 - - - - - 可用变更 - {formatSigned(item.changeAvailable)} - - - 已用变更 - {formatSigned(item.changeUsed)} - - - 冻结变更 - {formatSigned(item.changeFrozen)} - - - - - - 可用后 - {item.availableAfter ?? '-'} - - - 已用后 - {item.usedAfter ?? '-'} - - - 冻结后 - {item.frozenAfter ?? '-'} + + 下单时间:{formatDateTime(item.createTime)} + + + 配送时间:{formatDateTime(item.sendTime)} + + + 送水数量:{item.totalNum ?? 0} + + {(() => { + const meta = getOrderStatusText(item.status); + return {meta.text}; + })()} ))} diff --git a/src/user/ticket/use.tsx b/src/user/ticket/use.tsx index 2224340..ea8b6ad 100644 --- a/src/user/ticket/use.tsx +++ b/src/user/ticket/use.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' import { View, Text } from '@tarojs/components' import { @@ -6,12 +6,14 @@ import { Cell, CellGroup, ConfigProvider, + DatePicker, Input, InputNumber, Popup, Space } from '@nutui/nutui-react-taro' import { ArrowRight, Location, Shop, Ticket } from '@nutui/icons-react-taro' +import dayjs from 'dayjs' import type { ShopGoods } from '@/api/shop/shopGoods/model' import { getShopGoods } from '@/api/shop/shopGoods' import { listShopUserAddress } from '@/api/shop/shopUserAddress' @@ -26,14 +28,20 @@ import type { GltUserTicket } from '@/api/glt/gltUserTicket/model' import { listGltUserTicket } from '@/api/glt/gltUserTicket' import { addGltTicketOrder } from '@/api/glt/gltTicketOrder' +const MIN_START_QTY = 10 + const OrderConfirm = () => { const [goods, setGoods] = useState(null); const [address, setAddress] = useState() - const [quantity, setQuantity] = useState(1) + const [quantity, setQuantity] = useState(MIN_START_QTY) const [orderRemark, setOrderRemark] = useState('') + const [sendTime, setSendTime] = useState(new Date()) + const [sendTimePickerVisible, setSendTimePickerVisible] = useState(false) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [submitLoading, setSubmitLoading] = useState(false) + const loadAllDataLoadingRef = useRef(false) + const hasInitialLoadedRef = useRef(false) // InputNumber 主题配置 const customTheme = { @@ -101,10 +109,18 @@ const OrderConfirm = () => { return Math.max(0, Math.min(stockMax, availableTicketTotal)) }, [availableTicketTotal, goods?.stock]) + const canStartOrder = useMemo(() => { + return maxQuantity >= MIN_START_QTY + }, [maxQuantity]) + const displayQty = useMemo(() => { - if (maxQuantity <= 0) return 0 - return Math.max(1, Math.min(quantity, maxQuantity)) - }, [quantity, maxQuantity]) + if (!canStartOrder) return 0 + return Math.max(MIN_START_QTY, Math.min(quantity, maxQuantity)) + }, [quantity, maxQuantity, canStartOrder]) + + const sendTimeText = useMemo(() => { + return dayjs(sendTime).format('YYYY-MM-DD HH:mm') + }, [sendTime]) const loadStores = async () => { if (storeLoading) return @@ -133,11 +149,11 @@ const OrderConfirm = () => { const parsed = typeof value === 'string' ? parseInt(value) : value const newQuantity = Number.isFinite(parsed) ? Number(parsed) : 0 const upper = maxQuantity - if (upper <= 0) { + if (!canStartOrder || upper <= 0) { setQuantity(0) return } - setQuantity(Math.max(1, Math.min(newQuantity || 1, upper))) + setQuantity(Math.max(MIN_START_QTY, Math.min(newQuantity || MIN_START_QTY, upper))) } const loadUserTickets = async () => { @@ -198,10 +214,18 @@ const OrderConfirm = () => { Taro.showToast({ title: '商品库存不足', icon: 'none' }) return } + if (finalQty < MIN_START_QTY) { + Taro.showToast({ title: `最低起送 ${MIN_START_QTY} 桶`, icon: 'none' }) + return + } + if (!sendTime) { + Taro.showToast({ title: '请选择配送时间', icon: 'none' }) + return + } const confirmRes = await Taro.showModal({ title: '确认下单', - content: `将使用 ${finalQty} 张水票下单,送水 ${finalQty} 桶,是否确认?` + content: `配送时间:${sendTimeText}\n将使用 ${finalQty} 张水票下单,送水 ${finalQty} 桶,是否确认?` }) if (!confirmRes.confirm) return @@ -215,6 +239,7 @@ const OrderConfirm = () => { addressId: address.id, totalNum: finalQty, buyerRemarks: orderRemark, + sendTime: dayjs(sendTime).format('YYYY-MM-DD HH:mm:ss'), // Backend may take userId from token; pass-through is harmless if backend ignores it. userId, comments: goods.name ? `立即送水:${goods.name}` : '立即送水' @@ -237,17 +262,15 @@ const OrderConfirm = () => { } // 统一的数据加载函数 - const loadAllData = async () => { + const loadAllData = async (opts?: { silent?: boolean }) => { + if (loadAllDataLoadingRef.current) return + loadAllDataLoadingRef.current = true try { - setLoading(true) + if (!opts?.silent) setLoading(true) setError('') - let goodsRes: ShopGoods | null = null - if (numericGoodsId) { - goodsRes = await getShopGoods(numericGoodsId) - } - - const [addressRes] = await Promise.all([ + const [goodsRes, addressRes] = await Promise.all([ + numericGoodsId ? getShopGoods(numericGoodsId) : Promise.resolve(null), listShopUserAddress({ isDefault: true }) ]) @@ -260,30 +283,34 @@ const OrderConfirm = () => { if (addressRes && addressRes.length > 0) { setAddress(addressRes[0]) } - await loadUserTickets() + hasInitialLoadedRef.current = true + // Tickets are non-blocking for first paint; load in background. + loadUserTickets() } catch (err) { console.error('加载数据失败:', err) - setError('加载数据失败,请重试') + if (opts?.silent) { + Taro.showToast({ title: '刷新失败,请稍后重试', icon: 'none' }) + } else { + setError('加载数据失败,请重试') + } } finally { - setLoading(false) + if (!opts?.silent) setLoading(false) + loadAllDataLoadingRef.current = false } } useDidShow(() => { // 返回/切换到该页面时,刷新一下当前已选门店 setSelectedStore(getSelectedStoreFromStorage()) - loadAllData() + loadAllData({ silent: hasInitialLoadedRef.current }) }) - useEffect(() => { - loadAllData() - }, [goodsId]); - // When tickets/stock change, clamp quantity into [0..maxQuantity]. useEffect(() => { setQuantity(prev => { if (maxQuantity <= 0) return 0 - if (!prev || prev < 1) return 1 + if (maxQuantity < MIN_START_QTY) return 0 + if (!prev || prev < MIN_START_QTY) return MIN_START_QTY return Math.min(prev, maxQuantity) }) }, [maxQuantity]) @@ -374,16 +401,34 @@ const OrderConfirm = () => { )} + + + {sendTimeText} + + + )} + onClick={() => setSendTimePickerVisible(true)} + /> + + @@ -402,12 +447,18 @@ const OrderConfirm = () => { extra={( - {selectedTicket ? `${selectedTicket.templateName || '水票'}(可用${selectedTicket.availableQty ?? 0})` : '请选择'} + {ticketLoading + ? '加载中...' + : (selectedTicket + ? `${selectedTicket.templateName || '水票'}(可用${selectedTicket.availableQty ?? 0})` + : '请选择' + ) + } )} - onClick={() => setTicketPopupVisible(true)} + onClick={() => !ticketLoading && setTicketPopupVisible(true)} /> { + setSendTimePickerVisible(false)} + onCancel={() => setSendTimePickerVisible(false)} + onConfirm={(_options, selectedValue) => { + const [y, m, d, hh, mm] = (selectedValue || []).map(v => Number(v)) + const next = new Date(y, (m || 1) - 1, d || 1, hh || 0, mm || 0) + setSendTime(next) + setSendTimePickerVisible(false) + }} + /> +
@@ -549,7 +617,7 @@ const OrderConfirm = () => { type="success" size="large" loading={submitLoading} - disabled={!selectedTicket?.id || availableTicketTotal <= 0 || maxQuantity <= 0} + disabled={!selectedTicket?.id || availableTicketTotal <= 0 || !canStartOrder} onClick={onSubmit} > {submitLoading ? '提交中...' : '立即提交'}