This commit is contained in:
2026-08-06 13:56:04 +08:00
parent 6736893128
commit a813426588
35 changed files with 2822 additions and 489 deletions
@@ -17,12 +17,12 @@ describe('InboundWorkflowWorkspace', () => {
expect(source).toContain('label="基本信息"')
expect(source).toContain('label="货物与测量"')
expect(source).toContain('label="测量审核"')
expect(source).toContain('label="费用快照"')
expect(source).toContain('label="托盘与记录"')
expect(source).toContain('label="费用信息"')
expect(source).toContain('label="状态与其他"')
})
it('uses the established first-leg detail density without nested framed surfaces', () => {
expect(source).toContain('padding: 20px 24px')
expect(source).toContain('padding: 18px 24px 16px')
expect(source).toContain('border-bottom: 1px solid var(--cc-border)')
expect(source).not.toContain('border-radius: 10px')
expect(source).not.toContain('<el-tag')
@@ -54,7 +54,7 @@ describe('InboundWorkflowWorkspace', () => {
it('keeps pallet and location results read-only in the overview', () => {
expect(source).toContain(':pallet-locations="detail.palletLocations"')
expect(source).toContain('label="托盘与记录"')
expect(source).toContain('label="状态与其他"')
expect(source).not.toContain('reserveLocation')
expect(source).not.toContain('allocatePallet')
})
@@ -106,7 +106,7 @@ onBeforeUnmount(() => {
<header class="workspace-header">
<div>
<div class="workspace-title-row">
<h2>入仓码:{{ title }}</h2>
<h2>入库码:{{ title }}</h2>
<StatusBadge v-if="detail" :status="statusLabel" />
</div>
<p>按基本信息、货物测量、审核、费用和托盘库位顺序处理,所有结果以服务端最新状态为准。</p>
@@ -134,7 +134,7 @@ onBeforeUnmount(() => {
<el-tabs v-else v-model="activeTab" class="workflow-tabs">
<el-tab-pane label="基本信息" name="overview">
<InboundSummaryPanel :summary="detail.summary" />
<InboundSummaryPanel :summary="detail.summary" :public-remark="detail.publicRemark" />
</el-tab-pane>
<el-tab-pane label="货物与测量" name="measurement" lazy>
<div class="measurement-workflow">
@@ -169,7 +169,7 @@ onBeforeUnmount(() => {
show-icon
/>
</el-tab-pane>
<el-tab-pane label="费用快照" name="fee" lazy>
<el-tab-pane label="费用信息" name="fee" lazy>
<InboundFeePanel
:key="`fee-${revision}`"
:tenant-id="customerId"
@@ -177,13 +177,13 @@ onBeforeUnmount(() => {
@changed="handleDomainChanged"
/>
</el-tab-pane>
<el-tab-pane label="托盘与记录" name="records" lazy>
<el-tab-pane label="状态与其他" name="records" lazy>
<InboundHistoryPanel
:pallet-locations="detail.palletLocations"
:files="detail.files"
:internal-notes="detail.internalNotes"
:status-history="detail.statusHistory"
:public-remark="detail.publicRemark"
:public-remark="null"
/>
</el-tab-pane>
</el-tabs>
@@ -192,15 +192,18 @@ onBeforeUnmount(() => {
<style scoped>
.inbound-workspace { min-width: 0; }
.workspace-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 20px 24px; border-bottom: 1px solid var(--cc-border); }
.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: var(--cc-text); font-size: 18px; line-height: 1.35; }
.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: 16px; }
.workflow-tabs :deep(.el-tabs__item) { min-height: 48px; }
.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; }
.measurement-workflow { display: grid; min-width: 0; gap: 28px; }
@media (max-width: 720px) {
.workspace-header { align-items: stretch; flex-direction: column; padding: 16px; }
@@ -51,26 +51,27 @@ function date(value: string | null) {
</template>
<style scoped>
.history-panel { display: grid; gap: 22px; min-width: 0; }
.history-section { min-width: 0; }
.section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.section-heading h3 { margin: 0; color: var(--cc-text); font-size: 14px; }
.history-panel { display: grid; gap: 24px; min-width: 0; }
.history-section { min-width: 0; padding-top: 16px; border-top: 1px solid var(--cc-border); }
.history-section:first-child { padding-top: 0; border-top: 0; }
.section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.section-heading h3 { margin: 0; color: #172033; font-size: 13px; }
.section-heading span { color: var(--cc-muted); font-size: 12px; }
.allocation-list, .simple-list { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; }
.allocation-row { display: grid; grid-template-columns: 1fr auto minmax(100px, 0.8fr); gap: 12px; align-items: center; padding: 11px 12px; border-bottom: 1px solid var(--cc-border); font-size: 12px; }
.allocation-list, .simple-list { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; }
.allocation-row { display: grid; grid-template-columns: 1fr auto minmax(100px, 0.8fr); gap: 12px; align-items: center; padding: 10px 12px; border-bottom: 1px solid #edf0f4; font-size: 12px; }
.allocation-row:last-child, .simple-list > div:last-child { border-bottom: 0; }
.allocation-row > div, .simple-list > div { min-width: 0; }
.allocation-row span, .simple-list span { display: block; margin-top: 3px; color: var(--cc-muted); }
.allocation-location { overflow-wrap: anywhere; color: var(--cc-primary); text-align: right; }
.simple-list > div { padding: 11px 12px; border-bottom: 1px solid var(--cc-border); font-size: 12px; }
.simple-list > div { padding: 10px 12px; border-bottom: 1px solid #edf0f4; font-size: 12px; }
.simple-list strong, .simple-list span { overflow-wrap: anywhere; }
.muted-empty { margin: 0; padding: 18px 12px; border: 1px dashed var(--cc-border); border-radius: 8px; color: var(--cc-muted); font-size: 12px; text-align: center; }
.muted-empty { margin: 0; padding: 18px 12px; border: 1px dashed var(--cc-border); border-radius: 6px; background: #f8fafc; color: var(--cc-muted); font-size: 12px; text-align: center; }
.timeline { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
.timeline li { display: grid; grid-template-columns: 16px 1fr; gap: 10px; min-width: 0; padding-bottom: 16px; }
.timeline-dot { width: 8px; height: 8px; margin-top: 5px; border: 2px solid var(--cc-primary); border-radius: 50%; background: #fff; box-shadow: 0 0 0 3px color-mix(in srgb, var(--cc-primary) 12%, transparent); }
.timeline li > div { min-width: 0; padding-bottom: 12px; border-bottom: 1px solid var(--cc-border); }
.timeline li > div { min-width: 0; padding-bottom: 12px; border-bottom: 1px solid #edf0f4; }
.timeline strong, .timeline span, .timeline small { display: block; overflow-wrap: anywhere; }
.timeline span, .timeline small { margin-top: 3px; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
.remark-section p { margin: 0; padding: 12px; border-radius: 8px; background: #f8fafc; color: var(--cc-text); font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
.remark-section p { margin: 0; padding: 12px; border-radius: 6px; background: #f8fafc; color: var(--cc-text); font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
@media (max-width: 480px) { .allocation-row { grid-template-columns: 1fr auto; } .allocation-location { grid-column: 1 / -1; text-align: left; } }
</style>
@@ -14,6 +14,9 @@ describe('InboundSummaryPanel', () => {
expect(source).toContain('summary.chargeableWeightKg')
expect(source).toContain('summary.locationCodes')
expect(source).toContain('summary.notificationStatus')
expect(source).toContain('publicRemark')
expect(source).toContain('买单 / 退税')
expect(source).toContain('计费毛重')
})
it('does not restore excluded receiving review or customer fee confirmation actions', () => {
@@ -2,7 +2,7 @@
import { Box, MapPin, Scale, Warehouse } from '@lucide/vue'
import type { InboundOrderSummary } from './types'
defineProps<{ summary: InboundOrderSummary }>()
withDefaults(defineProps<{ summary: InboundOrderSummary; publicRemark?: string | null }>(), { publicRemark: null })
const processLabels: Record<string, string> = {
WAIT_MEASURE: '待测量', MEASURING: '测量中', MEASURE_PENDING: '待审核', MEASURE_REJECTED: '测量驳回',
@@ -33,8 +33,9 @@ function label(value: string | null, labels: Record<string, string>) { return va
<template>
<section class="summary-panel" aria-label="入库单概览">
<div class="summary-heading">
<div>
<h3>{{ summary.inboundCode || `入库单 ${summary.id}` }}</h3>
<span class="summary-icon" aria-hidden="true"><Box :size="20" /></span>
<div class="summary-title">
<h3>入库码:{{ summary.inboundCode || `入库单 ${summary.id}` }}</h3>
<p class="summary-subtitle">订单 {{ value(summary.orderNo) }} · {{ value(summary.customerName) }}</p>
</div>
<el-tag :type="processType(summary.processStatus)" effect="light">{{ processLabel(summary.processStatus) }}</el-tag>
@@ -50,9 +51,10 @@ function label(value: string | null, labels: Record<string, string>) { return va
<div><dt>公司</dt><dd>{{ value(summary.companyName) }}</dd></div>
<div><dt>业务类型</dt><dd>{{ value(summary.businessType) }}</dd></div>
<div><dt>渠道</dt><dd>{{ value(summary.channelName || summary.channelCode) }}</dd></div>
<div><dt>计税模式</dt><dd>{{ value(summary.taxMode) }}</dd></div>
<div><dt>装载模式</dt><dd>{{ value(summary.containerMode) }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ value(summary.taxMode) }}</dd></div>
<div><dt>是否整柜</dt><dd>{{ value(summary.containerMode) }}</dd></div>
<div><dt>商检验</dt><dd>{{ yesNo(summary.customsInspection) }}</dd></div>
<div><dt>备注</dt><dd>{{ value(publicRemark) }}</dd></div>
<div><dt>创建时间</dt><dd>{{ date(summary.createdAt) }}</dd></div>
</dl>
</section>
@@ -79,11 +81,11 @@ function label(value: string | null, labels: Record<string, string>) { return va
<h4 id="inbound-measure-heading">测量与计费</h4>
<dl class="summary-grid">
<div><dt><Box :size="14" />总箱数</dt><dd>{{ value(summary.totalPackageCount) }} 箱</dd></div>
<div><dt><Scale :size="14" />总毛重</dt><dd>{{ number(summary.actualGrossWeightKg) }} kg</dd></div>
<div><dt>总净重</dt><dd>{{ number(summary.actualNetWeightKg) }} kg</dd></div>
<div><dt><Scale :size="14" />总毛重 (T.K.GW)</dt><dd>{{ number(summary.actualGrossWeightKg) }} kg</dd></div>
<div><dt>总净重 (T.K.NW)</dt><dd>{{ number(summary.actualNetWeightKg) }} kg</dd></div>
<div><dt>总体积</dt><dd>{{ number(summary.actualVolumeCbm) }} CBM</dd></div>
<div><dt>总材积重</dt><dd>{{ number(summary.volumeWeightKg) }} kg</dd></div>
<div><dt>计费重量</dt><dd>{{ number(summary.chargeableWeightKg) }} kg</dd></div>
<div><dt>计费毛重</dt><dd>{{ number(summary.chargeableWeightKg) }} kg</dd></div>
<div><dt>计费体积</dt><dd>{{ number(summary.chargeableVolumeCbm) }} CBM</dd></div>
<div><dt>测量审核</dt><dd>{{ label(summary.measureReviewStatus, measureReviewLabels) }}</dd></div>
<div><dt>费用状态</dt><dd>{{ label(summary.feeStatus, feeLabels) }}</dd></div>
@@ -108,17 +110,21 @@ function label(value: string | null, labels: Record<string, string>) { return va
<style scoped>
.summary-panel { min-width: 0; }
.summary-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 18px; border-bottom: 1px solid var(--cc-border); }
h3 { margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 18px; line-height: 1.3; }
.summary-heading { display: grid; grid-template-columns: 36px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding-bottom: 18px; border-bottom: 1px solid var(--cc-border); }
.summary-icon { display: grid; width: 36px; height: 36px; place-items: center; border-radius: 8px; background: #172033; color: #fff; }
.summary-title { min-width: 0; }
h3 { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: 16px; line-height: 1.35; }
.summary-subtitle { margin: 6px 0 0; color: var(--cc-muted); font-size: 12px; }
.summary-sections { display: grid; min-width: 0; gap: 22px; margin-top: 18px; }
.summary-sections section { min-width: 0; }
h4 { margin: 0 0 9px; color: var(--cc-text); font-size: 14px; line-height: 1.4; }
.summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
.summary-grid > div { min-width: 0; min-height: 66px; padding: 11px 12px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
dt { display: flex; align-items: center; gap: 6px; color: var(--cc-muted); font-size: 12px; }
dd { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 13px; line-height: 1.5; }
.hold-note { margin: 16px 0 0; padding: 10px 12px; border: 1px solid #f5c26b; border-radius: 8px; background: #fffaeb; color: #93370d; font-size: 12px; line-height: 1.5; }
.summary-sections { display: grid; min-width: 0; gap: 24px; margin-top: 18px; }
.summary-sections section { min-width: 0; padding-top: 16px; border-top: 1px solid var(--cc-border); }
.summary-sections section:first-child { padding-top: 0; border-top: 0; }
h4 { margin: 0 0 12px; color: #172033; font-size: 13px; line-height: 1.4; }
.summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 24px; margin: 0; color: #475467; }
.summary-grid > div { display: grid; min-width: 0; grid-template-columns: max-content minmax(0, 1fr); align-items: baseline; gap: 4px; min-height: 26px; line-height: 1.7; }
dt { display: flex; align-items: center; gap: 6px; color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
dt::after { content: ":"; }
dd { margin: 0; overflow-wrap: anywhere; color: #475467; font-size: 13px; line-height: 1.7; }
.hold-note { margin: 16px 0 0; padding: 10px 12px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fffaeb; color: #b54708; font-size: 12px; line-height: 1.5; }
@media (max-width: 900px) { .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 480px) { .summary-heading { align-items: stretch; flex-direction: column; } .summary-grid { grid-template-columns: 1fr; } }
@media (max-width: 480px) { .summary-heading { grid-template-columns: 36px minmax(0, 1fr); align-items: start; } .summary-heading :deep(.el-tag) { grid-column: 2; justify-self: start; } .summary-grid { grid-template-columns: 1fr; } .summary-grid > div { grid-template-columns: 1fr; gap: 1px; } dt::after { content: ""; } }
</style>
@@ -14,8 +14,8 @@ describe('InboundOrderDetailView', () => {
expect(source).toContain('缺少所属企业信息,请从入库单列表重新打开')
expect(source).not.toContain('PlatformTenantSelector')
expect(source).not.toContain('租户')
expect(source).toContain('<h1>入库详情</h1>')
expect(source).toContain('货物测量、审核、费用及托盘库位记录')
expect(source).toContain('<h1>入库单详情</h1>')
expect(source).toContain('货品与测量、费用信息、状态历史及其他记录')
expect(source).toContain('class="surface-panel detail-page"')
expect(source).toContain('class="detail-page-actions"')
expect(source).toContain('返回入库单列表')
@@ -25,8 +25,8 @@ function backToList() {
<section class="surface-panel detail-page">
<header class="detail-page-header">
<div class="detail-page-heading">
<h1>入库详情</h1>
<p>查看基本信息、货物测量、审核、费用及托盘库位记录。</p>
<h1>入库单详情</h1>
<p>查看基本信息、货品与测量、费用信息、状态历史及其他记录。</p>
</div>
<div class="detail-page-actions">
<el-button @click="backToList"><ArrowLeft :size="16" />返回入库单列表</el-button>
@@ -101,7 +101,7 @@ describe('InboundOrdersView', () => {
await selectCustomer(wrapper)
expect(listPlatformInbounds).toHaveBeenCalledWith('10001', expect.objectContaining({ page: 1, size: 20 }))
expect(listPlatformWarehouses).toHaveBeenCalledWith('10001', 1, 200)
expect(listPlatformWarehouses).toHaveBeenCalledWith(1, 200)
expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200 })
expect(document.body.textContent).toContain('RK-001')
expect(document.body.textContent).toContain('查看')
@@ -185,8 +185,10 @@ describe('InboundOrdersView', () => {
expect(tabButtons).toHaveLength(5)
const toolbarRows = wrapper.find('.table-toolbar').element.children
expect(toolbarRows[0].classList.contains('table-tabs-row')).toBe(true)
expect(toolbarRows[1].classList.contains('table-action-row')).toBe(true)
expect(wrapper.find('.table-action-row').text()).toContain('共 1 条 · 所属企业 全部客户')
expect(toolbarRows[1].classList.contains('inbound-stats-row')).toBe(true)
expect(toolbarRows[2].classList.contains('table-action-row')).toBe(true)
expect(wrapper.find('.inbound-stats-row').text()).toContain('当前显示 1 条 · 共 1 条')
expect(wrapper.find('.table-action-row').text()).toContain('所属企业 全部客户')
expect(wrapper.find('.table-action-buttons').text()).toContain('刷新')
expect(wrapper.text()).not.toContain('全部入库单')
expect(tabButtons[0].attributes('aria-selected')).toBe('true')
@@ -214,7 +216,7 @@ describe('InboundOrdersView', () => {
toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), customerId: '10002' })
await flushPromises()
expect(listPlatformInbounds).toHaveBeenLastCalledWith('10002', expect.objectContaining({ page: 1 }))
expect(listPlatformWarehouses).toHaveBeenLastCalledWith('10002', 1, 200)
expect(listPlatformWarehouses).toHaveBeenLastCalledWith(1, 200)
})
it('resets to all customers without sending an empty backend tenant ID', async () => {
@@ -116,7 +116,7 @@ const columns: DataTableColumn[] = [
{ key: 'orderStatus', label: '订单 / 流程状态', minWidth: 156, slotName: 'orderStatus' },
{ key: 'measureReviewStatus', label: '测量数据审核状态', minWidth: 142, slotName: 'measureReviewStatus' },
{ key: 'expectedArrivalAt', label: '预计到达时间', minWidth: 164, slotName: 'expectedArrivalAt' },
{ key: 'feeStatus', label: '费用状态', minWidth: 124, slotName: 'feeStatus' },
{ key: 'feeStatus', label: '费用审核状态', minWidth: 132, slotName: 'feeStatus' },
{ key: 'customerName', label: '客户', minWidth: 142, slotName: 'customerName' },
{ key: 'warehouseName', label: '仓库', minWidth: 140, slotName: 'warehouseName' },
{ key: 'deliveryWarehouseName', label: '交货仓', minWidth: 140, slotName: 'deliveryWarehouseName' },
@@ -128,20 +128,20 @@ const columns: DataTableColumn[] = [
{ key: 'companyName', label: '公司', minWidth: 160, slotName: 'companyName' },
{ key: 'businessType', label: '业务类型', minWidth: 112, slotName: 'businessType' },
{ key: 'channelName', label: '渠道', minWidth: 138, slotName: 'channelName' },
{ key: 'taxMode', label: '计税模式', minWidth: 108, slotName: 'taxMode' },
{ key: 'containerMode', label: '装载模式', minWidth: 108, slotName: 'containerMode' },
{ key: 'taxMode', label: '买单/退税', minWidth: 108, slotName: 'taxMode' },
{ key: 'containerMode', label: '是否整柜', minWidth: 108, slotName: 'containerMode' },
{ key: 'customsInspection', label: '商检验', width: 92, slotName: 'customsInspection' },
{ key: 'sailingAt', label: '开船时间', minWidth: 164, slotName: 'sailingAt' },
{ key: 'portArrivalAt', label: '到港时间', minWidth: 164, slotName: 'portArrivalAt' },
{ key: 'totalPackageCount', label: '总箱数', width: 90, slotName: 'totalPackageCount' },
{ key: 'actualGrossWeightKg', label: '总毛重(kg)', minWidth: 118, slotName: 'actualGrossWeightKg' },
{ key: 'chargeableWeightKg', label: '计费重量(kg)', minWidth: 126, slotName: 'chargeableWeightKg' },
{ key: 'actualVolumeCbm', label: '总体积(CBM)', minWidth: 122, slotName: 'actualVolumeCbm' },
{ key: 'chargeableVolumeCbm', label: '计费体积(CBM)', minWidth: 132, slotName: 'chargeableVolumeCbm' },
{ key: 'actualGrossWeightKg', label: '总毛重(T.K.GW)', minWidth: 136, slotName: 'actualGrossWeightKg' },
{ key: 'chargeableWeightKg', label: '计费毛重', minWidth: 112, slotName: 'chargeableWeightKg' },
{ key: 'actualVolumeCbm', label: '总体积(m³)', minWidth: 118, slotName: 'actualVolumeCbm' },
{ key: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 128, slotName: 'chargeableVolumeCbm' },
{ key: 'palletCount', label: '总托盘数', width: 100, slotName: 'palletCount' },
{ key: 'locationCodes', label: '库位位置', minWidth: 124, slotName: 'locationCodes' },
{ key: 'volumeWeightKg', label: '总材积重(kg)', minWidth: 126, slotName: 'volumeWeightKg' },
{ key: 'actualNetWeightKg', label: '总净重(kg)', minWidth: 118, slotName: 'actualNetWeightKg' },
{ key: 'volumeWeightKg', label: '总材积重', minWidth: 110, slotName: 'volumeWeightKg' },
{ key: 'actualNetWeightKg', label: '总净重(T.K.NW)', minWidth: 136, slotName: 'actualNetWeightKg' },
{ key: 'destinationType', label: '目的仓类型', minWidth: 112, slotName: 'destinationType' },
{ key: 'destinationName', label: '目的仓', minWidth: 132, slotName: 'destinationName' },
{ key: 'measureCompletedAt', label: '测量完成时间', minWidth: 164, slotName: 'measureCompletedAt' },
@@ -156,6 +156,16 @@ const selectedCustomerName = computed(() => {
return customer?.legalName?.trim() || customer?.tenantName?.trim() || '全部客户'
})
const listEmptyText = '暂无符合条件的入库单'
const currentPageCount = computed(() => result.value.records.length)
const aggregateStats = computed(() => {
const records = result.value.records
return {
packages: records.reduce((total, item) => total + (item.totalPackageCount || 0), 0),
grossWeight: records.reduce((total, item) => total + (item.actualGrossWeightKg || 0), 0),
chargeableWeight: records.reduce((total, item) => total + (item.chargeableWeightKg || 0), 0),
volume: records.reduce((total, item) => total + (item.actualVolumeCbm || 0), 0),
}
})
function queryValue(value: string) { return value.trim() || undefined }
function handleTabKeydown(event: KeyboardEvent, currentIndex: number) {
@@ -186,7 +196,7 @@ async function loadWarehouseOptions() {
}
warehousesLoading.value = true
try {
const response = await listPlatformWarehouses(selectedCustomerId.value, 1, 200)
const response = await listPlatformWarehouses(1, 200)
if (disposed || requestId !== warehouseRequestId) return
warehouseOptions.value = response.records.map((warehouse) => ({
label: `${warehouse.warehouseName} · ${warehouse.warehouseCode}`,
@@ -297,6 +307,10 @@ function openWorkspace(row: InboundOrderSummary) {
void router.push({ name: 'inbound-order-detail', params: { inboundId: row.id }, query: { tenantId: row.tenantId } })
}
function formatNumber(value: number | null, digits = 3) { return value === null ? '—' : value.toFixed(digits) }
function formatAggregate(value: number, digits = 3) {
if (!Number.isFinite(value)) return '0'
return value.toLocaleString('zh-CN', { maximumFractionDigits: digits })
}
function formatDate(value: string | null) {
if (!value) return '—'
const date = new Date(value)
@@ -390,8 +404,20 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
<button v-for="(item, index) in tabs" :key="item.value || 'all'" type="button" role="tab" class="inbound-tab" :class="{ 'is-active': tab === item.value }" :aria-selected="tab === item.value" :tabindex="tab === item.value ? 0 : -1" :title="item.description" @click="tab = item.value" @keydown="handleTabKeydown($event, index)">{{ item.label }}</button>
</nav>
</div>
<div class="inbound-stats-row" aria-label="入库单统计">
<span class="selected-info">当前显示 {{ currentPageCount }} 条 · 共 {{ result.total }} 条</span>
<span class="total-info">
<b>总箱数 {{ formatAggregate(aggregateStats.packages, 0) }}</b>
<i aria-hidden="true" />
<b>总毛重 {{ formatAggregate(aggregateStats.grossWeight) }} kg</b>
<i aria-hidden="true" />
<b>计费重量 {{ formatAggregate(aggregateStats.chargeableWeight) }} kg</b>
<i aria-hidden="true" />
<b>总体积 {{ formatAggregate(aggregateStats.volume) }} m³</b>
</span>
</div>
<div class="table-action-row">
<span class="result-summary">共 {{ result.total }} 条 · 所属企业 {{ selectedCustomerName }}</span>
<span class="result-summary">所属企业 {{ selectedCustomerName }}</span>
<div class="table-action-buttons">
<el-button :loading="loading" @click="loadInbounds"><RefreshCw :size="16" />刷新</el-button>
</div>
@@ -442,37 +468,50 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
<AppDrawer v-model="detailOpen" :title="detail ? `入库单详情 · ${detail.summary.inboundCode || detail.summary.id}` : '入库单详情'" size="min(720px, 94vw)" :loading="detailLoading" :show-footer="false" destroy-on-close @closed="closeDetail">
<section v-if="detailError" class="detail-error" role="alert"><strong>入库单详情暂时无法加载</strong><p>{{ detailError.message }}</p><ApiErrorMeta :details="detailError" /><el-button type="primary" :disabled="!detailTarget" @click="detailTarget && openDetail(detailTarget)">重新加载</el-button></section>
<div v-else-if="detail" class="detail-content"><InboundSummaryPanel :summary="detail.summary" /><InboundHistoryPanel :pallet-locations="detail.palletLocations" :files="detail.files" :internal-notes="detail.internalNotes" :status-history="detail.statusHistory" :public-remark="detail.publicRemark" /></div>
<div v-else-if="detail" class="detail-content"><InboundSummaryPanel :summary="detail.summary" :public-remark="detail.publicRemark" /><InboundHistoryPanel :pallet-locations="detail.palletLocations" :files="detail.files" :internal-notes="detail.internalNotes" :status-history="detail.statusHistory" :public-remark="null" /></div>
</AppDrawer>
</div>
</template>
<style scoped>
.inbound-orders-page { min-width: 0; }
.inbound-tabs { display: flex; width: max-content; max-width: 100%; min-width: 0; overflow-x: auto; padding: 4px; border: 1px solid var(--cc-border); border-radius: 8px; background: #f7f9fc; scrollbar-width: none; }
.inbound-query-toolbar { align-items: stretch; border-radius: 8px; background: #f8fafc; }
.inbound-query-toolbar :deep(.query-toolbar__filters) { display: grid; flex: 1 1 auto; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px 16px; }
.inbound-query-toolbar :deep(.query-toolbar__filters .el-input),
.inbound-query-toolbar :deep(.query-toolbar__filters .el-select),
.inbound-query-toolbar :deep(.query-toolbar__filters .el-date-editor) { width: 100%; }
.inbound-query-toolbar :deep(.query-toolbar__actions) { align-content: flex-end; align-items: flex-end; }
.inbound-tabs { display: flex; width: max-content; max-width: 100%; min-width: 0; overflow-x: auto; border-bottom: 2px solid var(--cc-border); scrollbar-width: none; }
.inbound-tabs::-webkit-scrollbar { display: none; }
.inbound-tab { min-height: 44px; flex: 0 0 auto; padding: 8px 18px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--cc-muted); cursor: pointer; font-size: 13px; line-height: 1.4; white-space: nowrap; transition: background-color .16s ease, border-color .16s ease, color .16s ease, box-shadow .16s ease; }
.inbound-tab:hover { background: #fff; color: var(--cc-primary); }
.inbound-tab { min-height: 42px; flex: 0 0 auto; padding: 10px 22px; border: 0; border-bottom: 2px solid transparent; margin-bottom: -2px; background: transparent; color: #475467; cursor: pointer; font-size: 13px; line-height: 1.4; white-space: nowrap; transition: border-color .16s ease, color .16s ease, background-color .16s ease; }
.inbound-tab:hover { color: var(--cc-primary); background: #f8fbff; }
.inbound-tab:focus-visible { outline: 3px solid rgba(102, 126, 234, .2); outline-offset: 1px; }
.inbound-tab.is-active { border-color: #d8e0f0; background: #fff; color: var(--cc-primary); box-shadow: 0 1px 3px rgba(16, 24, 40, .08); font-weight: 650; }
.inbound-tab.is-active { border-bottom-color: var(--cc-primary); background: #fff; color: var(--cc-primary); font-weight: 650; }
.inbound-load-error { margin-top: 12px; }
.inbound-query-toolbar { border-radius: 8px; }
.row-actions { display: flex; align-items: center; gap: 2px; white-space: nowrap; }
.row-actions :deep(.el-button) { margin-left: 0; }
.table-toolbar { display: grid; min-width: 0; border-bottom: 1px solid var(--cc-border); }
.table-tabs-row { min-width: 0; padding: 10px 14px; }
.table-action-row { display: flex; min-width: 0; min-height: 56px; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 14px; border-top: 1px solid var(--cc-border); background: #fbfcfe; }
.table-tabs-row { min-width: 0; padding: 0 14px; background: #fff; }
.inbound-stats-row { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 14px; background: var(--cc-primary); color: #fff; }
.inbound-stats-row .selected-info, .inbound-stats-row .total-info { min-width: 0; font-size: 12px; font-weight: 650; line-height: 1.5; }
.inbound-stats-row .total-info { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.inbound-stats-row b { font-weight: 650; }
.inbound-stats-row i { width: 1px; height: 12px; background: rgba(255, 255, 255, .38); }
.table-action-row { display: flex; min-width: 0; min-height: 48px; align-items: center; justify-content: space-between; gap: 16px; padding: 7px 14px; border-top: 1px solid #dfe6f0; background: #fbfcfe; }
.table-action-buttons { display: flex; min-width: 0; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.result-summary { color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
.table-action-buttons :deep(.el-button) { min-height: 40px; margin-left: 0; }
.table-action-buttons :deep(.el-button) { min-height: 34px; margin-left: 0; }
.table-panel :deep(.el-table th.el-table__cell) { border-right: 1px solid #edf0f4; }
.table-panel :deep(.el-table td.el-table__cell) { border-right: 1px solid #edf0f4; }
.primary-cell, .status-stack { display: grid; min-width: 0; justify-items: start; gap: 4px; }
.primary-cell strong, .primary-cell span, .status-stack span, .status-stack small { overflow-wrap: anywhere; }
.primary-cell span, .status-stack span, .status-stack small { color: var(--cc-muted); font-size: 12px; }
.detail-content { display: grid; gap: 24px; min-width: 0; }
.detail-error { display: flex; min-height: 240px; align-items: flex-start; justify-content: center; flex-direction: column; }
.detail-error strong { color: var(--cc-danger); font-size: 15px; }
.detail-error p { margin: 7px 0; color: var(--cc-text); font-size: 13px; }
.detail-error strong { color: #b42318; font-size: 16px; }
.detail-error p { margin: 7px 0; color: #172033; font-size: 13px; }
.detail-error :deep(.el-button) { margin-top: 14px; }
@media (max-width: 900px) { .inbound-tabs { width: 100%; } }
@media (max-width: 720px) { .inbound-tab { padding-inline: 14px; } .table-tabs-row { padding: 10px; } .table-action-row { min-height: 0; align-items: stretch; flex-direction: column; gap: 10px; padding: 10px; } .table-action-buttons { justify-content: flex-start; } .result-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; } .table-action-buttons :deep(.el-button) { min-height: 44px; } }
@media (max-width: 1180px) { .inbound-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 900px) { .inbound-tabs { width: 100%; } .inbound-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 720px) { .inbound-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: 1fr; } .inbound-tab { padding-inline: 14px; } .table-tabs-row { padding: 0 10px; } .inbound-stats-row, .table-action-row { min-height: 0; align-items: stretch; flex-direction: column; gap: 10px; padding: 10px; } .inbound-stats-row .total-info { justify-content: flex-start; } .table-action-buttons { justify-content: flex-start; } .result-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; } .table-action-buttons :deep(.el-button) { min-height: 44px; } }
</style>
+3 -3
View File
@@ -4,10 +4,10 @@ export const inboundRoutes = [
{
path: 'inbounds',
name: 'inbound-orders',
component: () => import('./pages/InboundOrdersView.vue'),
redirect: '/orders',
meta: {
title: '入库单',
description: '查询头程入库单并跟进测量、审核、费用和入库进度',
title: '订单列表',
description: '旧入库列表入口已合并到统一订单列表',
permission: 'platform:firstleg:inbound:list',
},
},