349 lines
8.1 KiB
Plaintext
349 lines
8.1 KiB
Plaintext
<template>
|
||
<view class="container">
|
||
<view class="header">
|
||
<text class="back-link" @click="goBack">⬅ 返回</text>
|
||
<text class="title">第三方物流 API 模拟发送器</text>
|
||
<text class="subtitle">模拟外部物流平台向后端推送 Webhook 轨迹数据</text>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-title">1. 选择目标订单 (已发货)</text>
|
||
<scroll-view class="order-list" direction="horizontal">
|
||
<view v-for="(item, index) in shippedOrders" :key="index"
|
||
:class="['order-card', selectedOrderIndex == index ? 'active' : '']"
|
||
@click="selectOrder(index)">
|
||
<text class="order-no">{{ item.order_no }}</text>
|
||
<text class="tracking-no">{{ item.carrier }}: {{ item.tracking_no }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-title">2. 构造协议数据 (YTO Protocol)</text>
|
||
<view class="form-group">
|
||
<text class="label">物流单号 (mailNo):</text>
|
||
<input class="input" v-model="form.mailNo" placeholder="请输入运单号" />
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="label">订单号 (txLogisticId):</text>
|
||
<input class="input" v-model="form.txLogisticId" placeholder="请输入关联订单号" />
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="label">事件状态 (infoContent):</text>
|
||
<picker :range="statusOptions" range-key="label" @change="onStatusChange">
|
||
<view class="picker-val">{{ currentStatusLabel }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="label">轨迹描述 (remark):</text>
|
||
<textarea class="textarea" v-model="form.remark" placeholder="描述当前的物流位置或状态..." />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="payload-preview">
|
||
<text class="preview-title">接口发送原始数据预览:</text>
|
||
<view class="code-block">
|
||
<text class="code-text">{{ jsonString }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<button class="btn-send" type="primary" @click="sendWebhook">立即推送 API 数据</button>
|
||
|
||
<view class="footer-links">
|
||
<text class="link" @click="goToLogs">查看接收日志 (Webhook Logs)</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { mockService, MockOrder } from './mock-service.uts'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
|
||
const orders = ref([] as MockOrder[])
|
||
const shippedOrders = computed((): MockOrder[] => {
|
||
return orders.value.filter((o: MockOrder): boolean => o.status !== 'PENDING' && o.tracking_no !== '')
|
||
})
|
||
|
||
async function loadOrders() {
|
||
const data = await mockService.getMockOrders()
|
||
orders.value = data
|
||
}
|
||
|
||
onShow(() => {
|
||
loadOrders()
|
||
})
|
||
|
||
function goBack() {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
const selectedOrderIndex = ref(-1)
|
||
|
||
const form = reactive({
|
||
mailNo: '',
|
||
txLogisticId: '',
|
||
infoContent: 'SEND',
|
||
remark: '快件已到达【XX分拨中心】,准备发往下一站',
|
||
acceptTime: '',
|
||
carrier: '顺丰速运'
|
||
})
|
||
|
||
const statusOptions = [
|
||
{ label: '已揽收 (GOT)', value: 'GOT' },
|
||
{ label: '运输中 (SEND)', value: 'SEND' },
|
||
{ label: '派送中 (SENT)', value: 'SENT' },
|
||
{ label: '待取件 (PICKUP)', value: 'PICKUP' },
|
||
{ label: '已签收 (SIGNED)', value: 'SIGNED' },
|
||
{ label: '异常 (FAILED)', value: 'FAILED' },
|
||
{ label: '退回 (RETURNED)', value: 'RETURNED' }
|
||
]
|
||
|
||
const currentStatusLabel = computed((): string => {
|
||
const opt = statusOptions.find((o: UTSJSONObject): boolean => o['value'] === form.infoContent)
|
||
return (opt != null) ? opt['label'] as string : '请选择'
|
||
})
|
||
|
||
const jsonString = computed((): string => {
|
||
return JSON.stringify(form, null, 2)
|
||
})
|
||
|
||
function selectOrder(index: number) {
|
||
selectedOrderIndex.value = index
|
||
const order = shippedOrders.value[index]
|
||
form.mailNo = order.tracking_no
|
||
form.txLogisticId = order.order_no
|
||
form.carrier = order.carrier + '速递'
|
||
|
||
// 根据订单当前状态智能预设
|
||
if (order.status === 'SHIPPED') {
|
||
form.infoContent = 'SEND'
|
||
form.remark = '快件已到达北京分拨中心'
|
||
} else if (order.status === 'IN_TRANSIT') {
|
||
form.infoContent = 'SENT'
|
||
form.remark = '派送员王师傅(13700008888)正在派件'
|
||
}
|
||
}
|
||
|
||
function onStatusChange(e: UniPickerChangeEvent) {
|
||
const idx = e.detail.value as number
|
||
form.infoContent = statusOptions[idx].value
|
||
}
|
||
|
||
async function sendWebhook() {
|
||
if (!form.mailNo) {
|
||
uni.showToast({ title: '请先填写运单号', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 检查单号对应的订单是否已签收
|
||
const targetOrder = orders.value.find((o: MockOrder): boolean => o.tracking_no === form.mailNo)
|
||
if (targetOrder != null && targetOrder.status === 'DELIVERED') {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '该订单已显示已签收,无需继续推送物流动态。',
|
||
showCancel: false
|
||
})
|
||
return
|
||
}
|
||
|
||
// 获取当前时间戳作为圆通要求的 acceptTime
|
||
const now = new Date()
|
||
const Y = now.getFullYear()
|
||
const M = (now.getMonth() + 1).toString().padStart(2, '0')
|
||
const D = now.getDate().toString().padStart(2, '0')
|
||
const h = now.getHours().toString().padStart(2, '0')
|
||
const m = now.getMinutes().toString().padStart(2, '0')
|
||
const s = now.getSeconds().toString().padStart(2, '0')
|
||
form.acceptTime = `${Y}-${M}-${D} ${h}:${m}:${s}`
|
||
|
||
// 执行模拟推送 (转换为普通对象以兼容 UTS)
|
||
const payload = {
|
||
mailNo: form.mailNo,
|
||
txLogisticId: form.txLogisticId,
|
||
infoContent: form.infoContent,
|
||
remark: form.remark,
|
||
acceptTime: form.acceptTime,
|
||
carrier: form.carrier
|
||
} as UTSJSONObject
|
||
|
||
uni.showLoading({ title: '正在推送至数据库...' })
|
||
const success = await mockService.pushWebhookData(payload)
|
||
uni.hideLoading()
|
||
|
||
if (success) {
|
||
uni.showToast({ title: 'API 发送成功!', icon: 'success' })
|
||
// 成功后刷新列表,更新订单状态
|
||
loadOrders()
|
||
} else {
|
||
uni.showModal({
|
||
title: '发送失败',
|
||
content: '系统未找到该运单号,后端拒绝接收该数据。',
|
||
showCancel: false
|
||
})
|
||
}
|
||
}
|
||
|
||
function goToLogs() {
|
||
uni.navigateTo({ url: '/pages/mall/delivery/test/webhook-logs' })
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.container {
|
||
padding: 20px;
|
||
background-color: #f8f9fa;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.header {
|
||
margin-bottom: 25px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.back-link {
|
||
font-size: 14px;
|
||
color: #007aff;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.title {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin-top: 5px;
|
||
display: block;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 25px;
|
||
background: #fff;
|
||
padding: 15px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #444;
|
||
margin-bottom: 12px;
|
||
display: block;
|
||
}
|
||
|
||
.order-list {
|
||
white-space: nowrap;
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.order-card {
|
||
display: inline-block;
|
||
width: 160px;
|
||
padding: 12px;
|
||
background: #f0f4f8;
|
||
border: 1.5px solid transparent;
|
||
border-radius: 8px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.order-card.active {
|
||
border-color: #007aff;
|
||
background: #eef6ff;
|
||
}
|
||
|
||
.order-no {
|
||
font-size: 13px;
|
||
font-weight: bold;
|
||
display: block;
|
||
}
|
||
|
||
.tracking-no {
|
||
font-size: 11px;
|
||
color: #888;
|
||
margin-top: 4px;
|
||
display: block;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.label {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin-bottom: 5px;
|
||
display: block;
|
||
}
|
||
|
||
.input {
|
||
font-size: 14px;
|
||
color: #333;
|
||
height: 35px;
|
||
}
|
||
|
||
.picker-val {
|
||
font-size: 14px;
|
||
color: #007aff;
|
||
padding: 5px 0;
|
||
}
|
||
|
||
.textarea {
|
||
font-size: 14px;
|
||
width: 100%;
|
||
height: 80px;
|
||
background: #fafafa;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.payload-preview {
|
||
background: #2d2d2d;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.preview-title {
|
||
color: #aaa;
|
||
font-size: 12px;
|
||
margin-bottom: 10px;
|
||
display: block;
|
||
}
|
||
|
||
.code-block {
|
||
font-family: monospace;
|
||
}
|
||
|
||
.code-text {
|
||
color: #69f0ae;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.btn-send {
|
||
margin-top: 10px;
|
||
border-radius: 25px;
|
||
}
|
||
|
||
.footer-links {
|
||
text-align: center;
|
||
margin-top: 25px;
|
||
}
|
||
|
||
.link {
|
||
color: #007aff;
|
||
font-size: 14px;
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|