feat(ticket): 添加水票释放计划功能
- 在应用配置中注册新的释放计划页面路由 ticket/release/index - 简化 API 请求参数结构,移除不必要的包装对象 - 在用户票券列表中添加释放计划详情入口和跳转逻辑 - 显示票券套票名称信息增强用户体验 - 在配送时间选择中添加日期验证防止选择过去日期 - 新增完整的释放计划详情页面实现列表展示、下拉刷新、上拉加载等功能 - 添加释放计划状态显示和数量统计信息展示
This commit is contained in:
@@ -8,9 +8,7 @@ import type { GltUserTicketRelease, GltUserTicketReleaseParam } from './model';
|
|||||||
export async function pageGltUserTicketRelease(params: GltUserTicketReleaseParam) {
|
export async function pageGltUserTicketRelease(params: GltUserTicketReleaseParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<GltUserTicketRelease>>>(
|
const res = await request.get<ApiResult<PageResult<GltUserTicketRelease>>>(
|
||||||
'/glt/glt-user-ticket-release/page',
|
'/glt/glt-user-ticket-release/page',
|
||||||
{
|
params
|
||||||
params
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -24,9 +22,7 @@ export async function pageGltUserTicketRelease(params: GltUserTicketReleaseParam
|
|||||||
export async function listGltUserTicketRelease(params?: GltUserTicketReleaseParam) {
|
export async function listGltUserTicketRelease(params?: GltUserTicketReleaseParam) {
|
||||||
const res = await request.get<ApiResult<GltUserTicketRelease[]>>(
|
const res = await request.get<ApiResult<GltUserTicketRelease[]>>(
|
||||||
'/glt/glt-user-ticket-release',
|
'/glt/glt-user-ticket-release',
|
||||||
{
|
params
|
||||||
params
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export default {
|
|||||||
"points/points",
|
"points/points",
|
||||||
"ticket/index",
|
"ticket/index",
|
||||||
"ticket/use",
|
"ticket/use",
|
||||||
|
"ticket/release/index",
|
||||||
"ticket/orders/index",
|
"ticket/orders/index",
|
||||||
// "gift/index",
|
// "gift/index",
|
||||||
// "gift/redeem",
|
// "gift/redeem",
|
||||||
|
|||||||
@@ -116,6 +116,20 @@ const UserTicketList = () => {
|
|||||||
await Taro.navigateTo({ url });
|
await Taro.navigateTo({ url });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goReleasePlanDetail = async (ticket: GltUserTicket) => {
|
||||||
|
if (!ticket?.id) {
|
||||||
|
Taro.showToast({ title: '水票信息不完整', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `/user/ticket/release/index?userTicketId=${encodeURIComponent(String(ticket.id))}&templateName=${encodeURIComponent(
|
||||||
|
String(ticket.templateName ?? '')
|
||||||
|
)}&frozenQty=${encodeURIComponent(String(ticket.frozenQty ?? 0))}&releasedQty=${encodeURIComponent(
|
||||||
|
String(ticket.releasedQty ?? 0)
|
||||||
|
)}`;
|
||||||
|
if (!ensureLoggedIn(url)) return;
|
||||||
|
await Taro.navigateTo({ url });
|
||||||
|
};
|
||||||
|
|
||||||
const showTicketDetail = (ticket: GltUserTicket) => {
|
const showTicketDetail = (ticket: GltUserTicket) => {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
if (ticket.templateName) lines.push(`水票:${ticket.templateName}`);
|
if (ticket.templateName) lines.push(`水票:${ticket.templateName}`);
|
||||||
@@ -566,6 +580,9 @@ const UserTicketList = () => {
|
|||||||
<Text className="text-base font-semibold text-gray-900">
|
<Text className="text-base font-semibold text-gray-900">
|
||||||
票号:{item.id}
|
票号:{item.id}
|
||||||
</Text>
|
</Text>
|
||||||
|
<View className="mt-1">
|
||||||
|
<Text className="text-xs text-gray-500">套票名称:{item.templateName}</Text>
|
||||||
|
</View>
|
||||||
{item.orderNo && (
|
{item.orderNo && (
|
||||||
<View className="mt-1">
|
<View className="mt-1">
|
||||||
<Text className="text-xs text-gray-500">订单编号:{item.orderNo}</Text>
|
<Text className="text-xs text-gray-500">订单编号:{item.orderNo}</Text>
|
||||||
@@ -617,7 +634,14 @@ const UserTicketList = () => {
|
|||||||
<Text className="text-lg text-gray-900 text-center">{item.usedQty ?? 0}</Text>
|
<Text className="text-lg text-gray-900 text-center">{item.usedQty ?? 0}</Text>
|
||||||
<Text className="text-xs text-gray-500">已用水票</Text>
|
<Text className="text-xs text-gray-500">已用水票</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-col items-center">
|
<View
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
hoverClass="opacity-70"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void goReleasePlanDetail(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text className="text-lg text-gray-900 text-center">{item.frozenQty ?? 0}</Text>
|
<Text className="text-lg text-gray-900 text-center">{item.frozenQty ?? 0}</Text>
|
||||||
<Text className="text-xs text-gray-500">剩余赠票</Text>
|
<Text className="text-xs text-gray-500">剩余赠票</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
6
src/user/ticket/release/index.config.ts
Normal file
6
src/user/ticket/release/index.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '释放计划',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationBarBackgroundColor: '#ffffff'
|
||||||
|
})
|
||||||
|
|
||||||
221
src/user/ticket/release/index.tsx
Normal file
221
src/user/ticket/release/index.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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 currentPage = isRefresh ? 1 : page
|
||||||
|
const res = await pageGltUserTicketRelease({
|
||||||
|
page: currentPage,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
userId: uid,
|
||||||
|
userTicketId: userTicketId as any
|
||||||
|
} 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)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const at = dayjs(a.releaseTime || a.createTime || 0).valueOf()
|
||||||
|
const bt = dayjs(b.releaseTime || b.createTime || 0).valueOf()
|
||||||
|
return bt - at
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextList = isRefresh ? safe : list.concat(safe)
|
||||||
|
setList(nextList)
|
||||||
|
|
||||||
|
const serverCount = typeof (res as any)?.count === 'number' ? Number((res as any).count) : undefined
|
||||||
|
const nextTotal = typeof serverCount === 'number' ? serverCount : nextList.length
|
||||||
|
setTotal(nextTotal)
|
||||||
|
setHasMore(typeof serverCount === 'number' ? nextList.length < serverCount : incoming.length >= PAGE_SIZE)
|
||||||
|
|
||||||
|
if (incoming.length > 0) setPage(currentPage + 1)
|
||||||
|
else setHasMore(false)
|
||||||
|
} 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 ? (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Space
|
Space
|
||||||
} from '@nutui/nutui-react-taro'
|
} from '@nutui/nutui-react-taro'
|
||||||
import { ArrowRight, Location, Ticket } from '@nutui/icons-react-taro'
|
import { ArrowRight, Location, Ticket } from '@nutui/icons-react-taro'
|
||||||
import dayjs from 'dayjs'
|
import dayjs, { type Dayjs } from 'dayjs'
|
||||||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||||
import { getShopGoods } from '@/api/shop/shopGoods'
|
import { getShopGoods } from '@/api/shop/shopGoods'
|
||||||
import { getShopUserAddress, listShopUserAddress } from '@/api/shop/shopUserAddress'
|
import { getShopUserAddress, listShopUserAddress } from '@/api/shop/shopUserAddress'
|
||||||
@@ -136,6 +136,12 @@ const OrderConfirm = () => {
|
|||||||
return d.isValid() ? d : null
|
return d.isValid() ? d : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clampSendDateToToday = (d: Dayjs) => {
|
||||||
|
const today = dayjs().startOf('day')
|
||||||
|
if (!d.isValid()) return today
|
||||||
|
return d.isBefore(today, 'day') ? today : d.startOf('day')
|
||||||
|
}
|
||||||
|
|
||||||
const getOrderTime = (o?: Partial<GltTicketOrder> | null) => {
|
const getOrderTime = (o?: Partial<GltTicketOrder> | null) => {
|
||||||
return parseTime(o?.createTime) || parseTime(o?.updateTime)
|
return parseTime(o?.createTime) || parseTime(o?.updateTime)
|
||||||
}
|
}
|
||||||
@@ -728,6 +734,11 @@ const OrderConfirm = () => {
|
|||||||
Taro.showToast({ title: '请选择配送时间', icon: 'none' })
|
Taro.showToast({ title: '请选择配送时间', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (dayjs(sendTime).isBefore(dayjs().startOf('day'), 'day')) {
|
||||||
|
Taro.showToast({ title: '配送时间不能早于今天', icon: 'none' })
|
||||||
|
setSendTime(dayjs().startOf('day').toDate())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 配送范围校验(电子围栏)
|
// 配送范围校验(电子围栏)
|
||||||
const ok = await ensureInDeliveryRange()
|
const ok = await ensureInDeliveryRange()
|
||||||
@@ -869,7 +880,7 @@ const OrderConfirm = () => {
|
|||||||
setQuantity(Number.isFinite(initQty) && initQty > 0 ? initQty : minStartQty)
|
setQuantity(Number.isFinite(initQty) && initQty > 0 ? initQty : minStartQty)
|
||||||
setOrderRemark(String(editingOrderRes.buyerRemarks || ''))
|
setOrderRemark(String(editingOrderRes.buyerRemarks || ''))
|
||||||
const st = parseTime(editingOrderRes.sendTime)
|
const st = parseTime(editingOrderRes.sendTime)
|
||||||
if (st) setSendTime(st.startOf('day').toDate())
|
if (st) setSendTime(clampSendDateToToday(st).toDate())
|
||||||
|
|
||||||
const addrId = Number(editingOrderRes.addressId)
|
const addrId = Number(editingOrderRes.addressId)
|
||||||
const addrIdSafe = Number.isFinite(addrId) && addrId > 0 ? addrId : undefined
|
const addrIdSafe = Number.isFinite(addrId) && addrId > 0 ? addrId : undefined
|
||||||
@@ -1162,11 +1173,18 @@ const OrderConfirm = () => {
|
|||||||
extra={(
|
extra={(
|
||||||
<Picker
|
<Picker
|
||||||
mode="date"
|
mode="date"
|
||||||
|
start={dayjs().format('YYYY-MM-DD')}
|
||||||
value={dayjs(sendTime).format('YYYY-MM-DD')}
|
value={dayjs(sendTime).format('YYYY-MM-DD')}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const v = (e as any)?.detail?.value
|
const v = (e as any)?.detail?.value
|
||||||
const d = dayjs(v)
|
const d = dayjs(v)
|
||||||
if (d.isValid()) setSendTime(d.startOf('day').toDate())
|
if (!d.isValid()) return
|
||||||
|
if (d.isBefore(dayjs().startOf('day'), 'day')) {
|
||||||
|
Taro.showToast({ title: '配送时间不能早于今天', icon: 'none' })
|
||||||
|
setSendTime(dayjs().startOf('day').toDate())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSendTime(d.startOf('day').toDate())
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className={'flex items-center gap-2'}>
|
<View className={'flex items-center gap-2'}>
|
||||||
|
|||||||
Reference in New Issue
Block a user