Files
medical-mall/pages/mall/delivery/test/api-simulator.uvue
2026-03-12 18:05:32 +08:00

350 lines
8.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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.getAllOrders()
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>