2026-08-04 21:01:39 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
2026-08-13 19:00:37 +08:00
|
|
|
|
import { ClipboardCheck, Pencil, RefreshCw } from '@lucide/vue'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
|
|
|
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
|
|
|
|
|
import { getPlatformInbound } from '../core/api'
|
|
|
|
|
|
import InboundHistoryPanel from '../core/InboundHistoryPanel.vue'
|
|
|
|
|
|
import InboundSummaryPanel from '../core/InboundSummaryPanel.vue'
|
|
|
|
|
|
import type { InboundOrderDetail } from '../core/types'
|
2026-08-17 17:49:10 +08:00
|
|
|
|
import { InboundMeasurementDialog } from '../measurement'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
import { MeasurementReviewPanel } from '../review'
|
|
|
|
|
|
import { InboundFeePanel } from '../fee'
|
2026-08-11 19:35:28 +08:00
|
|
|
|
import { inboundProcessStatusLabels, inboundStatusLabel } from '../status'
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
customerId: string
|
|
|
|
|
|
inboundId: string
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
changed: []
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const access = useAccessPreviewStore()
|
|
|
|
|
|
const activeTab = ref('overview')
|
2026-08-12 16:31:46 +08:00
|
|
|
|
const measurementDialogOpen = ref(false)
|
|
|
|
|
|
const reviewDialogOpen = ref(false)
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const loadError = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const detail = ref<InboundOrderDetail | null>(null)
|
|
|
|
|
|
const revision = ref(0)
|
|
|
|
|
|
let requestSerial = 0
|
|
|
|
|
|
let disposed = false
|
|
|
|
|
|
|
2026-08-30 17:43:03 +08:00
|
|
|
|
const title = computed(() => detail.value?.summary.inboundCode || '入仓码待补充')
|
2026-08-04 21:01:39 +08:00
|
|
|
|
const canViewMeasurement = computed(() => access.can('platform:firstleg:inbound:measure:view'))
|
2026-08-11 19:35:28 +08:00
|
|
|
|
const statusLabel = computed(() => inboundStatusLabel(detail.value?.summary.processStatus, inboundProcessStatusLabels, '未知状态'))
|
2026-08-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
async function loadDetail() {
|
|
|
|
|
|
const serial = ++requestSerial
|
|
|
|
|
|
if (!props.customerId || !props.inboundId) {
|
|
|
|
|
|
detail.value = null
|
|
|
|
|
|
loadError.value = null
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
loadError.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await getPlatformInbound(props.customerId, props.inboundId)
|
|
|
|
|
|
if (disposed || serial !== requestSerial) return
|
|
|
|
|
|
detail.value = response
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (disposed || serial !== requestSerial) return
|
|
|
|
|
|
detail.value = null
|
|
|
|
|
|
loadError.value = getApiErrorDetails(error, '入库单详情加载失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (!disposed && serial === requestSerial) loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshWorkspace() {
|
|
|
|
|
|
revision.value += 1
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDomainChanged() {
|
|
|
|
|
|
void refreshWorkspace()
|
|
|
|
|
|
emit('changed')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-12 16:31:46 +08:00
|
|
|
|
function handleReviewChanged() {
|
|
|
|
|
|
reviewDialogOpen.value = false
|
|
|
|
|
|
handleDomainChanged()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 21:01:39 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => [props.customerId, props.inboundId],
|
|
|
|
|
|
() => {
|
|
|
|
|
|
activeTab.value = 'overview'
|
|
|
|
|
|
detail.value = null
|
|
|
|
|
|
loadError.value = null
|
|
|
|
|
|
revision.value += 1
|
|
|
|
|
|
void loadDetail()
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
disposed = true
|
|
|
|
|
|
requestSerial += 1
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<section class="inbound-workspace" :aria-busy="loading">
|
|
|
|
|
|
<header class="workspace-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="workspace-title-row">
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<h2>入库码:{{ title }}</h2>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<StatusBadge v-if="detail" :status="statusLabel" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p>按基本信息、货物测量、审核、费用和托盘库位顺序处理,所有结果以服务端最新状态为准。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-button :loading="loading" :disabled="!customerId || !inboundId" @click="refreshWorkspace">
|
|
|
|
|
|
<RefreshCw :size="16" />刷新
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<LoadErrorState
|
|
|
|
|
|
v-if="loadError"
|
|
|
|
|
|
class="workspace-state"
|
|
|
|
|
|
title="入库作业详情暂时无法加载"
|
|
|
|
|
|
:message="loadError.message"
|
|
|
|
|
|
@retry="loadDetail"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #details><ApiErrorMeta :details="loadError" /></template>
|
|
|
|
|
|
</LoadErrorState>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="loading && !detail" class="workspace-state workspace-skeleton" aria-label="正在加载入库作业详情">
|
|
|
|
|
|
<el-skeleton :rows="8" animated />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-empty v-else-if="!detail" class="workspace-state" description="缺少所属企业信息,无法查看入库作业详情" />
|
|
|
|
|
|
|
|
|
|
|
|
<el-tabs v-else v-model="activeTab" class="workflow-tabs">
|
|
|
|
|
|
<el-tab-pane label="基本信息" name="overview">
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<InboundSummaryPanel :summary="detail.summary" :public-remark="detail.publicRemark" />
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</el-tab-pane>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
<el-tab-pane label="货品与测量" name="measurement" lazy>
|
2026-08-13 19:00:37 +08:00
|
|
|
|
<div class="workflow-action-row">
|
|
|
|
|
|
<span>核对原单数据、确认扫描箱数,并查看服务端计算的重量和体积结果。</span>
|
2026-08-17 17:49:10 +08:00
|
|
|
|
<el-button v-if="canViewMeasurement" type="primary" @click="measurementDialogOpen = true"><Pencil :size="16" />编辑测量数据</el-button>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</div>
|
2026-08-17 17:49:10 +08:00
|
|
|
|
<el-alert
|
|
|
|
|
|
:title="canViewMeasurement ? '称重图片、OCR、托盘与箱规统一在测量弹框中维护。' : '当前账号缺少测量查看权限。'"
|
|
|
|
|
|
:type="canViewMeasurement ? 'info' : 'warning'"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
/>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane label="测量审核" name="review" lazy>
|
2026-08-13 19:00:37 +08:00
|
|
|
|
<div v-if="canViewMeasurement" class="workflow-action-row">
|
|
|
|
|
|
<span>对照原单、设备实测和称重 OCR 结果后作出审核决定。</span>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
<el-button type="primary" @click="reviewDialogOpen = true"><ClipboardCheck :size="16" />审核测量数据</el-button>
|
|
|
|
|
|
</div>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<el-alert
|
|
|
|
|
|
v-else
|
|
|
|
|
|
title="无测量审核查看权限"
|
|
|
|
|
|
description="当前账号缺少测量查看权限,无法读取审核所需的原单、箱规和称重数据。"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-tab-pane>
|
2026-08-06 13:56:04 +08:00
|
|
|
|
<el-tab-pane label="费用信息" name="fee" lazy>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<InboundFeePanel
|
|
|
|
|
|
:key="`fee-${revision}`"
|
|
|
|
|
|
:tenant-id="customerId"
|
|
|
|
|
|
:inbound-id="inboundId"
|
|
|
|
|
|
@changed="handleDomainChanged"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-tab-pane>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
<el-tab-pane label="状态历史" name="history" lazy>
|
|
|
|
|
|
<InboundHistoryPanel
|
|
|
|
|
|
:pallet-locations="[]"
|
|
|
|
|
|
:files="[]"
|
|
|
|
|
|
:internal-notes="[]"
|
|
|
|
|
|
:status-history="detail.statusHistory"
|
|
|
|
|
|
:public-remark="null"
|
|
|
|
|
|
:sections="['history']"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane label="其他记录" name="records" lazy>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
<InboundHistoryPanel
|
|
|
|
|
|
:pallet-locations="detail.palletLocations"
|
2026-09-02 10:43:43 +08:00
|
|
|
|
:dropship-pallets="detail.dropshipPallets"
|
|
|
|
|
|
:customer-id="customerId"
|
|
|
|
|
|
:inbound-id="inboundId"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
:files="detail.files"
|
|
|
|
|
|
:internal-notes="detail.internalNotes"
|
2026-08-12 16:31:46 +08:00
|
|
|
|
:status-history="[]"
|
|
|
|
|
|
:public-remark="detail.publicRemark"
|
2026-09-02 10:43:43 +08:00
|
|
|
|
:sections="['dropship', 'allocations', 'files', 'notes', 'remark']"
|
2026-08-04 21:01:39 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
|
2026-08-17 17:49:10 +08:00
|
|
|
|
<InboundMeasurementDialog
|
2026-08-12 16:31:46 +08:00
|
|
|
|
v-model="measurementDialogOpen"
|
2026-08-17 17:49:10 +08:00
|
|
|
|
:customer-id="customerId"
|
|
|
|
|
|
:inbound-id="inboundId"
|
|
|
|
|
|
:inbound-code="title"
|
|
|
|
|
|
@updated="handleDomainChanged"
|
|
|
|
|
|
@submitted="handleDomainChanged"
|
|
|
|
|
|
/>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
v-model="reviewDialogOpen"
|
|
|
|
|
|
class="inbound-operation-dialog inbound-review-dialog"
|
2026-08-14 19:59:32 +08:00
|
|
|
|
:title="`测量审核 · ${title}`"
|
2026-08-13 19:00:37 +08:00
|
|
|
|
width="min(1240px, calc(100vw - 32px))"
|
2026-08-12 16:31:46 +08:00
|
|
|
|
top="4vh"
|
|
|
|
|
|
destroy-on-close
|
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="inbound-operation-dialog__body">
|
|
|
|
|
|
<MeasurementReviewPanel
|
|
|
|
|
|
v-if="canViewMeasurement"
|
|
|
|
|
|
:key="`review-dialog-${revision}`"
|
|
|
|
|
|
:tenant-id="customerId"
|
|
|
|
|
|
:inbound-id="inboundId"
|
2026-08-18 11:41:14 +08:00
|
|
|
|
:customer-name="detail?.summary.customerName || detail?.summary.companyName"
|
|
|
|
|
|
:channel-name="detail?.summary.channelName"
|
|
|
|
|
|
:channel-code="detail?.summary.channelCode"
|
2026-08-12 16:31:46 +08:00
|
|
|
|
@changed="handleReviewChanged"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
2026-08-04 21:01:39 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.inbound-workspace { min-width: 0; }
|
2026-08-06 13:56:04 +08:00
|
|
|
|
.workspace-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 18px 24px 16px; border-bottom: 1px solid var(--cc-border); }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
.workspace-title-row { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 10px; }
|
2026-08-06 13:56:04 +08:00
|
|
|
|
.workspace-title-row h2 { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: 16px; line-height: 1.35; }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
.workspace-header p { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
|
|
|
|
|
.workspace-state { margin: 24px; }
|
|
|
|
|
|
.workspace-skeleton { padding: 0; }
|
|
|
|
|
|
.workflow-tabs { min-width: 0; padding: 0 24px 24px; }
|
2026-08-06 13:56:04 +08:00
|
|
|
|
.workflow-tabs :deep(.el-tabs__header) { margin-bottom: 20px; }
|
|
|
|
|
|
.workflow-tabs :deep(.el-tabs__nav-wrap::after) { height: 1px; background: var(--cc-border); }
|
|
|
|
|
|
.workflow-tabs :deep(.el-tabs__active-bar) { height: 2px; background: var(--cc-primary); }
|
|
|
|
|
|
.workflow-tabs :deep(.el-tabs__item) { min-height: 44px; padding: 0 18px; color: #475467; font-size: 13px; font-weight: 500; }
|
|
|
|
|
|
.workflow-tabs :deep(.el-tabs__item.is-active) { color: var(--cc-primary); font-weight: 650; }
|
2026-08-13 19:00:37 +08:00
|
|
|
|
.workflow-action-row { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 16px; padding: 0 0 14px; border-bottom: 1px solid var(--cc-border); color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
2026-08-12 16:31:46 +08:00
|
|
|
|
.inbound-operation-dialog__body { min-width: 0; }
|
|
|
|
|
|
:global(.inbound-operation-dialog.el-dialog) { display: flex; max-width: calc(100vw - 32px); height: 92vh; max-height: 92vh; margin-bottom: 0; flex-direction: column; overflow: hidden; }
|
|
|
|
|
|
:global(.inbound-operation-dialog .el-dialog__header) { flex: 0 0 auto; margin: 0; padding: 18px 24px; border-bottom: 1px solid var(--cc-border); }
|
|
|
|
|
|
:global(.inbound-operation-dialog .el-dialog__body) { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 20px 24px; }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
.workspace-header { align-items: stretch; flex-direction: column; padding: 16px; }
|
|
|
|
|
|
.workspace-header :deep(.el-button) { width: 100%; min-height: 44px; }
|
|
|
|
|
|
.workspace-state { margin: 16px; }
|
|
|
|
|
|
.workflow-tabs { padding: 0 16px 16px; }
|
|
|
|
|
|
.workflow-tabs :deep(.el-tabs__nav-wrap) { overflow-x: auto; }
|
|
|
|
|
|
.workflow-tabs :deep(.el-tabs__item) { min-height: 44px; padding-inline: 14px; }
|
2026-08-13 19:00:37 +08:00
|
|
|
|
.workflow-action-row { align-items: stretch; flex-direction: column; }
|
|
|
|
|
|
.workflow-action-row :deep(.el-button) { width: 100%; min-height: 44px; }
|
2026-08-12 16:31:46 +08:00
|
|
|
|
:global(.inbound-operation-dialog.el-dialog) { width: calc(100vw - 20px) !important; max-width: calc(100vw - 20px); height: 92vh; }
|
|
|
|
|
|
:global(.inbound-operation-dialog .el-dialog__header) { padding: 16px; }
|
|
|
|
|
|
:global(.inbound-operation-dialog .el-dialog__body) { padding: 14px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 360px) {
|
|
|
|
|
|
:global(.inbound-operation-dialog.el-dialog) { width: calc(100vw - 12px) !important; max-width: calc(100vw - 12px); }
|
|
|
|
|
|
:global(.inbound-operation-dialog .el-dialog__header) { padding: 14px 12px; }
|
|
|
|
|
|
:global(.inbound-operation-dialog .el-dialog__body) { padding: 10px; }
|
2026-08-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|