forked from gxwebsoft/mp-10550
feat(ticket): 添加送水订单功能和页面
- 新增 ticket/orders/index 页面用于展示送水订单 - 添加 GltTicketOrder 相关数据模型定义 - 实现送水订单的增删改查 API 接口 - 在水票使用页面集成订单功能 - 添加水票选择逻辑优化 - 实现送水订单列表分页加载 - 集成下拉刷新和上拉加载更多功能
This commit is contained in:
101
src/api/glt/gltTicketOrder/index.ts
Normal file
101
src/api/glt/gltTicketOrder/index.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api/index';
|
||||||
|
import type { GltTicketOrder, GltTicketOrderParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询送水订单
|
||||||
|
*/
|
||||||
|
export async function pageGltTicketOrder(params: GltTicketOrderParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<GltTicketOrder>>>(
|
||||||
|
'/glt/glt-ticket-order/page',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询送水订单列表
|
||||||
|
*/
|
||||||
|
export async function listGltTicketOrder(params?: GltTicketOrderParam) {
|
||||||
|
const res = await request.get<ApiResult<GltTicketOrder[]>>(
|
||||||
|
'/glt/glt-ticket-order',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加送水订单
|
||||||
|
*/
|
||||||
|
export async function addGltTicketOrder(data: GltTicketOrder) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-order',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改送水订单
|
||||||
|
*/
|
||||||
|
export async function updateGltTicketOrder(data: GltTicketOrder) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-order',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除送水订单
|
||||||
|
*/
|
||||||
|
export async function removeGltTicketOrder(id?: number) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-order/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除送水订单
|
||||||
|
*/
|
||||||
|
export async function removeBatchGltTicketOrder(data: (number | undefined)[]) {
|
||||||
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
|
'/glt/glt-ticket-order/batch',
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询送水订单
|
||||||
|
*/
|
||||||
|
export async function getGltTicketOrder(id: number) {
|
||||||
|
const res = await request.get<ApiResult<GltTicketOrder>>(
|
||||||
|
'/glt/glt-ticket-order/' + id
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
51
src/api/glt/gltTicketOrder/model/index.ts
Normal file
51
src/api/glt/gltTicketOrder/model/index.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { PageParam } from '@/api/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送水订单
|
||||||
|
*/
|
||||||
|
export interface GltTicketOrder {
|
||||||
|
//
|
||||||
|
id?: number;
|
||||||
|
// 用户水票ID
|
||||||
|
userTicketId?: number;
|
||||||
|
// 门店ID
|
||||||
|
storeId?: number;
|
||||||
|
// 配送员
|
||||||
|
riderId?: number;
|
||||||
|
// 仓库ID
|
||||||
|
warehouseId?: number;
|
||||||
|
// 关联收货地址
|
||||||
|
addressId?: number;
|
||||||
|
// 收货地址
|
||||||
|
address?: string;
|
||||||
|
// 买家留言
|
||||||
|
buyerRemarks?: string;
|
||||||
|
// 用于统计
|
||||||
|
price?: string;
|
||||||
|
// 购买数量
|
||||||
|
totalNum?: number;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 排序(数字越小越靠前)
|
||||||
|
sortNumber?: number;
|
||||||
|
// 备注
|
||||||
|
comments?: string;
|
||||||
|
// 状态, 0正常, 1冻结
|
||||||
|
status?: number;
|
||||||
|
// 是否删除, 0否, 1是
|
||||||
|
deleted?: number;
|
||||||
|
// 租户id
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 修改时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送水订单搜索条件
|
||||||
|
*/
|
||||||
|
export interface GltTicketOrderParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ export default {
|
|||||||
"points/points",
|
"points/points",
|
||||||
"ticket/index",
|
"ticket/index",
|
||||||
"ticket/use",
|
"ticket/use",
|
||||||
|
"ticket/orders/index",
|
||||||
// "gift/index",
|
// "gift/index",
|
||||||
// "gift/redeem",
|
// "gift/redeem",
|
||||||
// "gift/detail",
|
// "gift/detail",
|
||||||
|
|||||||
6
src/user/ticket/orders/index.config.ts
Normal file
6
src/user/ticket/orders/index.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '我的送水订单',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationBarBackgroundColor: '#ffffff'
|
||||||
|
})
|
||||||
|
|
||||||
132
src/user/ticket/orders/index.tsx
Normal file
132
src/user/ticket/orders/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { NavBar, Cell, CellGroup, InfiniteLoading, PullToRefresh, Empty, Loading } from '@nutui/nutui-react-taro'
|
||||||
|
import { ArrowLeft } from '@nutui/icons-react-taro'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
|
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
export default function TicketOrdersPage() {
|
||||||
|
const [statusBarHeight, setStatusBarHeight] = useState(0)
|
||||||
|
const [list, setList] = useState<GltTicketOrder[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
|
const userId = (() => {
|
||||||
|
const raw = Taro.getStorageSync('UserId')
|
||||||
|
const id = Number(raw)
|
||||||
|
return Number.isFinite(id) && id > 0 ? id : undefined
|
||||||
|
})()
|
||||||
|
|
||||||
|
const reload = async (isRefresh = true) => {
|
||||||
|
if (loading) return
|
||||||
|
if (!userId) {
|
||||||
|
setList([])
|
||||||
|
setHasMore(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const currentPage = isRefresh ? 1 : page
|
||||||
|
const res = await pageGltTicketOrder({
|
||||||
|
page: currentPage,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
userId
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const resList = res?.list || []
|
||||||
|
const next = isRefresh ? resList : [...list, ...resList]
|
||||||
|
setList(next)
|
||||||
|
|
||||||
|
const total = typeof res?.count === 'number' ? res.count : next.length
|
||||||
|
setHasMore(next.length < total)
|
||||||
|
setPage(currentPage + 1)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取送水订单失败:', e)
|
||||||
|
Taro.showToast({ title: '获取送水订单失败', icon: 'none' })
|
||||||
|
setHasMore(false)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Taro.getSystemInfo({
|
||||||
|
success: (res) => setStatusBarHeight(res.statusBarHeight ?? 0)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
setPage(1)
|
||||||
|
setHasMore(true)
|
||||||
|
reload(true)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen">
|
||||||
|
<View style={{ height: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff' }} />
|
||||||
|
<NavBar
|
||||||
|
fixed
|
||||||
|
style={{ marginTop: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff' }}
|
||||||
|
left={<ArrowLeft onClick={() => Taro.navigateBack()} />}
|
||||||
|
>
|
||||||
|
<span>我的送水订单</span>
|
||||||
|
</NavBar>
|
||||||
|
|
||||||
|
<View className="pt-14 px-3">
|
||||||
|
<PullToRefresh onRefresh={() => reload(true)}>
|
||||||
|
{list.length === 0 && !loading ? (
|
||||||
|
<View className="bg-white rounded-lg p-6">
|
||||||
|
<Empty description="暂无送水订单" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<CellGroup>
|
||||||
|
{list.map((o) => {
|
||||||
|
const qty = Number(o.totalNum || 0)
|
||||||
|
const timeText = o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : ''
|
||||||
|
const addr = o.address || (o.addressId ? `地址ID:${o.addressId}` : '')
|
||||||
|
const remark = o.buyerRemarks || ''
|
||||||
|
return (
|
||||||
|
<Cell
|
||||||
|
key={o.id}
|
||||||
|
title={
|
||||||
|
<View className="flex flex-col">
|
||||||
|
<Text className="text-sm text-gray-900">送水 {qty || '-'} 桶</Text>
|
||||||
|
{addr ? <Text className="text-xs text-gray-500">{addr}</Text> : null}
|
||||||
|
{remark ? <Text className="text-xs text-gray-500">备注:{remark}</Text> : null}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
extra={<Text className="text-xs text-gray-500">{timeText}</Text>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</CellGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<InfiniteLoading
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={() => reload(false)}
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PullToRefresh>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -23,13 +23,8 @@ import type {ShopStore} from "@/api/shop/shopStore/model";
|
|||||||
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
|
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
|
||||||
import { listGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket'
|
import { listGltUserTicket } from '@/api/glt/gltUserTicket'
|
||||||
import { addGltUserTicketLog } from '@/api/glt/gltUserTicketLog'
|
import { addGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
import { createOrder, listShopOrder } from '@/api/shop/shopOrder'
|
|
||||||
import type { OrderCreateRequest } from '@/api/shop/shopOrder/model'
|
|
||||||
|
|
||||||
// payType=12 in this project is "free order" (no payment). Used for water-ticket orders.
|
|
||||||
const PAY_TYPE_FREE = 12
|
|
||||||
|
|
||||||
const OrderConfirm = () => {
|
const OrderConfirm = () => {
|
||||||
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
||||||
@@ -58,6 +53,7 @@ const OrderConfirm = () => {
|
|||||||
|
|
||||||
// 水票:用于“立即送水”下单(用水票抵扣,无需支付)
|
// 水票:用于“立即送水”下单(用水票抵扣,无需支付)
|
||||||
const [tickets, setTickets] = useState<GltUserTicket[]>([])
|
const [tickets, setTickets] = useState<GltUserTicket[]>([])
|
||||||
|
const [selectedTicketId, setSelectedTicketId] = useState<number | undefined>(undefined)
|
||||||
const [ticketPopupVisible, setTicketPopupVisible] = useState(false)
|
const [ticketPopupVisible, setTicketPopupVisible] = useState(false)
|
||||||
const [ticketLoading, setTicketLoading] = useState(false)
|
const [ticketLoading, setTicketLoading] = useState(false)
|
||||||
|
|
||||||
@@ -80,7 +76,8 @@ const OrderConfirm = () => {
|
|||||||
.filter(t => t?.status !== 1)
|
.filter(t => t?.status !== 1)
|
||||||
.filter(t => Number.isFinite(Number(t?.id)) && Number(t.id) > 0)
|
.filter(t => Number.isFinite(Number(t?.id)) && Number(t.id) > 0)
|
||||||
.filter(t => (t.availableQty ?? 0) > 0)
|
.filter(t => (t.availableQty ?? 0) > 0)
|
||||||
.filter(t => (numericGoodsId ? t.goodsId === numericGoodsId : true))
|
// Some tenants don't fill goodsId on ticket; allow it as a fallback.
|
||||||
|
.filter(t => (numericGoodsId ? (!t.goodsId || t.goodsId === numericGoodsId) : true))
|
||||||
// FIFO: use older tickets first (reduce disputes).
|
// FIFO: use older tickets first (reduce disputes).
|
||||||
return list.sort((a, b) => {
|
return list.sort((a, b) => {
|
||||||
const ta = new Date(a.createTime || 0).getTime() || 0
|
const ta = new Date(a.createTime || 0).getTime() || 0
|
||||||
@@ -90,9 +87,14 @@ const OrderConfirm = () => {
|
|||||||
})
|
})
|
||||||
}, [tickets, numericGoodsId])
|
}, [tickets, numericGoodsId])
|
||||||
|
|
||||||
|
const selectedTicket = useMemo(() => {
|
||||||
|
if (!selectedTicketId) return undefined
|
||||||
|
return usableTickets.find(t => Number(t.id) === Number(selectedTicketId))
|
||||||
|
}, [usableTickets, selectedTicketId])
|
||||||
|
|
||||||
const availableTicketTotal = useMemo(() => {
|
const availableTicketTotal = useMemo(() => {
|
||||||
return usableTickets.reduce((sum, t) => sum + Number(t.availableQty || 0), 0)
|
return Number(selectedTicket?.availableQty || 0)
|
||||||
}, [usableTickets])
|
}, [selectedTicket?.availableQty])
|
||||||
|
|
||||||
const maxQuantity = useMemo(() => {
|
const maxQuantity = useMemo(() => {
|
||||||
const stockMax = goods?.stock ?? 999
|
const stockMax = goods?.stock ?? 999
|
||||||
@@ -104,34 +106,6 @@ const OrderConfirm = () => {
|
|||||||
return Math.max(1, Math.min(quantity, maxQuantity))
|
return Math.max(1, Math.min(quantity, maxQuantity))
|
||||||
}, [quantity, maxQuantity])
|
}, [quantity, maxQuantity])
|
||||||
|
|
||||||
type ConsumePlanItem = {
|
|
||||||
ticket: GltUserTicket
|
|
||||||
qty: number
|
|
||||||
availableAfter: number
|
|
||||||
usedAfter: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildConsumePlan = (needQty: number): ConsumePlanItem[] => {
|
|
||||||
let remaining = Math.max(0, needQty)
|
|
||||||
const plan: ConsumePlanItem[] = []
|
|
||||||
for (const t of usableTickets) {
|
|
||||||
if (!remaining) break
|
|
||||||
const available = Number(t.availableQty || 0)
|
|
||||||
const used = Number(t.usedQty || 0)
|
|
||||||
if (available <= 0) continue
|
|
||||||
const take = Math.min(available, remaining)
|
|
||||||
remaining -= take
|
|
||||||
plan.push({
|
|
||||||
ticket: t,
|
|
||||||
qty: take,
|
|
||||||
availableAfter: available - take,
|
|
||||||
usedAfter: used + take
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (remaining > 0) return []
|
|
||||||
return plan
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadStores = async () => {
|
const loadStores = async () => {
|
||||||
if (storeLoading) return
|
if (storeLoading) return
|
||||||
try {
|
try {
|
||||||
@@ -185,53 +159,6 @@ const OrderConfirm = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const findOrderIdByOrderNo = async (orderNo: string): Promise<number | undefined> => {
|
|
||||||
try {
|
|
||||||
const list = await listShopOrder({ orderNo, userId } as any)
|
|
||||||
const first = (list || []).find(o => o?.orderNo === orderNo)
|
|
||||||
return first?.orderId
|
|
||||||
} catch (_e) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const consumeTicketsForOrder = async (
|
|
||||||
needQty: number,
|
|
||||||
orderNo: string,
|
|
||||||
orderId?: number
|
|
||||||
) => {
|
|
||||||
const plan = buildConsumePlan(needQty)
|
|
||||||
if (!plan.length) throw new Error('水票可用次数不足')
|
|
||||||
|
|
||||||
// NOTE: This is a client-side best-effort implementation.
|
|
||||||
// For strict consistency (order + ticket deduction + log in one transaction),
|
|
||||||
// please implement a backend API to do these steps atomically.
|
|
||||||
for (const item of plan) {
|
|
||||||
const t = item.ticket
|
|
||||||
const availableBefore = Number(t.availableQty || 0)
|
|
||||||
|
|
||||||
await updateGltUserTicket({
|
|
||||||
...t,
|
|
||||||
availableQty: item.availableAfter,
|
|
||||||
usedQty: item.usedAfter
|
|
||||||
})
|
|
||||||
|
|
||||||
// Write-off log (核销记录)
|
|
||||||
await addGltUserTicketLog({
|
|
||||||
userTicketId: t.id,
|
|
||||||
changeType: 2, // 约定:2=消费/核销(若后端有枚举,请按后端约定调整)
|
|
||||||
changeAvailable: -item.qty,
|
|
||||||
changeUsed: item.qty,
|
|
||||||
availableAfter: item.availableAfter,
|
|
||||||
usedAfter: item.usedAfter,
|
|
||||||
orderId,
|
|
||||||
orderNo,
|
|
||||||
userId: userId || t.userId,
|
|
||||||
comments: `水票下单核销:${item.qty} 张(${availableBefore}→${item.availableAfter})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (submitLoading) return
|
if (submitLoading) return
|
||||||
if (!goods?.goodsId) return
|
if (!goods?.goodsId) return
|
||||||
@@ -249,6 +176,10 @@ const OrderConfirm = () => {
|
|||||||
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!selectedTicket?.id) {
|
||||||
|
Taro.showToast({ title: '请选择水票', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
if (availableTicketTotal <= 0) {
|
if (availableTicketTotal <= 0) {
|
||||||
Taro.showToast({ title: '暂无可用水票', icon: 'none' })
|
Taro.showToast({ title: '暂无可用水票', icon: 'none' })
|
||||||
return
|
return
|
||||||
@@ -278,41 +209,23 @@ const OrderConfirm = () => {
|
|||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
Taro.showLoading({ title: '提交中...' })
|
Taro.showLoading({ title: '提交中...' })
|
||||||
|
|
||||||
const orderData: OrderCreateRequest = {
|
await addGltTicketOrder({
|
||||||
goodsItems: [{ goodsId: goods.goodsId, quantity: finalQty }],
|
userTicketId: selectedTicket.id,
|
||||||
addressId: address.id,
|
|
||||||
storeId: selectedStore.id,
|
storeId: selectedStore.id,
|
||||||
storeName: selectedStore.name,
|
addressId: address.id,
|
||||||
payType: PAY_TYPE_FREE,
|
totalNum: finalQty,
|
||||||
deliveryType: 0,
|
buyerRemarks: orderRemark,
|
||||||
comments: orderRemark || '水票下单'
|
// Backend may take userId from token; pass-through is harmless if backend ignores it.
|
||||||
}
|
userId,
|
||||||
|
comments: goods.name ? `立即送水:${goods.name}` : '立即送水'
|
||||||
const res = await createOrder(orderData)
|
})
|
||||||
const orderNo = res?.orderNo
|
|
||||||
if (!orderNo) throw new Error('下单失败,请稍后重试')
|
|
||||||
|
|
||||||
const orderId = await findOrderIdByOrderNo(orderNo)
|
|
||||||
try {
|
|
||||||
await consumeTicketsForOrder(finalQty, orderNo, orderId)
|
|
||||||
} catch (consumeErr: any) {
|
|
||||||
console.error('订单已创建,但水票核销失败:', { orderNo, consumeErr })
|
|
||||||
await Taro.showModal({
|
|
||||||
title: '下单已成功',
|
|
||||||
content: `订单已创建(${orderNo}),但水票扣除/核销记录写入失败,请联系管理员处理。`,
|
|
||||||
showCancel: false
|
|
||||||
})
|
|
||||||
// 避免用户重复下单:直接跳转到订单列表查看处理结果
|
|
||||||
Taro.redirectTo({ url: '/user/order/order' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadUserTickets()
|
await loadUserTickets()
|
||||||
|
|
||||||
Taro.showToast({ title: '下单成功', icon: 'success' })
|
Taro.showToast({ title: '下单成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 跳转到“我的送水订单”(当前项目使用“我的订单”页承载)
|
// 跳转到“我的送水订单”
|
||||||
Taro.redirectTo({ url: '/user/order/order' })
|
Taro.redirectTo({ url: '/user/ticket/orders/index' })
|
||||||
}, 800)
|
}, 800)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('水票下单失败:', e)
|
console.error('水票下单失败:', e)
|
||||||
@@ -348,12 +261,6 @@ const OrderConfirm = () => {
|
|||||||
setAddress(addressRes[0])
|
setAddress(addressRes[0])
|
||||||
}
|
}
|
||||||
await loadUserTickets()
|
await loadUserTickets()
|
||||||
|
|
||||||
// Clamp quantity after loading tickets/stock.
|
|
||||||
setQuantity(prev => {
|
|
||||||
const upper = maxQuantity > 0 ? maxQuantity : 1
|
|
||||||
return Math.max(1, Math.min(prev, upper))
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载数据失败:', err)
|
console.error('加载数据失败:', err)
|
||||||
setError('加载数据失败,请重试')
|
setError('加载数据失败,请重试')
|
||||||
@@ -381,6 +288,18 @@ const OrderConfirm = () => {
|
|||||||
})
|
})
|
||||||
}, [maxQuantity])
|
}, [maxQuantity])
|
||||||
|
|
||||||
|
// Auto-pick a default ticket (first usable) when ticket list changes.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!usableTickets.length) {
|
||||||
|
setSelectedTicketId(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentValid = selectedTicketId && usableTickets.some(t => Number(t.id) === Number(selectedTicketId))
|
||||||
|
if (!currentValid) {
|
||||||
|
setSelectedTicketId(Number(usableTickets[0].id))
|
||||||
|
}
|
||||||
|
}, [usableTickets, selectedTicketId])
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
loadAllData()
|
loadAllData()
|
||||||
@@ -477,12 +396,14 @@ const OrderConfirm = () => {
|
|||||||
title={(
|
title={(
|
||||||
<View className="flex items-center gap-2">
|
<View className="flex items-center gap-2">
|
||||||
<Ticket className={'text-gray-500'}/>
|
<Ticket className={'text-gray-500'}/>
|
||||||
<Text>可用水票</Text>
|
<Text>选择水票</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
extra={(
|
extra={(
|
||||||
<View className={'flex items-center gap-2'}>
|
<View className={'flex items-center gap-2'}>
|
||||||
<View className={'text-gray-900'}>{availableTicketTotal} 张</View>
|
<View className={'text-gray-900'}>
|
||||||
|
{selectedTicket ? `${selectedTicket.templateName || '水票'}(可用${selectedTicket.availableQty ?? 0})` : '请选择'}
|
||||||
|
</View>
|
||||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -530,14 +451,21 @@ const OrderConfirm = () => {
|
|||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
{usableTickets.map((t) => (
|
{usableTickets.map((t) => {
|
||||||
|
const active = selectedTicket?.id && Number(selectedTicket.id) === Number(t.id)
|
||||||
|
return (
|
||||||
<Cell
|
<Cell
|
||||||
key={t.id}
|
key={t.id}
|
||||||
title={<Text>{t.templateName || '水票'}</Text>}
|
title={<Text className={active ? 'text-green-600' : ''}>{t.templateName || '水票'}</Text>}
|
||||||
description={t.orderNo ? `来源订单:${t.orderNo}` : ''}
|
description={t.orderNo ? `来源订单:${t.orderNo}` : ''}
|
||||||
extra={<Text className="text-gray-700">可用 {t.availableQty ?? 0}</Text>}
|
extra={<Text className="text-gray-700">可用 {t.availableQty ?? 0}</Text>}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTicketId(Number(t.id))
|
||||||
|
setTicketPopupVisible(false)
|
||||||
|
Taro.showToast({ title: '水票已选择', icon: 'success' })
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
)})}
|
||||||
{!usableTickets.length && (
|
{!usableTickets.length && (
|
||||||
<Cell title={<Text className="text-gray-500">暂无可用水票</Text>} />
|
<Cell title={<Text className="text-gray-500">暂无可用水票</Text>} />
|
||||||
)}
|
)}
|
||||||
@@ -621,7 +549,7 @@ const OrderConfirm = () => {
|
|||||||
type="success"
|
type="success"
|
||||||
size="large"
|
size="large"
|
||||||
loading={submitLoading}
|
loading={submitLoading}
|
||||||
disabled={availableTicketTotal <= 0 || maxQuantity <= 0}
|
disabled={!selectedTicket?.id || availableTicketTotal <= 0 || maxQuantity <= 0}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
>
|
>
|
||||||
{submitLoading ? '提交中...' : '立即提交'}
|
{submitLoading ? '提交中...' : '立即提交'}
|
||||||
|
|||||||
Reference in New Issue
Block a user