forked from gxwebsoft/mp-10550
feat(ticket): 添加水票功能模块
- 新增水票相关API接口,包括水票模板、用户水票、消费日志和水票释放功能 - 添加水票管理页面,实现水票的增删改查和详情展示功能 - 实现水票的分页查询和列表展示界面 - 替换原有的礼品卡功能为水票功能,在首页导航中更新路由链接 - 添加水票详情页面,支持二维码展示和兑换码复制功能 - 实现水票的状态管理和使用流程控制
This commit is contained in:
251
src/user/ticket/index.tsx
Normal file
251
src/user/ticket/index.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useState } from 'react';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import {
|
||||
ConfigProvider,
|
||||
Empty,
|
||||
InfiniteLoading,
|
||||
Loading,
|
||||
PullToRefresh,
|
||||
SearchBar,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Tag
|
||||
} from '@nutui/nutui-react-taro';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { pageGltUserTicket } from '@/api/glt/gltUserTicket';
|
||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const UserTicketList = () => {
|
||||
const [list, setList] = useState<GltUserTicket[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = 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 getUserId = () => {
|
||||
const raw = Taro.getStorageSync('UserId');
|
||||
const id = Number(raw);
|
||||
return Number.isFinite(id) && id > 0 ? id : undefined;
|
||||
};
|
||||
|
||||
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 reloadWithTab = async (tab: string, isRefresh = true, keywords?: string) => {
|
||||
if (loading) return;
|
||||
|
||||
const userId = getUserId();
|
||||
if (!userId) {
|
||||
setList([]);
|
||||
setTotal(0);
|
||||
setHasMore(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRefresh) {
|
||||
setPage(1);
|
||||
setList([]);
|
||||
setHasMore(true);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page;
|
||||
const status = Number(tab); // 0正常,1冻结
|
||||
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 count = typeof res.count === 'number' ? res.count : nextList.length;
|
||||
setTotal(count);
|
||||
setHasMore(nextList.length < count);
|
||||
|
||||
if (res.list.length > 0) {
|
||||
setPage(currentPage + 1);
|
||||
} else {
|
||||
setHasMore(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取水票列表失败:', error);
|
||||
Taro.showToast({ title: '获取水票失败', icon: 'error' });
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reload = async (isRefresh = true) => reloadWithTab(activeTab, isRefresh);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
useDidShow(() => {
|
||||
reloadWithTab(activeTab, 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="0"></TabPane>
|
||||
<TabPane title="冻结" value="1"></TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{total > 0 && (
|
||||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50 hidden">
|
||||
共 {total} 条水票记录
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 列表 */}
|
||||
<PullToRefresh onRefresh={handleRefresh} headHeight={60}>
|
||||
<View style={{ height: 'calc(100vh - 200px)', overflowY: 'auto' }} id="ticket-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View
|
||||
className="flex flex-col justify-center items-center"
|
||||
style={{ height: 'calc(100vh - 260px)' }}
|
||||
>
|
||||
<Empty
|
||||
description={activeTab === '0' ? '暂无正常水票' : '暂无冻结水票'}
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="ticket-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
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">
|
||||
{list.length === 0 ? '暂无数据' : '没有更多了'}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<View className="px-4 py-3">
|
||||
{list.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.templateName || '水票'}
|
||||
</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">创建时间:{item.createTime}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Tag type={item.status === 1 ? 'danger' : 'success'}>
|
||||
{item.status === 1 ? '冻结' : '正常'}
|
||||
</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-lg font-bold text-blue-600">{item.availableQty ?? 0}</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.totalQty ?? 0}</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.usedQty ?? 0}</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.frozenQty ?? 0}</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.releasedQty ?? 0}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserTicketList;
|
||||
Reference in New Issue
Block a user