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

@@ -1,7 +1,16 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { adminApi, type AdminExpressCompanyItem, type AdminManualOrderCreatePayload, type AdminManualOrderMeta, type AdminOrderDetail, type AdminOrderListItem, type AdminOrderWarehouseOption } from "../../api/admin";
import {
adminApi,
type AdminExpressCompanyItem,
type AdminExpressCompanyRecognitionCandidate,
type AdminManualOrderCreatePayload,
type AdminManualOrderMeta,
type AdminOrderDetail,
type AdminOrderListItem,
type AdminOrderWarehouseOption,
} from "../../api/admin";
import OrderStatusTag from "../../components/OrderStatusTag.vue";
import { recognizeReturnAddress } from "../../utils/address-recognition";
@@ -19,6 +28,8 @@ const returnDialogVisible = ref(false);
const returnSubmitting = ref(false);
const returnExpressCompany = ref("");
const returnTrackingNo = ref("");
const returnRecognitionLoading = ref(false);
const returnRecognitionCandidates = ref<AdminExpressCompanyRecognitionCandidate[]>([]);
const expressCompanyLoading = ref(false);
const expressCompanyOptions = ref<AdminExpressCompanyItem[]>([]);
const defaultExpressCompany = ref("");
@@ -139,6 +150,8 @@ const expressCompanySelectOptions = computed(() => {
...expressCompanyOptions.value,
];
});
let returnRecognitionTimer: ReturnType<typeof setTimeout> | undefined;
function createManualOrderForm(): AdminManualOrderCreatePayload {
return {
service_provider: "anxinyan",
@@ -377,7 +390,53 @@ async function openReturnDialog() {
await ensureExpressCompanyOptions();
returnExpressCompany.value = detail.value.return_logistics?.express_company || defaultExpressCompany.value || expressCompanyOptions.value[0]?.company_name || "";
returnTrackingNo.value = detail.value.return_logistics?.tracking_no || "";
returnRecognitionCandidates.value = [];
returnDialogVisible.value = true;
if (returnTrackingNo.value) {
scheduleReturnRecognition();
}
}
async function recognizeReturnExpressCompany() {
const trackingNo = returnTrackingNo.value.trim();
if (!returnDialogVisible.value || !trackingNo) {
returnRecognitionCandidates.value = [];
return;
}
returnRecognitionLoading.value = true;
try {
const response = await adminApi.recognizeExpressCompany({
tracking_no: trackingNo,
company_name: returnExpressCompany.value.trim(),
});
const result = response.data;
returnRecognitionCandidates.value = result.candidates || [];
if (result.resolved) {
returnExpressCompany.value = result.resolved.company_name;
} else if (result.candidates.length === 1) {
returnExpressCompany.value = result.candidates[0].company_name;
}
} catch (error) {
console.error(error);
returnRecognitionCandidates.value = [];
} finally {
returnRecognitionLoading.value = false;
}
}
function scheduleReturnRecognition() {
if (returnRecognitionTimer) {
clearTimeout(returnRecognitionTimer);
}
returnRecognitionTimer = setTimeout(() => {
void recognizeReturnExpressCompany();
}, 500);
}
function chooseReturnRecognitionCandidate(candidate: AdminExpressCompanyRecognitionCandidate) {
returnExpressCompany.value = candidate.company_name;
returnRecognitionCandidates.value = [candidate];
}
async function submitReturnLogistics() {
@@ -409,6 +468,18 @@ async function submitReturnLogistics() {
}
}
watch(returnTrackingNo, () => {
if (returnDialogVisible.value) {
scheduleReturnRecognition();
}
});
onBeforeUnmount(() => {
if (returnRecognitionTimer) {
clearTimeout(returnRecognitionTimer);
}
});
async function markReturnReceived() {
if (!detail.value) return;
returnReceiveSubmitting.value = true;
@@ -655,10 +726,18 @@ onMounted(fetchOrders);
<div class="order-detail-item__label">物流状态</div>
<div class="order-detail-item__value">{{ logisticsActionText }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">快递100状态</div>
<div class="order-detail-item__value">{{ detail.logistics_info.provider_status_text || detail.logistics_info.sync_status_text || "-" }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">最新节点</div>
<div class="order-detail-item__value">{{ detail.logistics_info.latest_desc || "-" }}</div>
</div>
<div class="order-detail-item order-detail-item--full" v-if="detail.logistics_info.sync_error">
<div class="order-detail-item__label">同步异常</div>
<div class="order-detail-item__value">{{ detail.logistics_info.sync_error }}</div>
</div>
<div class="order-detail-item order-detail-item--full" v-if="detail.logistics_info.latest_time">
<div class="order-detail-item__label">最新更新时间</div>
<div class="order-detail-item__value">{{ detail.logistics_info.latest_time }}</div>
@@ -680,10 +759,18 @@ onMounted(fetchOrders);
<div class="order-detail-item__label">物流状态</div>
<div class="order-detail-item__value">{{ detail.return_logistics.tracking_status_text }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">快递100状态</div>
<div class="order-detail-item__value">{{ detail.return_logistics.provider_status_text || detail.return_logistics.sync_status_text || "-" }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">最新节点</div>
<div class="order-detail-item__value">{{ detail.return_logistics.latest_desc || "-" }}</div>
</div>
<div class="order-detail-item order-detail-item--full" v-if="detail.return_logistics.sync_error">
<div class="order-detail-item__label">同步异常</div>
<div class="order-detail-item__value">{{ detail.return_logistics.sync_error }}</div>
</div>
<div class="order-detail-item order-detail-item--full" v-if="detail.return_logistics.latest_time">
<div class="order-detail-item__label">最新更新时间</div>
<div class="order-detail-item__value">{{ detail.return_logistics.latest_time }}</div>
@@ -738,6 +825,18 @@ onMounted(fetchOrders);
</div>
</div>
<div class="detail-card" v-if="detail.return_logistics" style="grid-column: 1 / -1">
<div class="detail-card__title">回寄物流轨迹</div>
<div v-if="detail.return_logistics.nodes.length" class="timeline-list" style="margin-top: 14px">
<div v-for="item in detail.return_logistics.nodes" :key="`${item.node_time}-${item.node_desc}`" class="timeline-node">
<div class="timeline-node__title">{{ item.node_desc }}</div>
<div class="timeline-node__time">{{ item.node_time }}</div>
<div class="timeline-node__desc">{{ item.node_location || "-" }}</div>
</div>
</div>
<el-empty v-else description="暂无回寄物流轨迹" :image-size="64" />
</div>
<div class="detail-card" v-if="detail.supplement_task" style="grid-column: 1 / -1">
<div class="detail-card__title">补图任务</div>
<div class="detail-card__desc">
@@ -810,6 +909,20 @@ onMounted(fetchOrders);
<el-form-item label="回寄运单号">
<el-input v-model="returnTrackingNo" placeholder="请输入回寄运单号" />
</el-form-item>
<div v-if="returnRecognitionLoading" style="margin: -8px 0 12px; color: var(--admin-text-subtle); font-size: 12px;">
正在识别快递公司...
</div>
<div v-if="returnRecognitionCandidates.length" style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px;">
<el-tag
v-for="candidate in returnRecognitionCandidates"
:key="`${candidate.company_code}-${candidate.company_name}`"
effect="plain"
style="cursor: pointer;"
@click="chooseReturnRecognitionCandidate(candidate)"
>
{{ candidate.company_name }}
</el-tag>
</div>
<el-alert
v-if="detail?.return_address"
type="info"