feat(ticket): 将核销记录替换为送水订单功能并优化用户体验
- 替换核销记录为送水订单展示功能 - 在订单模型中新增门店、配送员、仓库的名称和联系方式字段 - 添加用户昵称、头像、手机号等个人信息字段 - 实现配送时间选择器功能 - 设置最低起送数量限制为10桶 - 优化订单列表展示界面和交互逻辑 - 添加订单状态显示功能 - 实现订单数据分页加载和搜索功能 - 优化页面数据加载性能,支持静默刷新
This commit is contained in:
@@ -4,20 +4,36 @@ import type { PageParam } from '@/api/index';
|
|||||||
* 送水订单
|
* 送水订单
|
||||||
*/
|
*/
|
||||||
export interface GltTicketOrder {
|
export interface GltTicketOrder {
|
||||||
//
|
//
|
||||||
id?: number;
|
id?: number;
|
||||||
// 用户水票ID
|
// 用户水票ID
|
||||||
userTicketId?: number;
|
userTicketId?: number;
|
||||||
// 门店ID
|
// 门店ID
|
||||||
storeId?: number;
|
storeId?: number;
|
||||||
|
// 门店名称
|
||||||
|
storeName?: string;
|
||||||
|
// 门店地址
|
||||||
|
storeAddress?: string;
|
||||||
|
// 门店电话
|
||||||
|
storePhone?: string;
|
||||||
// 配送员
|
// 配送员
|
||||||
riderId?: number;
|
riderId?: number;
|
||||||
|
// 配送员名称
|
||||||
|
riderName?: string;
|
||||||
|
// 配送员电话
|
||||||
|
riderPhone?: string;
|
||||||
// 仓库ID
|
// 仓库ID
|
||||||
warehouseId?: number;
|
warehouseId?: number;
|
||||||
|
// 仓库名称
|
||||||
|
warehouseName?: string;
|
||||||
|
// 仓库地址
|
||||||
|
warehouseAddress?: string;
|
||||||
// 关联收货地址
|
// 关联收货地址
|
||||||
addressId?: number;
|
addressId?: number;
|
||||||
// 收货地址
|
// 收货地址
|
||||||
address?: string;
|
address?: string;
|
||||||
|
// 配送时间
|
||||||
|
sendTime?: string;
|
||||||
// 买家留言
|
// 买家留言
|
||||||
buyerRemarks?: string;
|
buyerRemarks?: string;
|
||||||
// 用于统计
|
// 用于统计
|
||||||
@@ -26,6 +42,12 @@ export interface GltTicketOrder {
|
|||||||
totalNum?: number;
|
totalNum?: number;
|
||||||
// 用户ID
|
// 用户ID
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
// 昵称
|
||||||
|
nickname?: string;
|
||||||
|
// 头像
|
||||||
|
avatar?: string;
|
||||||
|
// 手机号码
|
||||||
|
phone?: string;
|
||||||
// 排序(数字越小越靠前)
|
// 排序(数字越小越靠前)
|
||||||
sortNumber?: number;
|
sortNumber?: number;
|
||||||
// 备注
|
// 备注
|
||||||
@@ -48,4 +70,5 @@ export interface GltTicketOrder {
|
|||||||
export interface GltTicketOrderParam extends PageParam {
|
export interface GltTicketOrderParam extends PageParam {
|
||||||
id?: number;
|
id?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
|
userId?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
import { View, Text, Image } from '@tarojs/components';
|
import { View, Text, Image } from '@tarojs/components';
|
||||||
import { pageGltUserTicket } from '@/api/glt/gltUserTicket';
|
import { pageGltUserTicket } from '@/api/glt/gltUserTicket';
|
||||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
||||||
import { pageGltUserTicketLog } from '@/api/glt/gltUserTicketLog';
|
import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
||||||
import type { GltUserTicketLog } from '@/api/glt/gltUserTicketLog/model';
|
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model';
|
||||||
import { BaseUrl } from '@/config/app';
|
import { BaseUrl } from '@/config/app';
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -31,13 +31,13 @@ const UserTicketList = () => {
|
|||||||
const [ticketPage, setTicketPage] = useState(1);
|
const [ticketPage, setTicketPage] = useState(1);
|
||||||
const [ticketTotal, setTicketTotal] = useState(0);
|
const [ticketTotal, setTicketTotal] = useState(0);
|
||||||
|
|
||||||
const [logList, setLogList] = useState<GltUserTicketLog[]>([]);
|
const [orderList, setOrderList] = useState<GltTicketOrder[]>([]);
|
||||||
const [logLoading, setLogLoading] = useState(false);
|
const [orderLoading, setOrderLoading] = useState(false);
|
||||||
const [logHasMore, setLogHasMore] = useState(true);
|
const [orderHasMore, setOrderHasMore] = useState(true);
|
||||||
const [logPage, setLogPage] = useState(1);
|
const [orderPage, setOrderPage] = useState(1);
|
||||||
const [logTotal, setLogTotal] = useState(0);
|
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 [qrVisible, setQrVisible] = useState(false);
|
||||||
const [qrTicket, setQrTicket] = useState<GltUserTicket | null>(null);
|
const [qrTicket, setQrTicket] = useState<GltUserTicket | null>(null);
|
||||||
@@ -154,27 +154,27 @@ const UserTicketList = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadLogs = async (isRefresh = true, keywords?: string) => {
|
const reloadOrders = async (isRefresh = true, keywords?: string) => {
|
||||||
if (logLoading) return;
|
if (orderLoading) return;
|
||||||
|
|
||||||
const userId = getUserId();
|
const userId = getUserId();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
setLogList([]);
|
setOrderList([]);
|
||||||
setLogTotal(0);
|
setOrderTotal(0);
|
||||||
setLogHasMore(false);
|
setOrderHasMore(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRefresh) {
|
if (isRefresh) {
|
||||||
setLogPage(1);
|
setOrderPage(1);
|
||||||
setLogList([]);
|
setOrderList([]);
|
||||||
setLogHasMore(true);
|
setOrderHasMore(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLogLoading(true);
|
setOrderLoading(true);
|
||||||
try {
|
try {
|
||||||
const currentPage = isRefresh ? 1 : logPage;
|
const currentPage = isRefresh ? 1 : orderPage;
|
||||||
const res = await pageGltUserTicketLog({
|
const res = await pageGltTicketOrder({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
userId,
|
userId,
|
||||||
@@ -182,23 +182,23 @@ const UserTicketList = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resList = res?.list || [];
|
const resList = res?.list || [];
|
||||||
const nextList = isRefresh ? resList : [...logList, ...resList];
|
const nextList = isRefresh ? resList : [...orderList, ...resList];
|
||||||
setLogList(nextList);
|
setOrderList(nextList);
|
||||||
const count = typeof res?.count === 'number' ? res.count : nextList.length;
|
const count = typeof res?.count === 'number' ? res.count : nextList.length;
|
||||||
setLogTotal(count);
|
setOrderTotal(count);
|
||||||
setLogHasMore(nextList.length < count);
|
setOrderHasMore(nextList.length < count);
|
||||||
|
|
||||||
if (resList.length > 0) {
|
if (resList.length > 0) {
|
||||||
setLogPage(currentPage + 1);
|
setOrderPage(currentPage + 1);
|
||||||
} else {
|
} else {
|
||||||
setLogHasMore(false);
|
setOrderHasMore(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取核销记录失败:', error);
|
console.error('获取送水订单失败:', error);
|
||||||
Taro.showToast({ title: '获取核销记录失败', icon: 'error' });
|
Taro.showToast({ title: '获取送水订单失败', icon: 'error' });
|
||||||
setLogHasMore(false);
|
setOrderHasMore(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLogLoading(false);
|
setOrderLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ const UserTicketList = () => {
|
|||||||
if (activeTab === 'ticket') {
|
if (activeTab === 'ticket') {
|
||||||
reloadTickets(true, value);
|
reloadTickets(true, value);
|
||||||
} else {
|
} else {
|
||||||
reloadLogs(true, value);
|
reloadOrders(true, value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -215,12 +215,12 @@ const UserTicketList = () => {
|
|||||||
if (activeTab === 'ticket') {
|
if (activeTab === 'ticket') {
|
||||||
await reloadTickets(true);
|
await reloadTickets(true);
|
||||||
} else {
|
} else {
|
||||||
await reloadLogs(true);
|
await reloadOrders(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabChange = (value: string | number) => {
|
const handleTabChange = (value: string | number) => {
|
||||||
const tab = String(value) as 'ticket' | 'log';
|
const tab = String(value) as 'ticket' | 'order';
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
if (tab === 'ticket') {
|
if (tab === 'ticket') {
|
||||||
setTicketPage(1);
|
setTicketPage(1);
|
||||||
@@ -228,10 +228,10 @@ const UserTicketList = () => {
|
|||||||
setTicketHasMore(true);
|
setTicketHasMore(true);
|
||||||
reloadTickets(true);
|
reloadTickets(true);
|
||||||
} else {
|
} else {
|
||||||
setLogPage(1);
|
setOrderPage(1);
|
||||||
setLogList([]);
|
setOrderList([]);
|
||||||
setLogHasMore(true);
|
setOrderHasMore(true);
|
||||||
reloadLogs(true);
|
reloadOrders(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,23 +241,30 @@ const UserTicketList = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMoreLogs = async () => {
|
const loadMoreOrders = async () => {
|
||||||
if (!logLoading && logHasMore) {
|
if (!orderLoading && orderHasMore) {
|
||||||
await reloadLogs(false);
|
await reloadOrders(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatSigned = (n?: number) => {
|
const formatDateTime = (v?: string) => {
|
||||||
const val = Number(n || 0);
|
if (!v) return '-';
|
||||||
if (val === 0) return '0';
|
const d = dayjs(v);
|
||||||
return val > 0 ? `+${val}` : `${val}`;
|
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(() => {
|
useDidShow(() => {
|
||||||
if (activeTab === 'ticket') {
|
if (activeTab === 'ticket') {
|
||||||
reloadTickets(true).then();
|
reloadTickets(true).then();
|
||||||
} else {
|
} else {
|
||||||
reloadLogs(true).then();
|
reloadOrders(true).then();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -277,7 +284,7 @@ const UserTicketList = () => {
|
|||||||
<View className="bg-white">
|
<View className="bg-white">
|
||||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
<TabPane title="我的水票" value="ticket"></TabPane>
|
<TabPane title="我的水票" value="ticket"></TabPane>
|
||||||
<TabPane title="核销记录" value="log"></TabPane>
|
<TabPane title="送水订单" value="order"></TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -287,9 +294,9 @@ const UserTicketList = () => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'log' && logTotal > 0 && (
|
{activeTab === 'order' && orderTotal > 0 && (
|
||||||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50 hidden">
|
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50 hidden">
|
||||||
共 {logTotal} 条核销记录
|
共 {orderTotal} 条送水订单
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -306,12 +313,12 @@ const UserTicketList = () => {
|
|||||||
style={{ backgroundColor: 'transparent' }}
|
style={{ backgroundColor: 'transparent' }}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : activeTab === 'log' && logList.length === 0 && !logLoading ? (
|
) : activeTab === 'order' && orderList.length === 0 && !orderLoading ? (
|
||||||
<View
|
<View
|
||||||
className="flex flex-col justify-center items-center"
|
className="flex flex-col justify-center items-center"
|
||||||
style={{ height: 'calc(100vh - 160px)' }}
|
style={{ height: 'calc(100vh - 160px)' }}
|
||||||
>
|
>
|
||||||
<Empty description="暂无核销记录" style={{ backgroundColor: 'transparent' }} />
|
<Empty description="暂无送水订单" style={{ backgroundColor: 'transparent' }} />
|
||||||
</View>
|
</View>
|
||||||
) : activeTab === 'ticket' ? (
|
) : activeTab === 'ticket' ? (
|
||||||
<InfiniteLoading
|
<InfiniteLoading
|
||||||
@@ -393,8 +400,8 @@ const UserTicketList = () => {
|
|||||||
) : (
|
) : (
|
||||||
<InfiniteLoading
|
<InfiniteLoading
|
||||||
target="ticket-scroll"
|
target="ticket-scroll"
|
||||||
hasMore={logHasMore}
|
hasMore={orderHasMore}
|
||||||
onLoadMore={loadMoreLogs}
|
onLoadMore={loadMoreOrders}
|
||||||
loadingText={
|
loadingText={
|
||||||
<View className="flex justify-center items-center py-4">
|
<View className="flex justify-center items-center py-4">
|
||||||
<Loading />
|
<Loading />
|
||||||
@@ -403,68 +410,35 @@ const UserTicketList = () => {
|
|||||||
}
|
}
|
||||||
loadMoreText={
|
loadMoreText={
|
||||||
<View className="text-center py-4 text-gray-500">
|
<View className="text-center py-4 text-gray-500">
|
||||||
{logList.length === 0 ? '暂无数据' : '没有更多了'}
|
{orderList.length === 0 ? '暂无数据' : '没有更多了'}
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View className="px-4 py-3">
|
<View className="px-4 py-3">
|
||||||
{logList.map((item, index) => (
|
{orderList.map((item, index) => (
|
||||||
<View
|
<View
|
||||||
key={String(item.id ?? `log-${index}`)}
|
key={String(item.id ?? `order-${index}`)}
|
||||||
className="bg-white rounded-xl p-4 mb-3"
|
className="bg-white rounded-xl p-4 mb-3"
|
||||||
>
|
>
|
||||||
<View className="flex items-start justify-between">
|
<View className="flex items-start justify-between">
|
||||||
<View className="flex-1 pr-3">
|
<View className="flex-1 pr-3">
|
||||||
<Text className="text-base font-semibold text-gray-900">
|
<Text className="text-base font-semibold text-gray-900">
|
||||||
票号:{item.userTicketId}
|
票号:{item.userTicketId ?? '-'}
|
||||||
</Text>
|
</Text>
|
||||||
{item.createTime && (
|
<View className="mt-1">
|
||||||
<View className="mt-1">
|
<Text className="text-xs text-gray-400">下单时间:{formatDateTime(item.createTime)}</Text>
|
||||||
<Text className="text-xs text-gray-400">时间:{item.createTime}</Text>
|
</View>
|
||||||
</View>
|
<View className="mt-1">
|
||||||
)}
|
<Text className="text-xs text-gray-400">配送时间:{formatDateTime(item.sendTime)}</Text>
|
||||||
{item.orderNo && (
|
</View>
|
||||||
<View className="mt-1">
|
<View className="mt-1">
|
||||||
<Text className="text-xs text-gray-500">订单编号:{item.orderNo}</Text>
|
<Text className="text-xs text-gray-500">送水数量:{item.totalNum ?? 0}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
{item.comments && (
|
|
||||||
<View className="mt-1">
|
|
||||||
<Text className="text-xs text-gray-500">{item.comments}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<Tag type="warning">变更</Tag>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="mt-3 flex justify-between">
|
|
||||||
<View className="flex flex-col">
|
|
||||||
<Text className="text-xs text-gray-500">可用变更</Text>
|
|
||||||
<Text className="text-sm text-gray-900">{formatSigned(item.changeAvailable)}</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex flex-col items-center">
|
|
||||||
<Text className="text-xs text-gray-500">已用变更</Text>
|
|
||||||
<Text className="text-sm text-gray-900">{formatSigned(item.changeUsed)}</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex flex-col items-end">
|
|
||||||
<Text className="text-xs text-gray-500">冻结变更</Text>
|
|
||||||
<Text className="text-sm text-gray-900">{formatSigned(item.changeFrozen)}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="mt-2 flex justify-between">
|
|
||||||
<View className="flex flex-col">
|
|
||||||
<Text className="text-xs text-gray-500">可用后</Text>
|
|
||||||
<Text className="text-sm text-gray-900">{item.availableAfter ?? '-'}</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex flex-col items-center">
|
|
||||||
<Text className="text-xs text-gray-500">已用后</Text>
|
|
||||||
<Text className="text-sm text-gray-900">{item.usedAfter ?? '-'}</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex flex-col items-end">
|
|
||||||
<Text className="text-xs text-gray-500">冻结后</Text>
|
|
||||||
<Text className="text-sm text-gray-900">{item.frozenAfter ?? '-'}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
{(() => {
|
||||||
|
const meta = getOrderStatusText(item.status);
|
||||||
|
return <Tag type={meta.type}>{meta.text}</Tag>;
|
||||||
|
})()}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import Taro, { useDidShow } from '@tarojs/taro'
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import {
|
import {
|
||||||
@@ -6,12 +6,14 @@ import {
|
|||||||
Cell,
|
Cell,
|
||||||
CellGroup,
|
CellGroup,
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
|
DatePicker,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Popup,
|
Popup,
|
||||||
Space
|
Space
|
||||||
} from '@nutui/nutui-react-taro'
|
} from '@nutui/nutui-react-taro'
|
||||||
import { ArrowRight, Location, Shop, Ticket } from '@nutui/icons-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 type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||||
import { getShopGoods } from '@/api/shop/shopGoods'
|
import { getShopGoods } from '@/api/shop/shopGoods'
|
||||||
import { listShopUserAddress } from '@/api/shop/shopUserAddress'
|
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 { listGltUserTicket } from '@/api/glt/gltUserTicket'
|
||||||
import { addGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
import { addGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
|
|
||||||
|
const MIN_START_QTY = 10
|
||||||
|
|
||||||
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>(1)
|
const [quantity, setQuantity] = useState<number>(MIN_START_QTY)
|
||||||
const [orderRemark, setOrderRemark] = useState<string>('')
|
const [orderRemark, setOrderRemark] = useState<string>('')
|
||||||
|
const [sendTime, setSendTime] = useState<Date>(new Date())
|
||||||
|
const [sendTimePickerVisible, setSendTimePickerVisible] = useState(false)
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<string>('')
|
||||||
const [submitLoading, setSubmitLoading] = useState<boolean>(false)
|
const [submitLoading, setSubmitLoading] = useState<boolean>(false)
|
||||||
|
const loadAllDataLoadingRef = useRef(false)
|
||||||
|
const hasInitialLoadedRef = useRef(false)
|
||||||
|
|
||||||
// InputNumber 主题配置
|
// InputNumber 主题配置
|
||||||
const customTheme = {
|
const customTheme = {
|
||||||
@@ -101,10 +109,18 @@ const OrderConfirm = () => {
|
|||||||
return Math.max(0, Math.min(stockMax, availableTicketTotal))
|
return Math.max(0, Math.min(stockMax, availableTicketTotal))
|
||||||
}, [availableTicketTotal, goods?.stock])
|
}, [availableTicketTotal, goods?.stock])
|
||||||
|
|
||||||
|
const canStartOrder = useMemo(() => {
|
||||||
|
return maxQuantity >= MIN_START_QTY
|
||||||
|
}, [maxQuantity])
|
||||||
|
|
||||||
const displayQty = useMemo(() => {
|
const displayQty = useMemo(() => {
|
||||||
if (maxQuantity <= 0) return 0
|
if (!canStartOrder) return 0
|
||||||
return Math.max(1, Math.min(quantity, maxQuantity))
|
return Math.max(MIN_START_QTY, Math.min(quantity, maxQuantity))
|
||||||
}, [quantity, maxQuantity])
|
}, [quantity, maxQuantity, canStartOrder])
|
||||||
|
|
||||||
|
const sendTimeText = useMemo(() => {
|
||||||
|
return dayjs(sendTime).format('YYYY-MM-DD HH:mm')
|
||||||
|
}, [sendTime])
|
||||||
|
|
||||||
const loadStores = async () => {
|
const loadStores = async () => {
|
||||||
if (storeLoading) return
|
if (storeLoading) return
|
||||||
@@ -133,11 +149,11 @@ const OrderConfirm = () => {
|
|||||||
const parsed = typeof value === 'string' ? parseInt(value) : value
|
const parsed = typeof value === 'string' ? parseInt(value) : value
|
||||||
const newQuantity = Number.isFinite(parsed) ? Number(parsed) : 0
|
const newQuantity = Number.isFinite(parsed) ? Number(parsed) : 0
|
||||||
const upper = maxQuantity
|
const upper = maxQuantity
|
||||||
if (upper <= 0) {
|
if (!canStartOrder || upper <= 0) {
|
||||||
setQuantity(0)
|
setQuantity(0)
|
||||||
return
|
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 () => {
|
const loadUserTickets = async () => {
|
||||||
@@ -198,10 +214,18 @@ const OrderConfirm = () => {
|
|||||||
Taro.showToast({ title: '商品库存不足', icon: 'none' })
|
Taro.showToast({ title: '商品库存不足', icon: 'none' })
|
||||||
return
|
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({
|
const confirmRes = await Taro.showModal({
|
||||||
title: '确认下单',
|
title: '确认下单',
|
||||||
content: `将使用 ${finalQty} 张水票下单,送水 ${finalQty} 桶,是否确认?`
|
content: `配送时间:${sendTimeText}\n将使用 ${finalQty} 张水票下单,送水 ${finalQty} 桶,是否确认?`
|
||||||
})
|
})
|
||||||
if (!confirmRes.confirm) return
|
if (!confirmRes.confirm) return
|
||||||
|
|
||||||
@@ -215,6 +239,7 @@ const OrderConfirm = () => {
|
|||||||
addressId: address.id,
|
addressId: address.id,
|
||||||
totalNum: finalQty,
|
totalNum: finalQty,
|
||||||
buyerRemarks: orderRemark,
|
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.
|
// Backend may take userId from token; pass-through is harmless if backend ignores it.
|
||||||
userId,
|
userId,
|
||||||
comments: goods.name ? `立即送水:${goods.name}` : '立即送水'
|
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 {
|
try {
|
||||||
setLoading(true)
|
if (!opts?.silent) setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
let goodsRes: ShopGoods | null = null
|
const [goodsRes, addressRes] = await Promise.all([
|
||||||
if (numericGoodsId) {
|
numericGoodsId ? getShopGoods(numericGoodsId) : Promise.resolve(null),
|
||||||
goodsRes = await getShopGoods(numericGoodsId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [addressRes] = await Promise.all([
|
|
||||||
listShopUserAddress({ isDefault: true })
|
listShopUserAddress({ isDefault: true })
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -260,30 +283,34 @@ const OrderConfirm = () => {
|
|||||||
if (addressRes && addressRes.length > 0) {
|
if (addressRes && addressRes.length > 0) {
|
||||||
setAddress(addressRes[0])
|
setAddress(addressRes[0])
|
||||||
}
|
}
|
||||||
await loadUserTickets()
|
hasInitialLoadedRef.current = true
|
||||||
|
// Tickets are non-blocking for first paint; load in background.
|
||||||
|
loadUserTickets()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载数据失败:', err)
|
console.error('加载数据失败:', err)
|
||||||
setError('加载数据失败,请重试')
|
if (opts?.silent) {
|
||||||
|
Taro.showToast({ title: '刷新失败,请稍后重试', icon: 'none' })
|
||||||
|
} else {
|
||||||
|
setError('加载数据失败,请重试')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
if (!opts?.silent) setLoading(false)
|
||||||
|
loadAllDataLoadingRef.current = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
// 返回/切换到该页面时,刷新一下当前已选门店
|
// 返回/切换到该页面时,刷新一下当前已选门店
|
||||||
setSelectedStore(getSelectedStoreFromStorage())
|
setSelectedStore(getSelectedStoreFromStorage())
|
||||||
loadAllData()
|
loadAllData({ silent: hasInitialLoadedRef.current })
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadAllData()
|
|
||||||
}, [goodsId]);
|
|
||||||
|
|
||||||
// When tickets/stock change, clamp quantity into [0..maxQuantity].
|
// When tickets/stock change, clamp quantity into [0..maxQuantity].
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQuantity(prev => {
|
setQuantity(prev => {
|
||||||
if (maxQuantity <= 0) return 0
|
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)
|
return Math.min(prev, maxQuantity)
|
||||||
})
|
})
|
||||||
}, [maxQuantity])
|
}, [maxQuantity])
|
||||||
@@ -374,16 +401,34 @@ const OrderConfirm = () => {
|
|||||||
)}
|
)}
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
|
|
||||||
|
<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={() => setSendTimePickerVisible(true)}
|
||||||
|
/>
|
||||||
|
</CellGroup>
|
||||||
|
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
<Cell
|
<Cell
|
||||||
title={'送水数量'}
|
title={'送水数量'}
|
||||||
|
description={
|
||||||
|
canStartOrder
|
||||||
|
? `最低起送 ${MIN_START_QTY} 桶`
|
||||||
|
: `最低起送 ${MIN_START_QTY} 桶(当前最多 ${maxQuantity} 桶)`
|
||||||
|
}
|
||||||
extra={(
|
extra={(
|
||||||
<ConfigProvider theme={customTheme}>
|
<ConfigProvider theme={customTheme}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={maxQuantity <= 0 ? 0 : quantity}
|
value={displayQty}
|
||||||
min={maxQuantity <= 0 ? 0 : 1}
|
min={canStartOrder ? MIN_START_QTY : 0}
|
||||||
max={maxQuantity <= 0 ? 0 : maxQuantity}
|
max={canStartOrder ? maxQuantity : 0}
|
||||||
disabled={maxQuantity <= 0}
|
disabled={!canStartOrder}
|
||||||
onChange={handleQuantityChange}
|
onChange={handleQuantityChange}
|
||||||
/>
|
/>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
@@ -402,12 +447,18 @@ const OrderConfirm = () => {
|
|||||||
extra={(
|
extra={(
|
||||||
<View className={'flex items-center gap-2'}>
|
<View className={'flex items-center gap-2'}>
|
||||||
<View className={'text-gray-900'}>
|
<View className={'text-gray-900'}>
|
||||||
{selectedTicket ? `${selectedTicket.templateName || '水票'}(可用${selectedTicket.availableQty ?? 0})` : '请选择'}
|
{ticketLoading
|
||||||
|
? '加载中...'
|
||||||
|
: (selectedTicket
|
||||||
|
? `${selectedTicket.templateName || '水票'}(可用${selectedTicket.availableQty ?? 0})`
|
||||||
|
: '请选择'
|
||||||
|
)
|
||||||
|
}
|
||||||
</View>
|
</View>
|
||||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
onClick={() => setTicketPopupVisible(true)}
|
onClick={() => !ticketLoading && setTicketPopupVisible(true)}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
title={'本次使用'}
|
title={'本次使用'}
|
||||||
@@ -533,6 +584,23 @@ const OrderConfirm = () => {
|
|||||||
|
|
||||||
<Gap height={50}/>
|
<Gap height={50}/>
|
||||||
|
|
||||||
|
<DatePicker
|
||||||
|
visible={sendTimePickerVisible}
|
||||||
|
title="选择配送时间"
|
||||||
|
type="datetime"
|
||||||
|
startDate={new Date()}
|
||||||
|
endDate={dayjs().add(30, 'day').toDate()}
|
||||||
|
value={sendTime}
|
||||||
|
onClose={() => 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)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
|
<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'}>
|
<View className={'btn-bar flex justify-between items-center'}>
|
||||||
<div className={'flex flex-col justify-center items-start mx-4'}>
|
<div className={'flex flex-col justify-center items-start mx-4'}>
|
||||||
@@ -549,7 +617,7 @@ const OrderConfirm = () => {
|
|||||||
type="success"
|
type="success"
|
||||||
size="large"
|
size="large"
|
||||||
loading={submitLoading}
|
loading={submitLoading}
|
||||||
disabled={!selectedTicket?.id || availableTicketTotal <= 0 || maxQuantity <= 0}
|
disabled={!selectedTicket?.id || availableTicketTotal <= 0 || !canStartOrder}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
>
|
>
|
||||||
{submitLoading ? '提交中...' : '立即提交'}
|
{submitLoading ? '提交中...' : '立即提交'}
|
||||||
|
|||||||
Reference in New Issue
Block a user