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 { pageGltUserTicketLog } from '@/api/glt/gltUserTicketLog'; import type { GltUserTicketLog } from '@/api/glt/gltUserTicketLog/model'; import { BaseUrl } from '@/config/app'; 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 [logList, setLogList] = useState([]); const [logLoading, setLogLoading] = useState(false); const [logHasMore, setLogHasMore] = useState(true); const [logPage, setLogPage] = useState(1); const [logTotal, setLogTotal] = useState(0); const [activeTab, setActiveTab] = useState<'ticket' | 'log'>('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 reloadLogs = async (isRefresh = true, keywords?: string) => { if (logLoading) return; const userId = getUserId(); if (!userId) { setLogList([]); setLogTotal(0); setLogHasMore(false); return; } if (isRefresh) { setLogPage(1); setLogList([]); setLogHasMore(true); } setLogLoading(true); try { const currentPage = isRefresh ? 1 : logPage; const res = await pageGltUserTicketLog({ page: currentPage, limit: PAGE_SIZE, userId, keywords: (keywords ?? searchValue) || undefined }); const resList = res?.list || []; const nextList = isRefresh ? resList : [...logList, ...resList]; setLogList(nextList); const count = typeof res?.count === 'number' ? res.count : nextList.length; setLogTotal(count); setLogHasMore(nextList.length < count); if (resList.length > 0) { setLogPage(currentPage + 1); } else { setLogHasMore(false); } } catch (error) { console.error('获取核销记录失败:', error); Taro.showToast({ title: '获取核销记录失败', icon: 'error' }); setLogHasMore(false); } finally { setLogLoading(false); } }; const handleSearch = (value: string) => { setSearchValue(value); if (activeTab === 'ticket') { reloadTickets(true, value); } else { reloadLogs(true, value); } }; const handleRefresh = async () => { if (activeTab === 'ticket') { await reloadTickets(true); } else { await reloadLogs(true); } }; const handleTabChange = (value: string | number) => { const tab = String(value) as 'ticket' | 'log'; setActiveTab(tab); if (tab === 'ticket') { setTicketPage(1); setTicketList([]); setTicketHasMore(true); reloadTickets(true); } else { setLogPage(1); setLogList([]); setLogHasMore(true); reloadLogs(true); } }; const loadMoreTickets = async () => { if (!ticketLoading && ticketHasMore) { await reloadTickets(false); } }; const loadMoreLogs = async () => { if (!logLoading && logHasMore) { await reloadLogs(false); } }; const formatSigned = (n?: number) => { const val = Number(n || 0); if (val === 0) return '0'; return val > 0 ? `+${val}` : `${val}`; }; useDidShow(() => { if (activeTab === 'ticket') { reloadTickets(true).then(); } else { reloadLogs(true).then(); } }); return ( {/* 搜索栏 */} {/* Tab切换 */} {activeTab === 'ticket' && ticketTotal > 0 && ( 共 {ticketTotal} 条水票记录 )} {activeTab === 'log' && logTotal > 0 && ( 共 {logTotal} 条核销记录 )} {/* 列表 */} {activeTab === 'ticket' && ticketList.length === 0 && !ticketLoading ? ( ) : activeTab === 'log' && logList.length === 0 && !logLoading ? ( ) : activeTab === 'ticket' ? ( 加载中... } loadMoreText={ {ticketList.length === 0 ? '暂无数据' : '没有更多了'} } > {ticketList.map((item, index) => ( showTicketDetail(item)} > 票号:{item.id} {item.orderNo && ( 订单编号:{item.orderNo} )} {item.createTime && ( 下单时间:{item.createTime} )} {/**/} {/* {item.status === 1 ? '冻结' : '正常'}*/} {/**/} 可用 {item.availableQty ?? 0} 总量 {item.totalQty ?? 0} 已用 {item.usedQty ?? 0} 冻结 {item.frozenQty ?? 0} 已释放 {item.releasedQty ?? 0} ))} ) : ( 加载中... } loadMoreText={ {logList.length === 0 ? '暂无数据' : '没有更多了'} } > {logList.map((item, index) => ( 核销记录 {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 ?? '-'} ))} )} {/* 核销二维码 */} setQrVisible(false)} style={{ width: '90%' }} > 水票核销码 {qrTicket && ( 水票 {qrTicket.templateName || '水票'} 可用次数 {qrTicket.availableQty ?? 0} )} {qrImageUrl ? ( 请向配送员出示此二维码 ) : ( 生成中... )} ); }; export default UserTicketList;