forked from gxwebsoft/mp-10550
- 新增微信地址导入流程,支持从微信原生地址选择后跳转到编辑页面完善定位 - 添加WxAddressDraft缓存机制用于存储微信返回的地址草稿数据 - 实现一键导航功能,支持通过订单地址ID或地址信息进行地图导航 - 添加一键呼叫功能,支持直接拨打电话联系骑手或门店 - 优化地址编辑页面支持微信导入模式和默认地址检查
717 lines
26 KiB
TypeScript
717 lines
26 KiB
TypeScript
import { useRef, useState } from 'react';
|
||
import Taro, { useDidShow } from '@tarojs/taro';
|
||
import {
|
||
Button,
|
||
ConfigProvider,
|
||
Empty,
|
||
InfiniteLoading,
|
||
Loading,
|
||
Popup,
|
||
PullToRefresh,
|
||
SearchBar,
|
||
Tabs,
|
||
TabPane,
|
||
Tag
|
||
} from '@nutui/nutui-react-taro';
|
||
import { View, Text, Image } from '@tarojs/components';
|
||
import { pageGltUserTicket } from '@/api/glt/gltUserTicket';
|
||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
||
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
||
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model';
|
||
import { getShopUserAddress } from '@/api/shop/shopUserAddress';
|
||
import { BaseUrl } from '@/config/app';
|
||
import dayjs from "dayjs";
|
||
|
||
const PAGE_SIZE = 10;
|
||
|
||
const UserTicketList = () => {
|
||
const [ticketList, setTicketList] = useState<GltUserTicket[]>([]);
|
||
const [ticketLoading, setTicketLoading] = useState(false);
|
||
const [ticketHasMore, setTicketHasMore] = useState(true);
|
||
const [searchValue, setSearchValue] = useState('');
|
||
const [ticketPage, setTicketPage] = useState(1);
|
||
const [ticketTotal, setTicketTotal] = useState(0);
|
||
|
||
const [orderList, setOrderList] = useState<GltTicketOrder[]>([]);
|
||
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' | 'order'>(() => {
|
||
const tab = Taro.getCurrentInstance().router?.params?.tab
|
||
return tab === 'order' ? 'order' : 'ticket'
|
||
});
|
||
|
||
const [qrVisible, setQrVisible] = useState(false);
|
||
const [qrTicket, setQrTicket] = useState<GltUserTicket | null>(null);
|
||
const [qrImageUrl, setQrImageUrl] = useState('');
|
||
|
||
const addressCacheRef = useRef<Record<number, { lng: number; lat: number; fullAddress?: string } | null>>({});
|
||
|
||
const getUserId = () => {
|
||
const raw = Taro.getStorageSync('UserId');
|
||
const id = Number(raw);
|
||
return Number.isFinite(id) && id > 0 ? id : undefined;
|
||
};
|
||
|
||
const buildTicketQrContent = (ticket: GltUserTicket) => {
|
||
// QR will be encrypted by `/qr-code/create-encrypted-qr-image`,
|
||
// and decrypted on verifier side to get this payload.
|
||
return JSON.stringify({
|
||
userTicketId: ticket.id,
|
||
qty: 1,
|
||
userId: ticket.userId,
|
||
t: Date.now()
|
||
});
|
||
};
|
||
|
||
const buildEncryptedQrImageUrl = (businessType: string, data: string) => {
|
||
const size = '300x300';
|
||
const expireMinutes = 30;
|
||
const base = BaseUrl?.replace(/\/+$/, '');
|
||
return `${base}/qr-code/create-encrypted-qr-image?size=${encodeURIComponent(
|
||
size
|
||
)}&expireMinutes=${encodeURIComponent(String(expireMinutes))}&businessType=${encodeURIComponent(
|
||
businessType
|
||
)}&data=${encodeURIComponent(data)}`;
|
||
};
|
||
|
||
const openTicketQr = (ticket: GltUserTicket) => {
|
||
if (!ticket?.id) {
|
||
Taro.showToast({ title: '水票信息不完整', icon: 'none' });
|
||
return;
|
||
}
|
||
if (ticket.status === 1) {
|
||
Taro.showToast({ title: '该水票已冻结,无法核销', icon: 'none' });
|
||
return;
|
||
}
|
||
if ((ticket.availableQty ?? 0) <= 0) {
|
||
Taro.showToast({ title: '可用次数不足', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
const content = buildTicketQrContent(ticket);
|
||
setQrTicket(ticket);
|
||
setQrImageUrl(buildEncryptedQrImageUrl('ticket', content));
|
||
setQrVisible(true);
|
||
};
|
||
|
||
const showTicketDetail = (ticket: GltUserTicket) => {
|
||
const lines: string[] = [];
|
||
if (ticket.templateName) lines.push(`水票:${ticket.templateName}`);
|
||
lines.push(`可用:${ticket.availableQty ?? 0}`);
|
||
lines.push(`总量:${ticket.totalQty ?? 0}`);
|
||
lines.push(`已用:${ticket.usedQty ?? 0}`);
|
||
lines.push(`冻结:${ticket.frozenQty ?? 0}`);
|
||
lines.push(`已释放:${ticket.releasedQty ?? 0}`);
|
||
if (ticket.orderNo) lines.push(`订单号:${ticket.orderNo}`);
|
||
// Taro.showModal({
|
||
// title: '水票详情',
|
||
// content: lines.join('\n'),
|
||
// showCancel: false
|
||
// });
|
||
};
|
||
|
||
const reloadTickets = async (isRefresh = true, keywords?: string) => {
|
||
if (ticketLoading) return;
|
||
|
||
const userId = getUserId();
|
||
if (!userId) {
|
||
setTicketList([]);
|
||
setTicketTotal(0);
|
||
setTicketHasMore(false);
|
||
return;
|
||
}
|
||
|
||
if (isRefresh) {
|
||
setTicketPage(1);
|
||
setTicketList([]);
|
||
setTicketHasMore(true);
|
||
}
|
||
|
||
setTicketLoading(true);
|
||
try {
|
||
const currentPage = isRefresh ? 1 : ticketPage;
|
||
const res = await pageGltUserTicket({
|
||
page: currentPage,
|
||
limit: PAGE_SIZE,
|
||
userId,
|
||
keywords: (keywords ?? searchValue) || undefined
|
||
});
|
||
|
||
const nextList = isRefresh ? res.list : [...ticketList, ...res.list];
|
||
setTicketList(nextList);
|
||
const count = typeof res.count === 'number' ? res.count : nextList.length;
|
||
setTicketTotal(count);
|
||
setTicketHasMore(nextList.length < count);
|
||
|
||
if (res.list.length > 0) {
|
||
setTicketPage(currentPage + 1);
|
||
} else {
|
||
setTicketHasMore(false);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取水票列表失败:', error);
|
||
Taro.showToast({ title: '获取水票失败', icon: 'error' });
|
||
setTicketHasMore(false);
|
||
} finally {
|
||
setTicketLoading(false);
|
||
}
|
||
};
|
||
|
||
const reloadOrders = async (isRefresh = true, keywords?: string) => {
|
||
if (orderLoading) return;
|
||
|
||
const userId = getUserId();
|
||
if (!userId) {
|
||
setOrderList([]);
|
||
setOrderTotal(0);
|
||
setOrderHasMore(false);
|
||
return;
|
||
}
|
||
|
||
if (isRefresh) {
|
||
setOrderPage(1);
|
||
setOrderList([]);
|
||
setOrderHasMore(true);
|
||
}
|
||
|
||
setOrderLoading(true);
|
||
try {
|
||
const currentPage = isRefresh ? 1 : orderPage;
|
||
const res = await pageGltTicketOrder({
|
||
page: currentPage,
|
||
limit: PAGE_SIZE,
|
||
userId,
|
||
keywords: (keywords ?? searchValue) || undefined
|
||
});
|
||
|
||
const resList = res?.list || [];
|
||
const nextList = isRefresh ? resList : [...orderList, ...resList];
|
||
setOrderList(nextList);
|
||
const count = typeof res?.count === 'number' ? res.count : nextList.length;
|
||
setOrderTotal(count);
|
||
setOrderHasMore(nextList.length < count);
|
||
|
||
if (resList.length > 0) {
|
||
setOrderPage(currentPage + 1);
|
||
} else {
|
||
setOrderHasMore(false);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取送水订单失败:', error);
|
||
Taro.showToast({ title: '获取送水订单失败', icon: 'error' });
|
||
setOrderHasMore(false);
|
||
} finally {
|
||
setOrderLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSearch = (value: string) => {
|
||
setSearchValue(value);
|
||
if (activeTab === 'ticket') {
|
||
reloadTickets(true, value);
|
||
} else {
|
||
reloadOrders(true, value);
|
||
}
|
||
};
|
||
|
||
const handleRefresh = async () => {
|
||
if (activeTab === 'ticket') {
|
||
await reloadTickets(true);
|
||
} else {
|
||
await reloadOrders(true);
|
||
}
|
||
};
|
||
|
||
const handleTabChange = (value: string | number) => {
|
||
const tab = String(value) as 'ticket' | 'order';
|
||
setActiveTab(tab);
|
||
if (tab === 'ticket') {
|
||
setTicketPage(1);
|
||
setTicketList([]);
|
||
setTicketHasMore(true);
|
||
reloadTickets(true);
|
||
} else {
|
||
setOrderPage(1);
|
||
setOrderList([]);
|
||
setOrderHasMore(true);
|
||
reloadOrders(true);
|
||
}
|
||
};
|
||
|
||
const loadMoreTickets = async () => {
|
||
if (!ticketLoading && ticketHasMore) {
|
||
await reloadTickets(false);
|
||
}
|
||
};
|
||
|
||
const loadMoreOrders = async () => {
|
||
if (!orderLoading && orderHasMore) {
|
||
await reloadOrders(false);
|
||
}
|
||
};
|
||
|
||
const formatDateTime = (v?: string) => {
|
||
if (!v) return '-';
|
||
const d = dayjs(v);
|
||
return d.isValid() ? d.format('YYYY年MM月DD日 HH:mm:ss') : v;
|
||
};
|
||
|
||
const formatDate = (v?: string) => {
|
||
if (!v) return '-';
|
||
const d = dayjs(v);
|
||
return d.isValid() ? d.format('YYYY年MM月DD日') : v;
|
||
};
|
||
|
||
const parseLatLng = (latRaw?: unknown, lngRaw?: unknown) => {
|
||
const lat = typeof latRaw === 'number' ? latRaw : parseFloat(String(latRaw ?? ''));
|
||
const lng = typeof lngRaw === 'number' ? lngRaw : parseFloat(String(lngRaw ?? ''));
|
||
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
|
||
if (Math.abs(lat) > 90 || Math.abs(lng) > 180) return null;
|
||
return { lat, lng };
|
||
};
|
||
|
||
const handleNavigateToAddress = async (order: GltTicketOrder) => {
|
||
try {
|
||
// Prefer coordinates from backend if present (non-typed fields), otherwise fetch by addressId.
|
||
const anyOrder = order as any;
|
||
const direct =
|
||
parseLatLng(anyOrder?.addressLat ?? anyOrder?.lat, anyOrder?.addressLng ?? anyOrder?.lng) ||
|
||
parseLatLng(anyOrder?.receiverLat, anyOrder?.receiverLng);
|
||
|
||
let coords = direct;
|
||
let fullAddress: string | undefined = order.address || undefined;
|
||
|
||
if (!coords && order.addressId) {
|
||
const cached = addressCacheRef.current[order.addressId];
|
||
if (cached) {
|
||
coords = { lat: cached.lat, lng: cached.lng };
|
||
fullAddress = fullAddress || cached.fullAddress;
|
||
} else if (cached === null) {
|
||
coords = null;
|
||
} else {
|
||
const addr = await getShopUserAddress(order.addressId);
|
||
const parsed = parseLatLng(addr?.lat, addr?.lng);
|
||
if (parsed) {
|
||
coords = parsed;
|
||
fullAddress = fullAddress || addr?.fullAddress || addr?.address || undefined;
|
||
addressCacheRef.current[order.addressId] = { ...parsed, fullAddress };
|
||
} else {
|
||
addressCacheRef.current[order.addressId] = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!coords) {
|
||
if (fullAddress) {
|
||
await Taro.setClipboardData({ data: fullAddress });
|
||
Taro.showToast({ title: '未配置定位,地址已复制', icon: 'none' });
|
||
} else {
|
||
Taro.showToast({ title: '暂无可导航的地址', icon: 'none' });
|
||
}
|
||
return;
|
||
}
|
||
|
||
Taro.openLocation({
|
||
latitude: coords.lat,
|
||
longitude: coords.lng,
|
||
name: '收货地址',
|
||
address: fullAddress || ''
|
||
});
|
||
} catch (e) {
|
||
console.error('一键导航失败:', e);
|
||
Taro.showToast({ title: '导航失败,请重试', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
const handleOneClickCall = async (order: GltTicketOrder) => {
|
||
const phone = (order.riderPhone || order.storePhone || '').trim();
|
||
if (!phone) {
|
||
Taro.showToast({ title: '暂无可呼叫的电话', icon: 'none' });
|
||
return;
|
||
}
|
||
try {
|
||
await Taro.makePhoneCall({ phoneNumber: phone });
|
||
} catch (e) {
|
||
console.error('一键呼叫失败:', e);
|
||
Taro.showToast({ title: '呼叫失败,请手动拨打', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
const getTicketOrderStatusMeta = (order: GltTicketOrder) => {
|
||
if (order.status === 1) return { text: '已冻结', type: 'warning' as const };
|
||
|
||
const ds = order.deliveryStatus
|
||
if (ds === 40 || order.receiveConfirmTime) return { text: '已完成', type: 'success' as const };
|
||
if (ds === 30 || order.sendEndTime) return { text: '待确认收货', type: 'primary' as const };
|
||
if (ds === 20 || order.sendStartTime) return { text: '配送中', type: 'primary' as const };
|
||
if (ds === 10 || order.riderId) return { text: '待配送', type: 'warning' as const };
|
||
return { text: '待派单', type: 'primary' as const };
|
||
};
|
||
|
||
const canUserConfirmReceive = (order: GltTicketOrder) => {
|
||
if (!order?.id) return false
|
||
if (order.status === 1) return false
|
||
if (order.deliveryStatus === 40) return false
|
||
if (order.receiveConfirmTime) return false
|
||
// 必须是“已送达”后才能确认收货
|
||
return !!order.sendEndTime || order.deliveryStatus === 30
|
||
}
|
||
|
||
const handleUserConfirmReceive = async (order: GltTicketOrder) => {
|
||
if (!order?.id) return
|
||
if (!canUserConfirmReceive(order)) return
|
||
|
||
const modal = await Taro.showModal({
|
||
title: '确认收货',
|
||
content: '请确认已收到本次送水,确认后将无法撤销。',
|
||
confirmText: '确认收货'
|
||
})
|
||
if (!modal.confirm) return
|
||
|
||
try {
|
||
Taro.showLoading({ title: '提交中...' })
|
||
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||
await updateGltTicketOrder({
|
||
id: order.id,
|
||
deliveryStatus: 40,
|
||
receiveConfirmTime: now,
|
||
receiveConfirmType: 10
|
||
})
|
||
Taro.showToast({ title: '已确认收货', icon: 'success' })
|
||
await reloadOrders(true)
|
||
} catch (e) {
|
||
console.error('确认收货失败:', e)
|
||
Taro.showToast({ title: '确认失败,请重试', icon: 'none' })
|
||
} finally {
|
||
Taro.hideLoading()
|
||
}
|
||
}
|
||
|
||
useDidShow(() => {
|
||
if (activeTab === 'ticket') {
|
||
reloadTickets(true).then();
|
||
} else {
|
||
reloadOrders(true).then();
|
||
}
|
||
});
|
||
|
||
return (
|
||
<ConfigProvider>
|
||
{/* 搜索栏 */}
|
||
<View className="bg-white px-4 py-3 hidden">
|
||
<SearchBar
|
||
placeholder="搜索水票"
|
||
value={searchValue}
|
||
onChange={setSearchValue}
|
||
onSearch={handleSearch}
|
||
/>
|
||
</View>
|
||
|
||
{/* Tab切换 */}
|
||
<View className="bg-white">
|
||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||
<TabPane title="我的水票" value="ticket"></TabPane>
|
||
<TabPane title="送水订单" value="order"></TabPane>
|
||
</Tabs>
|
||
</View>
|
||
|
||
{activeTab === 'ticket' && ticketTotal > 0 && (
|
||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50 hidden">
|
||
共 {ticketTotal} 条水票记录
|
||
</View>
|
||
)}
|
||
|
||
{activeTab === 'order' && orderTotal > 0 && (
|
||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50 hidden">
|
||
共 {orderTotal} 条送水订单
|
||
</View>
|
||
)}
|
||
|
||
{/* 列表 */}
|
||
<PullToRefresh onRefresh={handleRefresh} headHeight={60}>
|
||
<View style={{ height: 'calc(100vh)', overflowY: 'auto' }} id="ticket-scroll">
|
||
{activeTab === 'ticket' && ticketList.length === 0 && !ticketLoading ? (
|
||
<View
|
||
className="flex flex-col justify-center items-center"
|
||
style={{ height: 'calc(100vh - 160px)' }}
|
||
>
|
||
<Empty
|
||
description="暂无水票"
|
||
style={{ backgroundColor: 'transparent' }}
|
||
/>
|
||
</View>
|
||
) : activeTab === 'order' && orderList.length === 0 && !orderLoading ? (
|
||
<View
|
||
className="flex flex-col justify-center items-center"
|
||
style={{ height: 'calc(100vh - 160px)' }}
|
||
>
|
||
<Empty description="暂无送水订单" style={{ backgroundColor: 'transparent' }} />
|
||
</View>
|
||
) : activeTab === 'ticket' ? (
|
||
<InfiniteLoading
|
||
target="ticket-scroll"
|
||
hasMore={ticketHasMore}
|
||
onLoadMore={loadMoreTickets}
|
||
loadingText={
|
||
<View className="flex justify-center items-center py-4">
|
||
<Loading />
|
||
<View className="ml-2">加载中...</View>
|
||
</View>
|
||
}
|
||
loadMoreText={
|
||
<View className="text-center py-4 text-gray-500">
|
||
{ticketList.length === 0 ? '暂无数据' : '没有更多了'}
|
||
</View>
|
||
}
|
||
>
|
||
<View className="px-4 py-3">
|
||
{ticketList.map((item, index) => (
|
||
<View
|
||
key={String(item.id ?? `${item.templateId ?? 't'}-${index}`)}
|
||
className="bg-white rounded-xl p-4 mb-3"
|
||
onClick={() => showTicketDetail(item)}
|
||
>
|
||
<View className="flex items-start justify-between">
|
||
<View className="flex-1 pr-3">
|
||
<Text className="text-base font-semibold text-gray-900">
|
||
票号:{item.id}
|
||
</Text>
|
||
{item.orderNo && (
|
||
<View className="mt-1">
|
||
<Text className="text-xs text-gray-500">订单编号:{item.orderNo}</Text>
|
||
</View>
|
||
)}
|
||
{item.createTime && (
|
||
<View className="mt-1">
|
||
<Text className="text-xs text-gray-400">下单时间:{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View className="flex flex-col items-end gap-2">
|
||
{/*<Tag type={item.status === 1 ? 'danger' : 'success'}>*/}
|
||
{/* {item.status === 1 ? '冻结' : '正常'}*/}
|
||
{/*</Tag>*/}
|
||
<Button
|
||
size="small"
|
||
type="primary"
|
||
disabled={(item.availableQty ?? 0) <= 0 || item.status === 1}
|
||
onClick={(e) => {
|
||
// Avoid triggering card click.
|
||
e.stopPropagation();
|
||
openTicketQr(item);
|
||
}}
|
||
>
|
||
出示核销码
|
||
</Button>
|
||
</View>
|
||
</View>
|
||
|
||
<View className="mt-3 flex justify-between">
|
||
<View className="flex flex-col items-center">
|
||
<Text className="text-lg font-bold text-blue-600 text-center">{item.availableQty ?? 0}</Text>
|
||
<Text className="text-xs text-gray-500">可用水票</Text>
|
||
</View>
|
||
<View className="flex flex-col items-center">
|
||
<Text className="text-lg text-gray-900 text-center">{item.usedQty ?? 0}</Text>
|
||
<Text className="text-xs text-gray-500">已用水票</Text>
|
||
</View>
|
||
<View className="flex flex-col items-center">
|
||
<Text className="text-lg text-gray-900 text-center">{item.frozenQty ?? 0}</Text>
|
||
<Text className="text-xs text-gray-500">剩余赠票</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</InfiniteLoading>
|
||
) : (
|
||
<InfiniteLoading
|
||
target="ticket-scroll"
|
||
hasMore={orderHasMore}
|
||
onLoadMore={loadMoreOrders}
|
||
loadingText={
|
||
<View className="flex justify-center items-center py-4">
|
||
<Loading />
|
||
<View className="ml-2">加载中...</View>
|
||
</View>
|
||
}
|
||
loadMoreText={
|
||
<View className="text-center py-4 text-gray-500">
|
||
{orderList.length === 0 ? '暂无数据' : '没有更多了'}
|
||
</View>
|
||
}
|
||
>
|
||
<View className="px-4 py-3">
|
||
{orderList.map((item, index) => (
|
||
<View
|
||
key={String(item.id ?? `order-${index}`)}
|
||
className="bg-white rounded-xl p-4 mb-3"
|
||
>
|
||
<View className="flex items-start justify-between">
|
||
<View className="flex-1 pr-3">
|
||
<Text className="text-base font-semibold text-gray-900">
|
||
票号:{item.userTicketId ?? '-'}
|
||
</Text>
|
||
<View className="mt-1">
|
||
<Text className="text-xs text-gray-500">送水数量:{item.totalNum ?? 0}</Text>
|
||
</View>
|
||
<View className="mt-1">
|
||
<Text className="text-xs text-gray-500">配送时间:{formatDate(item.sendTime)}</Text>
|
||
</View>
|
||
</View>
|
||
{(() => {
|
||
const meta = getTicketOrderStatusMeta(item);
|
||
return <Tag type={meta.type}>{meta.text}</Tag>;
|
||
})()}
|
||
</View>
|
||
<View className="mt-2 text-xs text-gray-500">
|
||
<Text>订单号:{item.id ?? '-'}</Text>
|
||
</View>
|
||
<View className="mt-1 text-xs text-gray-500">
|
||
<Text>收货地址:{item.address || '-'}</Text>
|
||
</View>
|
||
<View className="mt-1">
|
||
<Text className="text-xs text-gray-500">下单时间:{formatDateTime(item.createTime)}</Text>
|
||
</View>
|
||
{(!!item.addressId || !!item.address || !!item.riderPhone || !!item.storePhone) ? (
|
||
<View className="mt-3 flex justify-end gap-2">
|
||
{(!!item.addressId || !!item.address) ? (
|
||
<Button
|
||
size="small"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
void handleNavigateToAddress(item);
|
||
}}
|
||
>
|
||
一键导航
|
||
</Button>
|
||
) : null}
|
||
{(!!item.riderPhone || !!item.storePhone) ? (
|
||
<Button
|
||
size="small"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
void handleOneClickCall(item);
|
||
}}
|
||
>
|
||
一键呼叫
|
||
</Button>
|
||
) : null}
|
||
</View>
|
||
) : null}
|
||
{/*{item.storeName ? (*/}
|
||
{/* <View className="mt-1 text-xs text-gray-500">*/}
|
||
{/* <Text>门店:{item.storeName}</Text>*/}
|
||
{/* </View>*/}
|
||
{/*) : null}*/}
|
||
{item.sendStartTime ? (
|
||
<View className="mt-1 text-xs text-gray-500">
|
||
<Text>开始配送:{formatDateTime(item.sendStartTime)}</Text>
|
||
</View>
|
||
) : null}
|
||
{item.sendEndTime ? (
|
||
<View className="mt-1 text-xs text-gray-500">
|
||
<Text>送达时间:{formatDateTime(item.sendEndTime)}</Text>
|
||
</View>
|
||
) : null}
|
||
{item.receiveConfirmTime ? (
|
||
<View className="mt-1 text-xs text-gray-500">
|
||
<Text>确认收货:{formatDateTime(item.receiveConfirmTime)}</Text>
|
||
</View>
|
||
) : null}
|
||
{item.sendEndImg ? (
|
||
<View className="mt-3">
|
||
<Image src={item.sendEndImg} mode="aspectFill" style={{ width: '100%', height: '160px', borderRadius: '8px' }} />
|
||
</View>
|
||
) : null}
|
||
{canUserConfirmReceive(item) ? (
|
||
<View className="mt-3 flex justify-end">
|
||
<Button
|
||
type="primary"
|
||
size="small"
|
||
onClick={() => handleUserConfirmReceive(item)}
|
||
>
|
||
确认收货
|
||
</Button>
|
||
</View>
|
||
) : null}
|
||
</View>
|
||
))}
|
||
</View>
|
||
</InfiniteLoading>
|
||
)}
|
||
</View>
|
||
</PullToRefresh>
|
||
|
||
{/* 核销二维码 */}
|
||
<Popup
|
||
visible={qrVisible}
|
||
position="center"
|
||
closeable
|
||
onClose={() => setQrVisible(false)}
|
||
style={{ width: '90%' }}
|
||
>
|
||
<View className="p-6">
|
||
<View className="mb-4">
|
||
<Text className="text-lg font-bold">水票核销码</Text>
|
||
</View>
|
||
|
||
{qrTicket && (
|
||
<View className="bg-gray-50 rounded-lg p-3 mb-4">
|
||
<View className="flex justify-between mb-2">
|
||
<Text className="text-sm text-gray-600">票号</Text>
|
||
<Text className="text-sm text-gray-900">{qrTicket.id}</Text>
|
||
</View>
|
||
<View className="flex justify-between">
|
||
<Text className="text-sm text-gray-600">可用次数</Text>
|
||
<Text className="text-sm text-gray-900">{qrTicket.availableQty ?? 0}</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
<View className="text-center mb-4">
|
||
<View className="p-4 bg-white border border-gray-200 rounded-lg">
|
||
{qrImageUrl ? (
|
||
<View className="flex flex-col justify-center items-center">
|
||
<Image
|
||
src={qrImageUrl}
|
||
mode="aspectFit"
|
||
style={{ width: '200px', height: '200px' }}
|
||
/>
|
||
<Text className="text-sm text-gray-400 mt-2 px-2">
|
||
请向配送员出示此二维码
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
<View
|
||
className="bg-gray-100 rounded flex items-center justify-center mx-auto"
|
||
style={{ width: '200px', height: '200px' }}
|
||
>
|
||
<Text className="text-gray-500 text-sm">生成中...</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
|
||
<Button
|
||
type="primary"
|
||
block
|
||
onClick={() => {
|
||
if (!qrTicket) return;
|
||
const content = buildTicketQrContent(qrTicket);
|
||
setQrImageUrl(buildEncryptedQrImageUrl('ticket', content));
|
||
}}
|
||
>
|
||
刷新二维码
|
||
</Button>
|
||
</View>
|
||
</Popup>
|
||
</ConfigProvider>
|
||
);
|
||
};
|
||
|
||
export default UserTicketList;
|