feat(ticket): 完善水票配送订单功能
- 优化导入路径,修复 PageParam 类型引用 - 新增 DeliverConfirmMode 类型定义,支持拍照完成和等待客户确认两种模式 - 实现配送确认的双模式功能,支持直接完成和等待确认流程 - 重构订单状态判断逻辑,完善配送流程状态管理 - 新增用户端确认收货功能,支持手动确认收货操作 - 优化订单列表展示,增加票号、取货点、门店电话等详细信息 - 添加地址复制和联系门店功能按钮 - 实现补传照片完成订单功能 - 更新订单流程状态显示,提供更准确的状态标识 - 添加配送确认模式切换的单选框界面 - 优化下单成功后的页面跳转逻辑 - 新增水票配送订单后端接口设计文档
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 送水订单
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import { pageGltUserTicket } from '@/api/glt/gltUserTicket';
|
||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
||||
import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
||||
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
||||
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model';
|
||||
import { BaseUrl } from '@/config/app';
|
||||
import dayjs from "dayjs";
|
||||
@@ -37,7 +37,10 @@ const UserTicketList = () => {
|
||||
const [orderPage, setOrderPage] = useState(1);
|
||||
const [orderTotal, setOrderTotal] = useState(0);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'ticket' | 'order'>('ticket');
|
||||
const [activeTab, setActiveTab] = useState<'ticket' | 'order'>(() => {
|
||||
const tab = Taro.getCurrentInstance().router?.params?.tab
|
||||
return tab === 'order' ? 'order' : 'ticket'
|
||||
});
|
||||
|
||||
const [qrVisible, setQrVisible] = useState(false);
|
||||
const [qrTicket, setQrTicket] = useState<GltUserTicket | null>(null);
|
||||
@@ -253,13 +256,56 @@ const UserTicketList = () => {
|
||||
return d.isValid() ? d.format('YYYY年MM月DD日 HH:mm:ss') : v;
|
||||
};
|
||||
|
||||
const getOrderStatusText = (status?: number) => {
|
||||
// Backend field meaning may vary; page asks for "是否送达".
|
||||
if (status === 1) return { text: '已送达', type: 'success' as const };
|
||||
if (status === 0) return { text: '未送达', type: 'warning' as const };
|
||||
return { text: status == null ? '-' : String(status), type: 'primary' as const };
|
||||
const getTicketOrderStatusMeta = (order: GltTicketOrder) => {
|
||||
if (order.status === 1) return { text: '已冻结', type: 'warning' as const };
|
||||
|
||||
const ds = order.deliveryStatus
|
||||
if (ds === 40 || order.receiveConfirmTime) return { text: '已完成', type: 'success' as const };
|
||||
if (ds === 30 || order.sendEndTime) return { text: '待确认收货', type: 'primary' as const };
|
||||
if (ds === 20 || order.sendStartTime) return { text: '配送中', type: 'primary' as const };
|
||||
if (ds === 10 || order.riderId) return { text: '待配送', type: 'warning' as const };
|
||||
return { text: '待派单', type: 'primary' as const };
|
||||
};
|
||||
|
||||
const canUserConfirmReceive = (order: GltTicketOrder) => {
|
||||
if (!order?.id) return false
|
||||
if (order.status === 1) return false
|
||||
if (order.deliveryStatus === 40) return false
|
||||
if (order.receiveConfirmTime) return false
|
||||
// 必须是“已送达”后才能确认收货
|
||||
return !!order.sendEndTime || order.deliveryStatus === 30
|
||||
}
|
||||
|
||||
const handleUserConfirmReceive = async (order: GltTicketOrder) => {
|
||||
if (!order?.id) return
|
||||
if (!canUserConfirmReceive(order)) return
|
||||
|
||||
const modal = await Taro.showModal({
|
||||
title: '确认收货',
|
||||
content: '请确认已收到本次送水,确认后将无法撤销。',
|
||||
confirmText: '确认收货'
|
||||
})
|
||||
if (!modal.confirm) return
|
||||
|
||||
try {
|
||||
Taro.showLoading({ title: '提交中...' })
|
||||
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
await updateGltTicketOrder({
|
||||
id: order.id,
|
||||
deliveryStatus: 40,
|
||||
receiveConfirmTime: now,
|
||||
receiveConfirmType: 10
|
||||
})
|
||||
Taro.showToast({ title: '已确认收货', icon: 'success' })
|
||||
await reloadOrders(true)
|
||||
} catch (e) {
|
||||
console.error('确认收货失败:', e)
|
||||
Taro.showToast({ title: '确认失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
if (activeTab === 'ticket') {
|
||||
reloadTickets(true).then();
|
||||
@@ -436,10 +482,52 @@ const UserTicketList = () => {
|
||||
</View>
|
||||
</View>
|
||||
{(() => {
|
||||
const meta = getOrderStatusText(item.status);
|
||||
const meta = getTicketOrderStatusMeta(item);
|
||||
return <Tag type={meta.type}>{meta.text}</Tag>;
|
||||
})()}
|
||||
</View>
|
||||
<View className="mt-2 text-xs text-gray-500">
|
||||
<Text>订单号:{item.id ?? '-'}</Text>
|
||||
</View>
|
||||
<View className="mt-1 text-xs text-gray-500">
|
||||
<Text>收货地址:{item.address || '-'}</Text>
|
||||
</View>
|
||||
{item.storeName ? (
|
||||
<View className="mt-1 text-xs text-gray-500">
|
||||
<Text>门店:{item.storeName}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{item.sendStartTime ? (
|
||||
<View className="mt-1 text-xs text-gray-500">
|
||||
<Text>开始配送:{formatDateTime(item.sendStartTime)}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{item.sendEndTime ? (
|
||||
<View className="mt-1 text-xs text-gray-500">
|
||||
<Text>送达时间:{formatDateTime(item.sendEndTime)}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{item.receiveConfirmTime ? (
|
||||
<View className="mt-1 text-xs text-gray-500">
|
||||
<Text>确认收货:{formatDateTime(item.receiveConfirmTime)}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{item.sendEndImg ? (
|
||||
<View className="mt-3">
|
||||
<Image src={item.sendEndImg} mode="aspectFill" style={{ width: '100%', height: '160px', borderRadius: '8px' }} />
|
||||
</View>
|
||||
) : null}
|
||||
{canUserConfirmReceive(item) ? (
|
||||
<View className="mt-3 flex justify-end">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => handleUserConfirmReceive(item)}
|
||||
>
|
||||
确认收货
|
||||
</Button>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
Space,
|
||||
Button,
|
||||
Dialog,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Image,
|
||||
Empty,
|
||||
InfiniteLoading,
|
||||
@@ -22,6 +24,8 @@ import { uploadFile } from '@/api/system/file'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm'
|
||||
|
||||
export default function TicketOrdersPage() {
|
||||
const riderId = useMemo(() => {
|
||||
const raw = Taro.getStorageSync('UserId')
|
||||
@@ -42,6 +46,7 @@ export default function TicketOrdersPage() {
|
||||
const [deliverSubmitting, setDeliverSubmitting] = useState(false)
|
||||
const [deliverOrder, setDeliverOrder] = useState<GltTicketOrder | null>(null)
|
||||
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
|
||||
const [deliverConfirmMode, setDeliverConfirmMode] = useState<DeliverConfirmMode>('photoComplete')
|
||||
|
||||
const riderTabs = useMemo(
|
||||
() => [
|
||||
@@ -95,7 +100,21 @@ export default function TicketOrdersPage() {
|
||||
if (!riderId || order.riderId !== riderId) return false
|
||||
if (order.receiveConfirmTime) return false
|
||||
if (order.deliveryStatus === 40) return false
|
||||
return !order.sendEndTime
|
||||
if (order.sendEndTime) return false
|
||||
|
||||
// 只允许在“配送中”阶段确认送达
|
||||
if (typeof order.deliveryStatus === 'number') return order.deliveryStatus === 20
|
||||
return !!order.sendStartTime
|
||||
}
|
||||
|
||||
const canCompleteByPhoto = (order: GltTicketOrder) => {
|
||||
if (!order.id) return false
|
||||
if (order.status === 1) return false
|
||||
if (!riderId || order.riderId !== riderId) return false
|
||||
if (order.receiveConfirmTime) return false
|
||||
if (order.deliveryStatus === 40) return false
|
||||
// 已送达但未完成:允许补传照片并直接完成
|
||||
return !!order.sendEndTime
|
||||
}
|
||||
|
||||
const filterByTab = useCallback(
|
||||
@@ -173,9 +192,10 @@ export default function TicketOrdersPage() {
|
||||
await reload(false)
|
||||
}, [hasMore, loading, reload])
|
||||
|
||||
const openDeliverDialog = (order: GltTicketOrder) => {
|
||||
const openDeliverDialog = (order: GltTicketOrder, opts?: { mode?: DeliverConfirmMode }) => {
|
||||
setDeliverOrder(order)
|
||||
setDeliverImg(order.sendEndImg)
|
||||
setDeliverConfirmMode(opts?.mode || (order.sendEndImg ? 'photoComplete' : 'waitCustomerConfirm'))
|
||||
setDeliverDialogVisible(true)
|
||||
}
|
||||
|
||||
@@ -210,22 +230,41 @@ export default function TicketOrdersPage() {
|
||||
const handleConfirmDelivered = async () => {
|
||||
if (!deliverOrder?.id) return
|
||||
if (deliverSubmitting) return
|
||||
if (deliverConfirmMode === 'photoComplete' && !deliverImg) {
|
||||
Taro.showToast({ title: '请先拍照/上传送达照片', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setDeliverSubmitting(true)
|
||||
try {
|
||||
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
// 送达时间:首次“确认送达”写入;补传照片时不要覆盖原送达时间
|
||||
const deliveredAt = deliverOrder.sendEndTime || now
|
||||
// 说明:
|
||||
// - sendEndImg:送达照片留档(可选/必填由后端策略决定)
|
||||
// - sendEndTime:配送员确认送达时间
|
||||
// - deliveryStatus:建议后端设置为 30(待客户确认)
|
||||
await updateGltTicketOrder({
|
||||
id: deliverOrder.id,
|
||||
deliveryStatus: 30,
|
||||
sendEndTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
sendEndImg: deliverImg
|
||||
})
|
||||
// - waitCustomerConfirm:只标记“已送达”,进入待客户确认(客户点击确认收货后完成)
|
||||
// - photoComplete:拍照留档后可直接完成(由后端策略决定是否允许)
|
||||
const payload: GltTicketOrder =
|
||||
deliverConfirmMode === 'photoComplete'
|
||||
? {
|
||||
id: deliverOrder.id,
|
||||
deliveryStatus: 40,
|
||||
sendEndTime: deliveredAt,
|
||||
sendEndImg: deliverImg,
|
||||
receiveConfirmTime: now,
|
||||
receiveConfirmType: 20
|
||||
}
|
||||
: {
|
||||
id: deliverOrder.id,
|
||||
deliveryStatus: 30,
|
||||
sendEndTime: deliveredAt,
|
||||
sendEndImg: deliverImg
|
||||
}
|
||||
|
||||
await updateGltTicketOrder(payload)
|
||||
Taro.showToast({ title: '已确认送达', icon: 'success' })
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
setDeliverConfirmMode('photoComplete')
|
||||
pageRef.current = 1
|
||||
await reload(true)
|
||||
} catch (e) {
|
||||
@@ -320,13 +359,19 @@ export default function TicketOrdersPage() {
|
||||
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 || o.comments || ''
|
||||
const ticketNo = o.userTicketId || '-'
|
||||
|
||||
const flow1Done = !!o.riderId
|
||||
const flow2Done = !!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20)
|
||||
const flow3Done = !!o.sendEndTime
|
||||
const flow2Done =
|
||||
!!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20)
|
||||
const flow3Done =
|
||||
!!o.sendEndTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 30)
|
||||
const flow4Done = !!o.receiveConfirmTime || o.deliveryStatus === 40
|
||||
|
||||
const phoneToCall = o.phone
|
||||
const storePhone = o.storePhone
|
||||
const pickupName = o.warehouseName || o.storeName
|
||||
const pickupAddr = o.warehouseAddress || o.storeAddress
|
||||
|
||||
return (
|
||||
<Cell key={String(o.id)} style={{ padding: '16px' }}>
|
||||
@@ -337,6 +382,7 @@ export default function TicketOrdersPage() {
|
||||
</View>
|
||||
|
||||
<View className="text-gray-400 text-xs mt-1">下单时间:{timeText}</View>
|
||||
<View className="text-gray-400 text-xs mt-1">票号:{ticketNo}</View>
|
||||
|
||||
<View className="mt-3 bg-white rounded-lg">
|
||||
<View className="text-sm text-gray-700">
|
||||
@@ -349,6 +395,16 @@ export default function TicketOrdersPage() {
|
||||
{o.nickname || '-'} {o.phone ? `(${o.phone})` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">取货点:</Text>
|
||||
<Text>{pickupName || '-'}</Text>
|
||||
</View>
|
||||
{pickupAddr ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">取货地址:</Text>
|
||||
<Text>{pickupAddr}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">预约配送:</Text>
|
||||
<Text>{o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}</Text>
|
||||
@@ -359,6 +415,12 @@ export default function TicketOrdersPage() {
|
||||
<Text className="text-gray-500 ml-3">门店:</Text>
|
||||
<Text>{o.storeName || '-'}</Text>
|
||||
</View>
|
||||
{o.storePhone ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">门店电话:</Text>
|
||||
<Text>{o.storePhone}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{remark ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">备注:</Text>
|
||||
@@ -402,7 +464,7 @@ export default function TicketOrdersPage() {
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow3Done ? 'text-purple-600 font-medium' : 'text-gray-400'}>3 送达留档</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 客户确认收货</Text>
|
||||
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 完成</Text>
|
||||
</View>
|
||||
|
||||
<View className="mt-3 flex justify-end">
|
||||
@@ -418,6 +480,29 @@ export default function TicketOrdersPage() {
|
||||
联系客户
|
||||
</Button>
|
||||
)}
|
||||
{!!addr && addr !== '-' && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
void Taro.setClipboardData({ data: addr })
|
||||
Taro.showToast({ title: '地址已复制', icon: 'none' })
|
||||
}}
|
||||
>
|
||||
复制地址
|
||||
</Button>
|
||||
)}
|
||||
{!!storePhone && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
Taro.makePhoneCall({ phoneNumber: storePhone })
|
||||
}}
|
||||
>
|
||||
联系门店
|
||||
</Button>
|
||||
)}
|
||||
{canStartDeliver(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
@@ -435,12 +520,24 @@ export default function TicketOrdersPage() {
|
||||
type="primary"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o)
|
||||
openDeliverDialog(o, { mode: 'waitCustomerConfirm' })
|
||||
}}
|
||||
>
|
||||
确认送达
|
||||
</Button>
|
||||
)}
|
||||
{canCompleteByPhoto(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o, { mode: 'photoComplete' })
|
||||
}}
|
||||
>
|
||||
补传照片完成
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
@@ -455,7 +552,13 @@ export default function TicketOrdersPage() {
|
||||
<Dialog
|
||||
title="确认送达"
|
||||
visible={deliverDialogVisible}
|
||||
confirmText={deliverSubmitting ? '提交中...' : '确认送达'}
|
||||
confirmText={
|
||||
deliverSubmitting
|
||||
? '提交中...'
|
||||
: deliverConfirmMode === 'photoComplete'
|
||||
? '拍照完成'
|
||||
: '确认送达'
|
||||
}
|
||||
cancelText="取消"
|
||||
onConfirm={handleConfirmDelivered}
|
||||
onCancel={() => {
|
||||
@@ -463,10 +566,18 @@ export default function TicketOrdersPage() {
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
setDeliverConfirmMode('photoComplete')
|
||||
}}
|
||||
>
|
||||
<View className="text-sm text-gray-700">
|
||||
<View>到达收货点后,可拍照留档(推荐/可设为必填),再点确认送达。</View>
|
||||
<View>到达收货点后,可选择“拍照留档直接完成”或“等待客户确认收货”。</View>
|
||||
|
||||
<View className="mt-3">
|
||||
<RadioGroup value={deliverConfirmMode} onChange={v => setDeliverConfirmMode(v as DeliverConfirmMode)}>
|
||||
<Radio value="photoComplete">拍照留档(直接完成)</Radio>
|
||||
<Radio value="waitCustomerConfirm">客户确认收货(可不拍照)</Radio>
|
||||
</RadioGroup>
|
||||
</View>
|
||||
<View className="mt-3">
|
||||
<Button size="small" onClick={handleChooseDeliverImg}>
|
||||
{deliverImg ? '重新拍照/上传' : '拍照/上传'}
|
||||
@@ -483,7 +594,7 @@ export default function TicketOrdersPage() {
|
||||
</View>
|
||||
)}
|
||||
<View className="mt-3 text-xs text-gray-500">
|
||||
送达后订单进入“待客户确认”;客户在用户端确认收货或超时自动确认(需后端支持)。
|
||||
说明:如选择“客户确认收货”,订单进入“待客户确认”;客户在用户端确认收货或超时自动确认(需后端支持)。
|
||||
</View>
|
||||
</View>
|
||||
</Dialog>
|
||||
|
||||
@@ -250,7 +250,7 @@ const OrderConfirm = () => {
|
||||
Taro.showToast({ title: '下单成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
// 跳转到“我的送水订单”
|
||||
Taro.redirectTo({ url: '/user/ticket/orders/index' })
|
||||
Taro.redirectTo({ url: '/user/ticket/index?tab=order' })
|
||||
}, 800)
|
||||
} catch (e: any) {
|
||||
console.error('水票下单失败:', e)
|
||||
|
||||
Reference in New Issue
Block a user