Files
admin-web/src/shared/components/InboundCodeRelationshipDialog.vue
T

99 lines
11 KiB
Vue
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.
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation, type LoadingAssociationSummary, type OverseasInboundCodeRelationship, type PlatformInboundCodeAssociation } from '@/shared/api/inboundCodeRelations'
import ApiErrorMeta from './ApiErrorMeta.vue'
import AppDialog from './AppDialog.vue'
import LoadErrorState from './LoadErrorState.vue'
import StatusBadge from './StatusBadge.vue'
const props = defineProps<{ modelValue: boolean; inboundCode: string; canReadBase: boolean; canReadLoading: boolean; canReadOverseas: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const baseAssociation = ref<PlatformInboundCodeAssociation | null>(null)
const overseas = ref<OverseasInboundCodeRelationship | null>(null)
const loadingAssociation = ref<LoadingAssociationSummary | null>(null)
const baseError = ref<ApiErrorDetails | null>(null)
const overseasError = ref<ApiErrorDetails | null>(null)
const loadingAssociationError = ref<ApiErrorDetails | null>(null)
const loading = ref(false)
let serial = 0
const canReadBase = computed(() => props.canReadBase)
const canReadLoading = computed(() => props.canReadLoading)
const canReadOverseas = computed(() => props.canReadOverseas)
const normalizedCode = computed(() => props.inboundCode.trim())
function display(value: unknown, fallback = '—') { return String(value ?? '').trim() || fallback }
function overseasWarehouseName(value: OverseasInboundCodeRelationship['overseasWarehouse']) {
return value ? display(value.warehouseName, '仓库名称待补充') : '尚未分配海外仓'
}
function close(value: boolean) { emit('update:modelValue', value) }
async function load() {
const code = normalizedCode.value
const request = ++serial
baseAssociation.value = null; overseas.value = null; loadingAssociation.value = null; baseError.value = null; overseasError.value = null; loadingAssociationError.value = null
if (!code || (!canReadBase.value && !canReadLoading.value && !canReadOverseas.value)) return
loading.value = true
const tasks: Promise<void>[] = []
if (canReadBase.value) tasks.push(getPlatformInboundCodeAssociation(code).then((value) => { if (request === serial) baseAssociation.value = value }).catch((cause) => { if (request === serial) baseError.value = getApiErrorDetails(cause, '订单基础关联数据加载失败') }))
if (canReadLoading.value) tasks.push(getLoadingAssociationSummary(code).then((value) => { if (request === serial) loadingAssociation.value = value }).catch((cause) => { if (request === serial) loadingAssociationError.value = getApiErrorDetails(cause, '装柜与PDA关联数据加载失败') }))
if (canReadOverseas.value) tasks.push(getOverseasInboundCodeRelationship(code).then((value) => { if (request === serial) overseas.value = value }).catch((cause) => { if (request === serial) overseasError.value = getApiErrorDetails(cause, '海外入仓关联数据加载失败') }))
await Promise.all(tasks)
if (request === serial) loading.value = false
}
watch(() => [props.modelValue, props.inboundCode, props.canReadBase, props.canReadLoading, props.canReadOverseas] as const, ([open]) => {
if (open) void load()
else serial += 1
}, { immediate: true })
</script>
<template>
<AppDialog class="inbound-relationship-dialog" :model-value="modelValue" :title="`入仓码关联数据 · ${normalizedCode || '未提供'}`" width="min(1060px, 94vw)" :loading="loading" :show-footer="false" destroy-on-close @update:model-value="close">
<el-alert v-if="!normalizedCode" type="warning" :closable="false" show-icon title="缺少入仓码,已停止查询关联数据。" />
<el-alert v-else-if="!canReadBase && !canReadLoading && !canReadOverseas" type="warning" :closable="false" show-icon title="当前账号无入仓关联数据查看权限。" />
<div v-else class="relationship-sections">
<section v-if="canReadBase" class="relationship-section" aria-label="订单基础关联">
<header><h3>订单基础信息</h3><StatusBadge v-if="baseAssociation?.orderStatus" class="relationship-status" :status="baseAssociation.orderStatus" /></header>
<LoadErrorState v-if="baseError" title="订单基础关联加载失败" :message="baseError.message" @retry="load"><template #details><ApiErrorMeta :details="baseError" /></template></LoadErrorState>
<dl v-else-if="baseAssociation" class="compact-facts">
<div><dt>入仓码</dt><dd :title="display(baseAssociation.inboundCode)">{{ display(baseAssociation.inboundCode) }}</dd></div><div><dt>订单编码</dt><dd :title="display(baseAssociation.orderNo, '订单编码待补充')">{{ display(baseAssociation.orderNo, '订单编码待补充') }}</dd></div>
<div><dt>客户</dt><dd :title="display(baseAssociation.customerName, '客户名称待补充')">{{ display(baseAssociation.customerName, '客户名称待补充') }}</dd></div><div><dt>目的类型</dt><dd>{{ display(baseAssociation.destinationType) }}</dd></div>
<div><dt>目的国家</dt><dd>{{ display(baseAssociation.destinationCountryCode) }}</dd></div><div><dt>目的仓</dt><dd>{{ display(baseAssociation.destinationWarehouseName, '目的仓名称待补充') }}<small v-if="baseAssociation.destinationWarehouseCode">{{ baseAssociation.destinationWarehouseCode }}</small></dd></div>
</dl>
</section>
<section v-if="canReadLoading" class="relationship-section" aria-label="装柜与PDA关联">
<header><h3>装柜与PDA</h3><StatusBadge v-if="loadingAssociation?.loadingStatus" class="relationship-status" :status="loadingAssociation.loadingStatus" /></header>
<LoadErrorState v-if="loadingAssociationError" title="装柜关联加载失败" :message="loadingAssociationError.message" @retry="load"><template #details><ApiErrorMeta :details="loadingAssociationError" /></template></LoadErrorState>
<dl v-else-if="loadingAssociation" class="compact-facts">
<div><dt>订单编码</dt><dd :title="display(loadingAssociation.orderNo, '订单编码待补充')">{{ display(loadingAssociation.orderNo, '订单编码待补充') }}</dd></div><div><dt>装柜计划</dt><dd :title="display(loadingAssociation.loadingPlanNo, '尚未进入装柜')">{{ display(loadingAssociation.loadingPlanNo, '尚未进入装柜') }}</dd></div>
<div><dt>柜号</dt><dd :title="display(loadingAssociation.containerNo || loadingAssociation.cabinetNo, '尚未关联柜号')">{{ display(loadingAssociation.containerNo || loadingAssociation.cabinetNo, '尚未关联柜号') }}</dd></div><div><dt>同柜组</dt><dd :title="display(loadingAssociation.coLoadGroupNo)">{{ display(loadingAssociation.coLoadGroupNo) }}</dd></div>
<div><dt>装柜状态</dt><dd>{{ display(loadingAssociation.loadingStatus) }}</dd></div><div><dt>PDA任务状态</dt><dd>{{ display(loadingAssociation.pdaTaskStatus, '尚未生成PDA任务') }}</dd></div>
</dl>
</section>
<section v-if="canReadOverseas" class="relationship-section" aria-label="海外入仓关联">
<header><h3>海外入仓与库存</h3><StatusBadge v-if="overseas?.overseasReceivingStatus" class="relationship-status" :status="overseas.overseasReceivingStatus" /></header>
<LoadErrorState v-if="overseasError" title="海外关联加载失败" :message="overseasError.message" @retry="load"><template #details><ApiErrorMeta :details="overseasError" /></template></LoadErrorState>
<el-empty v-else-if="overseas && !overseas.found" description="服务端未找到该入仓码的海外关联数据" :image-size="56" />
<template v-else-if="overseas">
<dl class="compact-facts"><div><dt>客户</dt><dd>{{ display(overseas.customer?.customerName, '客户名称待补充') }}</dd></div><div><dt>海外仓</dt><dd>{{ overseasWarehouseName(overseas.overseasWarehouse) }}<small v-if="overseas.overseasWarehouse?.warehouseCode">{{ overseas.overseasWarehouse.warehouseCode }}</small></dd></div></dl>
<div class="compact-list"><strong>尾程订单</strong><span :title="overseas.taillegOrders.map((item) => item.orderNo).join('、')">{{ overseas.taillegOrders.map((item) => `${item.orderNo}${item.orderStatus ? `(${item.orderStatus})` : ''}`).filter(Boolean).join('、') || '—' }}</span></div>
<div class="compact-list"><strong>入库计划</strong><span :title="overseas.inboundPlans.map((item) => item.planNo).join('、')">{{ overseas.inboundPlans.map((item) => `${item.planNo}${item.planStatus ? `(${item.planStatus})` : ''}${item.receivingStatus ? ` · ${item.receivingStatus}` : ''}`).join('、') || '—' }}</span></div>
<div class="compact-list"><strong>托盘号</strong><span :title="overseas.palletCodes.join('、')">{{ overseas.palletCodes.join('、') || '—' }}</span></div>
<el-table v-if="overseas.inventory.length" :data="overseas.inventory" size="small" border class="inventory-table">
<el-table-column prop="skuCode" label="SKU编码" min-width="150" /><el-table-column label="产品名称" min-width="190"><template #default="{ row }">{{ display(row.productNameZh || row.productNameEn) }}</template></el-table-column><el-table-column prop="packageCount" label="库存箱数" width="110" align="right" />
</el-table>
</template>
</section>
</div>
</AppDialog>
</template>
<style scoped>
:global(.inbound-relationship-dialog.el-dialog){width:min(1060px,94vw);max-width:94vw}:global(.inbound-relationship-dialog .el-dialog__body){box-sizing:border-box;min-width:0;max-height:calc(90vh - 112px);overflow-x:hidden;overflow-y:auto;padding-right:28px}:global(.inbound-relationship-dialog .app-dialog__body){box-sizing:border-box;width:auto;max-width:100%;min-width:0;overflow-x:hidden}.relationship-sections{display:grid;width:auto;max-width:100%;min-width:0;grid-template-columns:minmax(0,1fr);gap:12px}.relationship-section{box-sizing:border-box;width:100%;min-width:0;border:1px solid var(--cc-border);border-radius:8px;padding:12px;background:var(--cc-surface)}.relationship-section>header{display:flex;min-width:0;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 12px;margin-bottom:10px}.relationship-section h3{flex:1 1 auto;min-width:0;margin:0;font-size:15px}.relationship-status{flex:0 0 auto;max-width:100%}.compact-facts{display:grid;min-width:0;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;margin:0;background:var(--cc-border)}.compact-facts>div{display:grid;min-width:0;grid-template-columns:92px minmax(0,1fr);gap:8px;padding:8px 10px;background:var(--cc-surface)}.compact-facts dt{color:var(--cc-muted)}.compact-facts dd{min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.compact-facts small{display:block;overflow:hidden;color:var(--cc-muted);margin-top:2px;text-overflow:ellipsis;white-space:nowrap}.compact-list{display:grid;min-width:0;grid-template-columns:92px minmax(0,1fr);gap:8px;padding:8px 10px;border-top:1px solid var(--cc-border)}.compact-list span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inventory-table{width:100%;max-width:100%;min-width:0;margin-top:8px}@media(max-width:720px){:global(.inbound-relationship-dialog .el-dialog__body){padding-right:20px}.compact-facts{grid-template-columns:1fr}}
</style>