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
@@ -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>