feat: add kuaidi100 logistics sync

This commit is contained in:
wushumin
2026-05-26 17:08:33 +08:00
parent 09d9fcbe69
commit a5f00d7e31
31 changed files with 2596 additions and 67 deletions

View File

@@ -63,6 +63,10 @@ const materialItems = computed(() => detail.value.materials || []);
const hasReturnAddress = computed(() => Boolean(detail.value.return_address));
const hasReturnLogistics = computed(() => Boolean(detail.value.return_logistics?.tracking_no));
const returnReceived = computed(() => detail.value.return_logistics?.tracking_status === "received");
const returnLogisticsNodes = computed(() => detail.value.return_logistics?.nodes || []);
const returnLogisticsStatusText = computed(
() => detail.value.return_logistics?.provider_status_text || detail.value.return_logistics?.tracking_status_text || "",
);
const canEditReturnAddress = computed(() => detail.value.order_info.can_edit_return_address);
const heroTagClass = computed(() => {
@@ -459,7 +463,7 @@ onShow(fetchDetail);
<view v-if="detail.return_logistics" id="return-logistics-card" class="return-logistics-card">
<view class="return-logistics-card__top">
<view class="return-logistics-card__title">回寄物流</view>
<text class="detail-chip">{{ detail.return_logistics.tracking_status_text }}</text>
<text class="detail-chip">{{ returnLogisticsStatusText || detail.return_logistics.tracking_status_text }}</text>
</view>
<view class="report-meta__row">
<text class="report-meta__label">快递公司</text>
@@ -473,6 +477,27 @@ onShow(fetchDetail);
<text class="report-meta__label">最新进展</text>
<text class="report-meta__value">{{ detail.return_logistics.latest_desc || "待平台登记回寄运单" }}</text>
</view>
<view v-if="returnLogisticsStatusText" class="report-meta__row">
<text class="report-meta__label">快递状态</text>
<text class="report-meta__value">{{ returnLogisticsStatusText }}</text>
</view>
<view v-if="returnLogisticsNodes.length" class="compact-timeline return-logistics-card__timeline">
<view
v-for="(item, index) in returnLogisticsNodes"
:key="`${item.node_time}-${item.node_desc}`"
:class="['compact-timeline__item', index === 0 ? 'compact-timeline__item--current' : '']"
>
<view class="compact-timeline__rail"></view>
<view class="compact-timeline__dot"></view>
<view class="compact-timeline__content">
<view class="compact-timeline__row">
<text class="compact-timeline__title">{{ item.node_desc }}</text>
<text class="compact-timeline__time">{{ item.node_time }}</text>
</view>
<view v-if="item.node_location" class="compact-timeline__desc">{{ item.node_location }}</view>
</view>
</view>
</view>
</view>
</view>
@@ -770,6 +795,10 @@ onShow(fetchDetail);
line-height: 1.6;
}
.return-logistics-card__timeline {
margin-top: 24rpx;
}
.return-address-card__address,
.return-address-sheet__item-address {
margin-top: 14rpx;

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { appApi, type ShippingDetailData } from "../../api/app";
import { appApi, type ExpressCompanyRecognitionCandidate, type ShippingDetailData } from "../../api/app";
import { shippingDetailFallback } from "../../mocks/app";
import { resolveErrorMessage, showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
import { getPrivacyMode, maskOrderNo } from "../../utils/privacy";
@@ -17,9 +17,18 @@ const warehouseSheetVisible = ref(false);
const loading = ref(false);
const pageReady = ref(false);
const loadError = ref("");
const recognitionLoading = ref(false);
const recognitionCandidates = ref<ExpressCompanyRecognitionCandidate[]>([]);
let recognitionTimer: ReturnType<typeof setTimeout> | undefined;
const submitted = computed(() => detail.value.logistics_info.is_submitted);
const canEditTracking = computed(() => !submitted.value && detail.value.can_submit_tracking);
const logisticsStatusText = computed(
() => detail.value.logistics_info.provider_status_text || detail.value.logistics_info.tracking_status_text || "待提交",
);
const logisticsLatestDesc = computed(
() => detail.value.logistics_info.latest_desc || detail.value.logistics_info.sync_status_text || "",
);
const hasWarehouseChoices = computed(
() => detail.value.shipping_options.can_select_warehouse && detail.value.shipping_options.list.length > 1,
);
@@ -102,6 +111,47 @@ function useCompany(name: string) {
expressCompany.value = name;
}
async function recognizeExpressCompany() {
const trackingValue = trackingNo.value.trim();
if (!trackingValue || submitted.value) {
recognitionCandidates.value = [];
return;
}
recognitionLoading.value = true;
try {
const result = await appApi.recognizeOrderShippingCompany({
tracking_no: trackingValue,
company_name: expressCompany.value.trim(),
});
recognitionCandidates.value = result.candidates || [];
if (result.resolved) {
expressCompany.value = result.resolved.company_name;
} else if (result.candidates.length === 1) {
expressCompany.value = result.candidates[0].company_name;
}
} catch (error) {
console.error(error);
recognitionCandidates.value = [];
} finally {
recognitionLoading.value = false;
}
}
function scheduleRecognition() {
if (recognitionTimer) {
clearTimeout(recognitionTimer);
}
recognitionTimer = setTimeout(() => {
void recognizeExpressCompany();
}, 500);
}
function chooseRecognitionCandidate(candidate: ExpressCompanyRecognitionCandidate) {
expressCompany.value = candidate.company_name;
recognitionCandidates.value = [candidate];
}
async function fetchDetail() {
if (!orderId.value) return;
loading.value = true;
@@ -168,6 +218,16 @@ onLoad((options) => {
});
onShow(fetchDetail);
watch(trackingNo, () => {
scheduleRecognition();
});
onBeforeUnmount(() => {
if (recognitionTimer) {
clearTimeout(recognitionTimer);
}
});
</script>
<template>
@@ -254,7 +314,7 @@ onShow(fetchDetail);
<view class="metric-card__label">当前寄送商品请确保与订单信息一致</view>
</view>
<view class="metric-card">
<view class="metric-card__value">{{ submitted ? detail.logistics_info.tracking_status_text : "待提交" }}</view>
<view class="metric-card__value">{{ submitted ? logisticsStatusText : "待提交" }}</view>
<view class="metric-card__label">寄送状态提交运单后我们会继续同步节点</view>
</view>
</view>
@@ -288,6 +348,17 @@ onShow(fetchDetail);
<input v-model="trackingNo" class="field-input" maxlength="40" placeholder="请输入快递单号" :disabled="submitted" />
</view>
</view>
<view v-if="recognitionLoading" class="form-group__hint">正在识别快递公司...</view>
<view v-if="recognitionCandidates.length" class="chip-list" style="margin-top: 16rpx;">
<view
v-for="candidate in recognitionCandidates"
:key="`${candidate.company_code}-${candidate.company_name}`"
class="choice-chip"
@click="chooseRecognitionCandidate(candidate)"
>
{{ candidate.company_name }}
</view>
</view>
<view class="form-group__hint">
{{ submitted ? "如物流信息存在异常,请联系平台客服协助处理。" : "提交后将进入待签收跟踪状态,请确认信息无误后再提交。" }}
</view>
@@ -295,7 +366,7 @@ onShow(fetchDetail);
<view v-if="submitted" class="section timeline-panel">
<view class="section__title">寄送轨迹</view>
<view class="section__desc">{{ detail.logistics_info.latest_desc }}</view>
<view class="section__desc">{{ logisticsLatestDesc }}</view>
<view class="timeline" style="margin-top: 24rpx">
<view
v-for="item in detail.logistics_nodes"