forked from gxwebsoft/mp-10550
feat(ticket): 添加水票功能支持
- 在订单模型中增加formId字段用于标识商品ID - 更新统一扫码组件以支持水票和礼品卡核销 - 实现水票列表页面,包含我的水票和核销记录两个标签页 - 添加水票核销二维码生成功能 - 支持水票的分页加载和搜索功能 - 实现水票核销记录的展示 - 添加水票状态变更历史追踪 - 更新订单状态判断逻辑以支持特定商品完成状态 - 扩展扫码验证功能以处理水票业务类型
This commit is contained in:
@@ -93,6 +93,8 @@ export interface ShopOrder {
|
||||
totalNum?: number;
|
||||
// 教练id
|
||||
coachId?: number;
|
||||
// 商品ID
|
||||
formId?: number;
|
||||
// 支付的用户id
|
||||
payUserId?: number;
|
||||
// 0余额支付, 1微信支付,102微信Native,2会员卡支付,3支付宝,4现金,5POS机,6VIP月卡,7VIP年卡,8VIP次卡,9IC月卡,10IC年卡,11IC次卡,12免费,13VIP充值卡,14IC充值卡,15积分支付,16VIP季卡,17IC季卡,18代付
|
||||
|
||||
@@ -68,7 +68,7 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
content: '是否继续扫码核销其他水票/礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleClick(); // 递归调用继续扫码
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseQRContent
|
||||
} from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket';
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
import { isValidJSON } from "@/utils/jsonUtils";
|
||||
import dayjs from 'dayjs';
|
||||
@@ -29,6 +30,15 @@ export enum ScanType {
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
type VerificationBusinessType = 'gift' | 'ticket';
|
||||
|
||||
interface TicketVerificationPayload {
|
||||
userTicketId: number;
|
||||
qty?: number;
|
||||
userId?: number;
|
||||
t?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
@@ -73,7 +83,11 @@ export function useUnifiedQRScan() {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
// Allow plaintext (non-encrypted) ticket verification payload for debugging/internal use.
|
||||
if (json.userTicketId) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
@@ -130,35 +144,79 @@ export function useUnifiedQRScan() {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
let businessType: VerificationBusinessType = 'gift';
|
||||
let decryptedOrRaw = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
|
||||
businessType = json.businessType;
|
||||
// 解密获取核销内容
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
decryptedOrRaw = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
} else if (json.userTicketId) {
|
||||
businessType = 'ticket';
|
||||
decryptedOrRaw = scanResult.trim();
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
// 直接使用扫码结果作为核销内容
|
||||
decryptedOrRaw = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
if (!decryptedOrRaw) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
if (businessType === 'ticket') {
|
||||
if (!isValidJSON(decryptedOrRaw)) {
|
||||
throw new Error('水票核销信息格式错误');
|
||||
}
|
||||
const payload = JSON.parse(decryptedOrRaw) as TicketVerificationPayload;
|
||||
const userTicketId = Number(payload.userTicketId);
|
||||
const qty = Math.max(1, Number(payload.qty || 1));
|
||||
if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
|
||||
throw new Error('水票核销信息无效');
|
||||
}
|
||||
|
||||
const ticket = await getGltUserTicket(userTicketId);
|
||||
if (!ticket) throw new Error('水票不存在');
|
||||
if (ticket.status === 1) throw new Error('该水票已冻结');
|
||||
const available = Number(ticket.availableQty || 0);
|
||||
const used = Number(ticket.usedQty || 0);
|
||||
if (available < qty) throw new Error('水票可用次数不足');
|
||||
|
||||
await updateGltUserTicket({
|
||||
...ticket,
|
||||
availableQty: available - qty,
|
||||
usedQty: used + qty
|
||||
});
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: {
|
||||
businessType: 'ticket',
|
||||
ticket: {
|
||||
...ticket,
|
||||
availableQty: available - qty,
|
||||
usedQty: used + qty
|
||||
},
|
||||
qty
|
||||
},
|
||||
message: `核销成功(已使用${qty}次)`
|
||||
};
|
||||
}
|
||||
|
||||
// 验证礼品卡核销码
|
||||
const gift = await getShopGiftByCode(decryptedOrRaw);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
@@ -187,7 +245,7 @@ export function useUnifiedQRScan() {
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: gift,
|
||||
data: { businessType: 'gift', gift },
|
||||
message: '核销成功'
|
||||
};
|
||||
}, [isAdmin]);
|
||||
|
||||
@@ -233,10 +233,27 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
console.log('统一扫码成功:', result);
|
||||
// 根据扫码类型给出不同的提示
|
||||
if (result.type === 'verification') {
|
||||
// 核销成功,可以显示更多信息或跳转到详情页
|
||||
const businessType = result?.data?.businessType;
|
||||
if (businessType === 'gift' && result?.data?.gift) {
|
||||
const gift = result.data.gift;
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销的品类:${result.data.goodsName || '水票'},面值¥${result.data.faceValue}`
|
||||
content: `已成功核销:${gift.goodsName || gift.name || '礼品'},面值¥${gift.faceValue}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (businessType === 'ticket' && result?.data?.ticket) {
|
||||
const ticket = result.data.ticket;
|
||||
const qty = result.data.qty || 1;
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销:${ticket.templateName || '水票'},本次使用${qty}次,剩余可用${ticket.availableQty ?? 0}次`
|
||||
});
|
||||
return;
|
||||
}
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '已成功核销'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -43,7 +43,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
content: '是否继续扫码核销其他水票/礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleStartScan();
|
||||
@@ -179,7 +179,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{scanType === ScanType.LOGIN ? '正在确认登录' :
|
||||
scanType === ScanType.VERIFICATION ? '正在核销礼品卡' : '正在处理'}
|
||||
scanType === ScanType.VERIFICATION ? '正在核销' : '正在处理'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
@@ -192,12 +192,29 @@ const UnifiedQRPage: React.FC = () => {
|
||||
</Text>
|
||||
{result.type === ScanType.VERIFICATION && result.data && (
|
||||
<View className="bg-green-50 rounded-lg p-3 mb-4">
|
||||
{result.data.businessType === 'gift' && result.data.gift && (
|
||||
<>
|
||||
<Text className="text-sm text-green-800 block">
|
||||
水票:{result.data.goodsName || '未知商品'}
|
||||
礼品:{result.data.gift.goodsName || result.data.gift.name || '未知'}
|
||||
</Text>
|
||||
<Text className="text-sm text-green-800 block">
|
||||
面值:¥{result.data.faceValue}
|
||||
面值:¥{result.data.gift.faceValue}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{result.data.businessType === 'ticket' && result.data.ticket && (
|
||||
<>
|
||||
<Text className="text-sm text-green-800 block">
|
||||
水票:{result.data.ticket.templateName || '水票'}
|
||||
</Text>
|
||||
<Text className="text-sm text-green-800 block">
|
||||
本次核销:{result.data.qty || 1} 次
|
||||
</Text>
|
||||
<Text className="text-sm text-green-800 block">
|
||||
剩余可用:{result.data.ticket.availableQty ?? 0} 次
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View className="mt-2">
|
||||
@@ -278,9 +295,14 @@ const UnifiedQRPage: React.FC = () => {
|
||||
<Text className="text-sm text-gray-800">
|
||||
{record.success ? record.message : record.error}
|
||||
</Text>
|
||||
{record.success && record.type === ScanType.VERIFICATION && record.data && (
|
||||
{record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'gift' && record.data?.gift && (
|
||||
<Text className="text-xs text-gray-500">
|
||||
{record.data.goodsName} - ¥{record.data.faceValue}
|
||||
{record.data.gift.goodsName || record.data.gift.name} - ¥{record.data.gift.faceValue}
|
||||
</Text>
|
||||
)}
|
||||
{record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'ticket' && record.data?.ticket && (
|
||||
<Text className="text-xs text-gray-500">
|
||||
{record.data.ticket.templateName || '水票'} - 本次核销 {record.data.qty || 1} 次
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
@@ -304,7 +326,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
• 登录二维码:自动确认网页端登录
|
||||
</Text>
|
||||
<Text className="text-xs text-blue-700 block mb-1">
|
||||
• 核销二维码:门店核销用户礼品卡
|
||||
• 核销二维码:核销用户水票/礼品卡
|
||||
</Text>
|
||||
<Text className="text-xs text-blue-700 block">
|
||||
• 系统会自动识别二维码类型并执行相应操作
|
||||
|
||||
@@ -131,6 +131,7 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
// 已付款后检查发货状态
|
||||
if (order.deliveryStatus === 10) return '待发货';
|
||||
if (order.formId === 10074) return '已完成';
|
||||
if (order.deliveryStatus === 20) {
|
||||
// 若订单没有配送员,沿用原“待收货”语义
|
||||
if (!order.riderId) return '待收货';
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
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 } from '@tarojs/components';
|
||||
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 [list, setList] = useState<GltUserTicket[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [ticketList, setTicketList] = useState<GltUserTicket[]>([]);
|
||||
const [ticketLoading, setTicketLoading] = useState(false);
|
||||
const [ticketHasMore, setTicketHasMore] = useState(true);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
// 0-正常 1-冻结
|
||||
const [activeTab, setActiveTab] = useState<string>('0');
|
||||
const [ticketPage, setTicketPage] = useState(1);
|
||||
const [ticketTotal, setTicketTotal] = useState(0);
|
||||
|
||||
const [logList, setLogList] = useState<GltUserTicketLog[]>([]);
|
||||
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<GltUserTicket | null>(null);
|
||||
const [qrImageUrl, setQrImageUrl] = useState('');
|
||||
|
||||
const getUserId = () => {
|
||||
const raw = Taro.getStorageSync('UserId');
|
||||
@@ -33,6 +48,48 @@ const UserTicketList = () => {
|
||||
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}`);
|
||||
@@ -49,83 +106,149 @@ const UserTicketList = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const reloadWithTab = async (tab: string, isRefresh = true, keywords?: string) => {
|
||||
if (loading) return;
|
||||
const reloadTickets = async (isRefresh = true, keywords?: string) => {
|
||||
if (ticketLoading) return;
|
||||
|
||||
const userId = getUserId();
|
||||
if (!userId) {
|
||||
setList([]);
|
||||
setTotal(0);
|
||||
setHasMore(false);
|
||||
setTicketList([]);
|
||||
setTicketTotal(0);
|
||||
setTicketHasMore(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRefresh) {
|
||||
setPage(1);
|
||||
setList([]);
|
||||
setHasMore(true);
|
||||
setTicketPage(1);
|
||||
setTicketList([]);
|
||||
setTicketHasMore(true);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setTicketLoading(true);
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page;
|
||||
const status = Number(tab); // 0正常,1冻结
|
||||
const currentPage = isRefresh ? 1 : ticketPage;
|
||||
const res = await pageGltUserTicket({
|
||||
page: currentPage,
|
||||
limit: PAGE_SIZE,
|
||||
userId,
|
||||
status,
|
||||
keywords: (keywords ?? searchValue) || undefined
|
||||
});
|
||||
|
||||
const nextList = isRefresh ? res.list : [...list, ...res.list];
|
||||
setList(nextList);
|
||||
const nextList = isRefresh ? res.list : [...ticketList, ...res.list];
|
||||
setTicketList(nextList);
|
||||
const count = typeof res.count === 'number' ? res.count : nextList.length;
|
||||
setTotal(count);
|
||||
setHasMore(nextList.length < count);
|
||||
setTicketTotal(count);
|
||||
setTicketHasMore(nextList.length < count);
|
||||
|
||||
if (res.list.length > 0) {
|
||||
setPage(currentPage + 1);
|
||||
setTicketPage(currentPage + 1);
|
||||
} else {
|
||||
setHasMore(false);
|
||||
setTicketHasMore(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取水票列表失败:', error);
|
||||
Taro.showToast({ title: '获取水票失败', icon: 'error' });
|
||||
setHasMore(false);
|
||||
setTicketHasMore(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setTicketLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reload = async (isRefresh = true) => reloadWithTab(activeTab, isRefresh);
|
||||
const reloadLogs = async (isRefresh = true, keywords?: string) => {
|
||||
if (logLoading) 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,
|
||||
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);
|
||||
reloadWithTab(activeTab, true, value);
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await reload(true);
|
||||
};
|
||||
|
||||
const handleTabChange = (value: string | number) => {
|
||||
const tab = String(value);
|
||||
setActiveTab(tab);
|
||||
setPage(1);
|
||||
setList([]);
|
||||
setHasMore(true);
|
||||
reloadWithTab(tab, true);
|
||||
};
|
||||
|
||||
const loadMore = async () => {
|
||||
if (!loading && hasMore) {
|
||||
await reload(false);
|
||||
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(() => {
|
||||
reloadWithTab(activeTab, true).then();
|
||||
if (activeTab === 'ticket') {
|
||||
reloadTickets(true).then();
|
||||
} else {
|
||||
reloadLogs(true).then();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -143,35 +266,48 @@ const UserTicketList = () => {
|
||||
{/* Tab切换 */}
|
||||
<View className="bg-white">
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<TabPane title="正常" value="0"></TabPane>
|
||||
<TabPane title="冻结" value="1"></TabPane>
|
||||
<TabPane title="我的水票" value="ticket"></TabPane>
|
||||
<TabPane title="核销记录" value="log"></TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{total > 0 && (
|
||||
{activeTab === 'ticket' && ticketTotal > 0 && (
|
||||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50 hidden">
|
||||
共 {total} 条水票记录
|
||||
共 {ticketTotal} 条水票记录
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'log' && logTotal > 0 && (
|
||||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50 hidden">
|
||||
共 {logTotal} 条核销记录
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 列表 */}
|
||||
<PullToRefresh onRefresh={handleRefresh} headHeight={60}>
|
||||
<View style={{ height: 'calc(100vh - 200px)', overflowY: 'auto' }} id="ticket-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
{activeTab === 'ticket' && ticketList.length === 0 && !ticketLoading ? (
|
||||
<View
|
||||
className="flex flex-col justify-center items-center"
|
||||
style={{ height: 'calc(100vh - 260px)' }}
|
||||
>
|
||||
<Empty
|
||||
description={activeTab === '0' ? '暂无正常水票' : '暂无冻结水票'}
|
||||
description="暂无水票"
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
) : activeTab === 'log' && logList.length === 0 && !logLoading ? (
|
||||
<View
|
||||
className="flex flex-col justify-center items-center"
|
||||
style={{ height: 'calc(100vh - 260px)' }}
|
||||
>
|
||||
<Empty description="暂无核销记录" style={{ backgroundColor: 'transparent' }} />
|
||||
</View>
|
||||
) : activeTab === 'ticket' ? (
|
||||
<InfiniteLoading
|
||||
target="ticket-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
hasMore={ticketHasMore}
|
||||
onLoadMore={loadMoreTickets}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
@@ -180,12 +316,12 @@ const UserTicketList = () => {
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? '暂无数据' : '没有更多了'}
|
||||
{ticketList.length === 0 ? '暂无数据' : '没有更多了'}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<View className="px-4 py-3">
|
||||
{list.map((item, index) => (
|
||||
{ticketList.map((item, index) => (
|
||||
<View
|
||||
key={String(item.id ?? `${item.templateId ?? 't'}-${index}`)}
|
||||
className="bg-white rounded-xl p-4 mb-3"
|
||||
@@ -198,18 +334,32 @@ const UserTicketList = () => {
|
||||
</Text>
|
||||
{item.orderNo && (
|
||||
<View className="mt-1">
|
||||
<Text className="text-xs text-gray-500">订单号:{item.orderNo}</Text>
|
||||
<Text className="text-xs text-gray-500">订单编号:{item.orderNo}</Text>
|
||||
</View>
|
||||
)}
|
||||
{item.createTime && (
|
||||
<View className="mt-1">
|
||||
<Text className="text-xs text-gray-400">创建时间:{item.createTime}</Text>
|
||||
<Text className="text-xs text-gray-400">下单时间:{item.createTime}</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">
|
||||
@@ -241,9 +391,153 @@ const UserTicketList = () => {
|
||||
))}
|
||||
</View>
|
||||
</InfiniteLoading>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="ticket-scroll"
|
||||
hasMore={logHasMore}
|
||||
onLoadMore={loadMoreLogs}
|
||||
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">
|
||||
{logList.length === 0 ? '暂无数据' : '没有更多了'}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<View className="px-4 py-3">
|
||||
{logList.map((item, index) => (
|
||||
<View
|
||||
key={String(item.id ?? `log-${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">
|
||||
核销记录
|
||||
</Text>
|
||||
{item.createTime && (
|
||||
<View className="mt-1">
|
||||
<Text className="text-xs text-gray-400">时间:{item.createTime}</Text>
|
||||
</View>
|
||||
)}
|
||||
{item.orderNo && (
|
||||
<View className="mt-1">
|
||||
<Text className="text-xs text-gray-500">订单编号:{item.orderNo}</Text>
|
||||
</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>
|
||||
</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.templateName || '水票'}</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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user