- 在应用配置中注册新的释放计划页面路由 ticket/release/index - 简化 API 请求参数结构,移除不必要的包装对象 - 在用户票券列表中添加释放计划详情入口和跳转逻辑 - 显示票券套票名称信息增强用户体验 - 在配送时间选择中添加日期验证防止选择过去日期 - 新增完整的释放计划详情页面实现列表展示、下拉刷新、上拉加载等功能 - 添加释放计划状态显示和数量统计信息展示
246 lines
8.7 KiB
TypeScript
246 lines
8.7 KiB
TypeScript
import { useState } from 'react'
|
||
import Taro, { useDidShow } from '@tarojs/taro'
|
||
import { View, Text } from '@tarojs/components'
|
||
import { ConfigProvider, Empty, InfiniteLoading, Loading, PullToRefresh, Tag } from '@nutui/nutui-react-taro'
|
||
import dayjs from 'dayjs'
|
||
|
||
import { pageGltUserTicketRelease } from '@/api/glt/gltUserTicketRelease'
|
||
import type { GltUserTicketRelease } from '@/api/glt/gltUserTicketRelease/model'
|
||
import { ensureLoggedIn } from '@/utils/auth'
|
||
|
||
const PAGE_SIZE = 10
|
||
const MAX_FETCH_ROUNDS = 10
|
||
|
||
export default function TicketReleasePlanPage() {
|
||
const [list, setList] = useState<GltUserTicketRelease[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [hasMore, setHasMore] = useState(true)
|
||
const [page, setPage] = useState(1)
|
||
const [total, setTotal] = useState<number | undefined>(undefined)
|
||
|
||
const router = Taro.getCurrentInstance().router
|
||
const userTicketId = String(router?.params?.userTicketId || '').trim()
|
||
const templateName = (() => {
|
||
const raw = String(router?.params?.templateName || '')
|
||
try {
|
||
return decodeURIComponent(raw)
|
||
} catch {
|
||
return raw
|
||
}
|
||
})()
|
||
const frozenQtyText = router?.params?.frozenQty !== undefined ? String(router?.params?.frozenQty) : undefined
|
||
const releasedQtyText = router?.params?.releasedQty !== undefined ? String(router?.params?.releasedQty) : undefined
|
||
|
||
const getUserId = () => {
|
||
const raw = Taro.getStorageSync('UserId')
|
||
const id = Number(raw)
|
||
return Number.isFinite(id) && id > 0 ? id : undefined
|
||
}
|
||
|
||
const getStatusMeta = (item: GltUserTicketRelease) => {
|
||
const status = Number(item.status)
|
||
if (status === 1) return { text: '已释放', type: 'success' as const }
|
||
if (status === 0) return { text: '待释放', type: 'warning' as const }
|
||
return { text: `状态${Number.isFinite(status) ? status : '-'}`, type: 'primary' as const }
|
||
}
|
||
|
||
const formatDateTime = (v?: string) => {
|
||
if (!v) return '-'
|
||
const d = dayjs(v)
|
||
return d.isValid() ? d.format('YYYY年MM月DD日 HH:mm:ss') : v
|
||
}
|
||
|
||
const reload = async (isRefresh = true) => {
|
||
if (loading) return
|
||
|
||
const uid = getUserId()
|
||
if (!uid) {
|
||
setList([])
|
||
setHasMore(false)
|
||
setTotal(0)
|
||
return
|
||
}
|
||
if (!userTicketId) {
|
||
setList([])
|
||
setHasMore(false)
|
||
setTotal(0)
|
||
return
|
||
}
|
||
|
||
if (isRefresh) {
|
||
setPage(1)
|
||
setList([])
|
||
setHasMore(true)
|
||
}
|
||
|
||
setLoading(true)
|
||
try {
|
||
const baseList = isRefresh ? [] : list
|
||
const seen = new Set(baseList.map(r => String(r.id ?? `${r.userTicketId ?? ''}:${r.periodNo ?? ''}:${r.releaseTime ?? ''}`)))
|
||
|
||
let nextPage = isRefresh ? 1 : page
|
||
let serverHasMore = true
|
||
let added = 0
|
||
let nextList = baseList.slice()
|
||
|
||
for (let round = 0; round < MAX_FETCH_ROUNDS; round++) {
|
||
if (!serverHasMore) break
|
||
|
||
// Only query by current logged-in userId; userTicketId is filtered on the client.
|
||
const res = await pageGltUserTicketRelease({
|
||
page: nextPage,
|
||
limit: PAGE_SIZE,
|
||
userId: uid
|
||
} as any)
|
||
|
||
const incoming = Array.isArray(res?.list) ? res.list : []
|
||
const safe = incoming
|
||
.filter(r => Number((r as any)?.deleted) !== 1)
|
||
.filter(r => !userTicketId || String(r.userTicketId || '') === userTicketId)
|
||
.filter(r => {
|
||
const k = String(r.id ?? `${r.userTicketId ?? ''}:${r.periodNo ?? ''}:${r.releaseTime ?? ''}`)
|
||
if (seen.has(k)) return false
|
||
seen.add(k)
|
||
return true
|
||
})
|
||
|
||
if (safe.length) {
|
||
nextList = nextList.concat(safe)
|
||
added += safe.length
|
||
}
|
||
|
||
serverHasMore = incoming.length >= PAGE_SIZE
|
||
if (!serverHasMore) break
|
||
nextPage += 1
|
||
|
||
// Stop early once we got something to render for this ticket.
|
||
if (added > 0) break
|
||
}
|
||
|
||
nextList.sort((a, b) => {
|
||
const at = dayjs(a.releaseTime || a.createTime || 0).valueOf()
|
||
const bt = dayjs(b.releaseTime || b.createTime || 0).valueOf()
|
||
return bt - at
|
||
})
|
||
|
||
setList(nextList)
|
||
setTotal(nextList.length)
|
||
setHasMore(serverHasMore)
|
||
setPage(nextPage)
|
||
} catch (e) {
|
||
console.error('加载释放计划失败:', e)
|
||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||
setHasMore(false)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useDidShow(() => {
|
||
const redirect = userTicketId
|
||
? `/user/ticket/release/index?userTicketId=${encodeURIComponent(userTicketId)}`
|
||
: '/user/ticket/index'
|
||
if (!ensureLoggedIn(redirect)) return
|
||
void reload(true)
|
||
})
|
||
|
||
const handleRefresh = async () => {
|
||
await reload(true)
|
||
}
|
||
|
||
const loadMore = async () => {
|
||
if (!loading && hasMore) await reload(false)
|
||
}
|
||
|
||
return (
|
||
<ConfigProvider>
|
||
<PullToRefresh onRefresh={handleRefresh} headHeight={60}>
|
||
<View style={{ height: 'calc(100vh)', overflowY: 'auto' }} id="ticket-release-scroll">
|
||
<View className="px-4 py-3">
|
||
<View className="bg-white rounded-xl p-4 mb-3">
|
||
<View className="flex items-center justify-between">
|
||
<Text className="text-base font-semibold text-gray-900">释放计划明细</Text>
|
||
{typeof total === 'number' ? (
|
||
<Text className="text-xs text-gray-400">共 {total} 条</Text>
|
||
) : null}
|
||
</View>
|
||
<View className="mt-2 text-xs text-gray-500">
|
||
<Text>票号:{userTicketId || '-'}</Text>
|
||
</View>
|
||
{templateName ? (
|
||
<View className="mt-1 text-xs text-gray-500">
|
||
<Text>套票名称:{templateName}</Text>
|
||
</View>
|
||
) : null}
|
||
{frozenQtyText !== undefined || releasedQtyText !== undefined ? (
|
||
<View className="mt-2 flex gap-4">
|
||
{frozenQtyText !== undefined ? (
|
||
<View>
|
||
<Text className="text-xs text-gray-500">剩余赠票:{frozenQtyText}</Text>
|
||
</View>
|
||
) : null}
|
||
{releasedQtyText !== undefined ? (
|
||
<View>
|
||
<Text className="text-xs text-gray-500">已释放:{releasedQtyText}</Text>
|
||
</View>
|
||
) : null}
|
||
</View>
|
||
) : null}
|
||
</View>
|
||
|
||
{list.length === 0 && !loading && !hasMore ? (
|
||
<View className="flex flex-col justify-center items-center" style={{ height: 'calc(100vh - 220px)' }}>
|
||
<Empty description="暂无释放计划" style={{ backgroundColor: 'transparent' }} />
|
||
</View>
|
||
) : (
|
||
<InfiniteLoading
|
||
target="ticket-release-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>
|
||
{list.map((item, index) => {
|
||
const meta = getStatusMeta(item)
|
||
return (
|
||
<View
|
||
key={String(item.id ?? `${item.userTicketId ?? 't'}-${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-sm font-semibold text-gray-900">
|
||
周期:{item.periodNo ?? '-'}
|
||
</Text>
|
||
<View className="mt-1 text-xs text-gray-500">
|
||
<Text>释放数量:{item.releaseQty ?? 0}</Text>
|
||
</View>
|
||
<View className="mt-1 text-xs text-gray-500">
|
||
<Text>释放时间:{formatDateTime(item.releaseTime)}</Text>
|
||
</View>
|
||
</View>
|
||
<Tag type={meta.type}>{meta.text}</Tag>
|
||
</View>
|
||
</View>
|
||
)
|
||
})}
|
||
</View>
|
||
</InfiniteLoading>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</PullToRefresh>
|
||
</ConfigProvider>
|
||
)
|
||
}
|