Files
admin-web/src/modules/inbounds/components/InboundWorkflowWorkspace.vue
T
2026-09-18 11:41:27 +08:00

271 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, onBeforeUnmount, ref, watch } from 'vue'
import { ClipboardCheck, Pencil, RefreshCw } from '@lucide/vue'
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, listPlatformInboundDropshipPallets } from '../core/api'
import InboundHistoryPanel from '../core/InboundHistoryPanel.vue'
import InboundSummaryPanel from '../core/InboundSummaryPanel.vue'
import type { InboundOrderDetail } from '../core/types'
import { InboundMeasurementDialog } from '../measurement'
import { MeasurementReviewPanel } from '../review'
import { InboundFeePanel } from '../fee'
import { inboundProcessStatusLabels, inboundStatusLabel } from '../status'
const props = defineProps<{
customerId: string
inboundId: string
}>()
const emit = defineEmits<{
changed: []
}>()
const access = useAccessPreviewStore()
const activeTab = ref('overview')
const measurementDialogOpen = ref(false)
const reviewDialogOpen = ref(false)
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
const title = computed(() => detail.value?.summary.inboundCode || '入仓码待补充')
const canViewMeasurement = computed(() => access.can('platform:firstleg:inbound:measure:view'))
const canViewDropshipPallets = computed(() => access.can('platform:firstleg:pda:view'))
const statusLabel = computed(() => inboundStatusLabel(detail.value?.summary.processStatus, inboundProcessStatusLabels, '未知状态'))
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 (response.summary.fulfillmentType === 'ONE_CLICK_DROPSHIP' && canViewDropshipPallets.value) {
response.dropshipPallets = await listPlatformInboundDropshipPallets(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')
}
function handleReviewChanged() {
reviewDialogOpen.value = false
handleDomainChanged()
}
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">
<h2>入库码:{{ title }}</h2>
<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">
<InboundSummaryPanel :summary="detail.summary" :public-remark="detail.publicRemark" />
</el-tab-pane>
<el-tab-pane label="货品与测量" name="measurement" lazy>
<div class="workflow-action-row">
<span>核对原单数据、确认扫描箱数,并查看服务端计算的重量和体积结果。</span>
<el-button v-if="canViewMeasurement" type="primary" @click="measurementDialogOpen = true"><Pencil :size="16" />编辑测量数据</el-button>
</div>
<el-alert
:title="canViewMeasurement ? '称重图片、OCR、托盘与箱规统一在测量弹框中维护。' : '当前账号缺少测量查看权限。'"
:type="canViewMeasurement ? 'info' : 'warning'"
:closable="false"
show-icon
/>
</el-tab-pane>
<el-tab-pane label="测量审核" name="review" lazy>
<div v-if="canViewMeasurement" class="workflow-action-row">
<span>对照原单、设备实测和称重 OCR 结果后作出审核决定。</span>
<el-button type="primary" @click="reviewDialogOpen = true"><ClipboardCheck :size="16" />审核测量数据</el-button>
</div>
<el-alert
v-else
title="无测量审核查看权限"
description="当前账号缺少测量查看权限,无法读取审核所需的原单、箱规和称重数据。"
type="warning"
:closable="false"
show-icon
/>
</el-tab-pane>
<el-tab-pane label="费用信息" name="fee" lazy>
<InboundFeePanel
:key="`fee-${revision}`"
:tenant-id="customerId"
:inbound-id="inboundId"
@changed="handleDomainChanged"
/>
</el-tab-pane>
<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="detail.summary.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '清点组托与其他记录' : '其他记录'" name="records" lazy>
<InboundHistoryPanel
:pallet-locations="detail.palletLocations"
:dropship-pallets="detail.dropshipPallets"
:customer-id="customerId"
:inbound-id="inboundId"
:can-view-dropship="canViewDropshipPallets"
:files="detail.files"
:internal-notes="detail.internalNotes"
:status-history="[]"
:public-remark="detail.publicRemark"
:sections="['dropship', 'allocations', 'files', 'notes', 'remark']"
/>
</el-tab-pane>
</el-tabs>
<InboundMeasurementDialog
v-model="measurementDialogOpen"
:customer-id="customerId"
:customer-name="detail?.summary.customerName || detail?.summary.companyName"
:inbound-id="inboundId"
:inbound-code="title"
@updated="handleDomainChanged"
@submitted="handleDomainChanged"
/>
<el-dialog
v-model="reviewDialogOpen"
class="inbound-operation-dialog inbound-review-dialog"
:title="`测量审核 · ${title}`"
width="min(1240px, calc(100vw - 32px))"
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"
:fulfillment-type="detail?.summary.fulfillmentType"
:customer-name="detail?.summary.customerName || detail?.summary.companyName"
:channel-name="detail?.summary.channelName"
:channel-code="detail?.summary.channelCode"
@changed="handleReviewChanged"
/>
</div>
</el-dialog>
</section>
</template>
<style scoped>
.inbound-workspace { min-width: 0; }
.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); }
.workspace-title-row { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 10px; }
.workspace-title-row h2 { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: 16px; line-height: 1.35; }
.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; }
.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; }
.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; }
.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; }
@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; }
.workflow-action-row { align-items: stretch; flex-direction: column; }
.workflow-action-row :deep(.el-button) { width: 100%; min-height: 44px; }
: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; }
}
</style>