import { 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 { BaseUrl } from '@/config/app'; import dayjs from "dayjs"; const PAGE_SIZE = 10; const UserTicketList = () => { const [ticketList, setTicketList] = useState([]); 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([]); 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(null); const [qrImageUrl, setQrImageUrl] = useState(''); 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 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 ( {/* 搜索栏 */} {/* Tab切换 */} {activeTab === 'ticket' && ticketTotal > 0 && ( 共 {ticketTotal} 条水票记录 )} {activeTab === 'order' && orderTotal > 0 && ( 共 {orderTotal} 条送水订单 )} {/* 列表 */} {activeTab === 'ticket' && ticketList.length === 0 && !ticketLoading ? ( ) : activeTab === 'order' && orderList.length === 0 && !orderLoading ? ( ) : activeTab === 'ticket' ? ( 加载中... } loadMoreText={ {ticketList.length === 0 ? '暂无数据' : '没有更多了'} } > {ticketList.map((item, index) => ( showTicketDetail(item)} > 票号:{item.id} {item.orderNo && ( 订单编号:{item.orderNo} )} {item.createTime && ( 下单时间:{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')} )} {/**/} {/* {item.status === 1 ? '冻结' : '正常'}*/} {/**/} {item.availableQty ?? 0} 可用水票 {item.usedQty ?? 0} 已用水票 {item.frozenQty ?? 0} 剩余赠票 ))} ) : ( 加载中... } loadMoreText={ {orderList.length === 0 ? '暂无数据' : '没有更多了'} } > {orderList.map((item, index) => ( 票号:{item.userTicketId ?? '-'} 送水数量:{item.totalNum ?? 0} 配送时间:{formatDate(item.sendTime)} {(() => { const meta = getTicketOrderStatusMeta(item); return {meta.text}; })()} 订单号:{item.id ?? '-'} 收货地址:{item.address || '-'} 下单时间:{formatDateTime(item.createTime)} {/*{item.storeName ? (*/} {/* */} {/* 门店:{item.storeName}*/} {/* */} {/*) : null}*/} {item.sendStartTime ? ( 开始配送:{formatDateTime(item.sendStartTime)} ) : null} {item.sendEndTime ? ( 送达时间:{formatDateTime(item.sendEndTime)} ) : null} {item.receiveConfirmTime ? ( 确认收货:{formatDateTime(item.receiveConfirmTime)} ) : null} {item.sendEndImg ? ( ) : null} {canUserConfirmReceive(item) ? ( ) : null} ))} )} {/* 核销二维码 */} setQrVisible(false)} style={{ width: '90%' }} > 水票核销码 {qrTicket && ( 票号 {qrTicket.id} 可用次数 {qrTicket.availableQty ?? 0} )} {qrImageUrl ? ( 请向配送员出示此二维码 ) : ( 生成中... )} ); }; export default UserTicketList;