This commit is contained in:
2026-08-13 20:00:38 +08:00
parent 5489bff92f
commit b9cfdcf42a
13 changed files with 183 additions and 479 deletions
@@ -1,61 +0,0 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import ElementPlus from 'element-plus'
import { flushPromises, mount } from '@vue/test-utils'
import { createMemoryHistory, createRouter } from 'vue-router'
import { describe, expect, it } from 'vitest'
import InboundOrderDetailView from './InboundOrderDetailView.vue'
const source = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/pages/InboundOrderDetailView.vue'), 'utf8')
describe('InboundOrderDetailView', () => {
it('maps the compatible route parameter to customer context without a detail-page selector', async () => {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'dashboard', component: { template: '<div>首页</div>' } },
{ path: '/inbounds', name: 'inbound-orders', component: { template: '<div>入库单列表</div>' } },
{ path: '/inbounds/:inboundId', name: 'inbound-order-detail', component: InboundOrderDetailView },
],
})
await router.push('/inbounds/19001?tenantId=10002')
await router.isReady()
const wrapper = mount(InboundOrderDetailView, {
global: {
plugins: [router, ElementPlus],
stubs: {
InboundWorkflowWorkspace: {
props: ['customerId', 'inboundId'],
template: '<div data-testid="workspace">{{ customerId }}:{{ inboundId }}</div>',
},
},
},
})
expect(wrapper.get('[aria-label="面包屑"]').text()).toContain('首页入库单列表入库单详情')
expect(wrapper.get('[data-testid="workspace"]').text()).toBe('10002:19001')
expect(wrapper.findAll('button').filter((button) => button.text().includes('返回入库单列表'))).toHaveLength(1)
await wrapper.findAll('button').filter((button) => button.text().includes('返回入库单列表')).at(-1)?.trigger('click')
await flushPromises()
expect(router.currentRoute.value.name).toBe('inbound-orders')
expect(router.currentRoute.value.query.tenantId).toBe('10002')
expect(source).toContain('route.query.tenantId')
expect(source).toContain('const customerId = computed')
expect(source).toContain("name: 'inbound-orders'")
expect(source).toContain(':customer-id="customerId"')
expect(source).toContain(':inbound-id="inboundId"')
expect(source).toContain('缺少所属企业信息,请从入库单列表重新打开')
expect(source).not.toContain('PlatformTenantSelector')
expect(source).not.toContain('租户')
expect(source).toContain('<PageToolbar title="入库单详情"')
expect(source).toContain('货品与测量、费用信息、状态历史及其他记录')
expect(source).toContain('class="detail-page"')
expect(source).toContain('class="detail-page-actions"')
expect(source).toContain('返回入库单列表')
expect(source).toContain('class="detail-breadcrumb"')
expect(source).not.toContain('class="detail-page-footer"')
expect(source).toContain('@media (max-width: 720px)')
expect(source).toContain('@media (max-width: 360px)')
})
})
@@ -1,75 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft, ChevronRight, Home } from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import PageToolbar from '@/shared/components/PageToolbar.vue'
import InboundWorkflowWorkspace from '../components/InboundWorkflowWorkspace.vue'
const route = useRoute()
const router = useRouter()
function routeValue(value: unknown) {
if (Array.isArray(value)) return String(value[0] || '')
return value === null || value === undefined ? '' : String(value)
}
const inboundId = computed(() => routeValue(route.params.inboundId))
const customerId = computed(() => routeValue(route.query.tenantId))
function backToList() {
void router.push({ name: 'inbound-orders', query: customerId.value ? { tenantId: customerId.value } : undefined })
}
function backToHome() {
void router.push({ name: 'dashboard' })
}
</script>
<template>
<AdminPage as="div" class="business-page inbound-detail-page">
<nav class="detail-breadcrumb" aria-label="面包屑">
<button type="button" @click="backToHome"><Home :size="14" />首页</button>
<ChevronRight :size="14" aria-hidden="true" />
<button type="button" @click="backToList">入库单列表</button>
<ChevronRight :size="14" aria-hidden="true" />
<span aria-current="page">入库单详情</span>
</nav>
<DetailPageLayout class="detail-page" aria-label="入库单详情">
<template #header>
<PageToolbar title="入库单详情" description="查看基本信息、货品与测量、费用信息、状态历史及其他记录。">
<div class="detail-page-actions">
<el-button @click="backToList"><ArrowLeft :size="16" />返回入库单列表</el-button>
</div>
</PageToolbar>
</template>
<InboundWorkflowWorkspace
v-if="inboundId && customerId"
:customer-id="customerId"
:inbound-id="inboundId"
/>
<el-empty
v-else
:description="inboundId ? '缺少所属企业信息,请从入库单列表重新打开' : '缺少有效的入库单标识'"
/>
</DetailPageLayout>
</AdminPage>
</template>
<style scoped>
.inbound-detail-page { min-width: 0; }
.detail-breadcrumb { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 6px; padding: 0 2px; color: var(--cc-muted); font-size: 13px; }
.detail-breadcrumb button { display: inline-flex; min-height: 32px; align-items: center; gap: 5px; padding: 4px 6px; border: 0; border-radius: 4px; background: transparent; color: var(--cc-primary); cursor: pointer; font: inherit; }
.detail-breadcrumb button:hover { background: #f0f7ff; }
.detail-breadcrumb button:focus-visible { outline: 3px solid rgba(102, 126, 234, .2); outline-offset: 1px; }
.detail-page { min-width: 0; overflow: hidden; padding: 20px 24px; }
.detail-page-actions { display: flex; min-width: 0; align-items: center; justify-content: flex-end; gap: 12px; }
@media (max-width: 720px) {
.detail-page { padding: 16px; }
.detail-page-actions { width: 100%; align-items: stretch; flex-direction: column; }
.detail-page-actions :deep(.el-button) { width: 100%; min-height: 44px; }
}
@media (max-width: 360px) { .detail-page { padding: 12px; } }
</style>
@@ -169,7 +169,7 @@ describe('InboundOrdersView', () => {
}))
})
it('passes the selected backend tab and routes view to the independent detail page', async () => {
it('passes the selected backend tab and routes view to the unified order detail page', async () => {
const wrapper = mountPage()
const toolbar = await selectCustomer(wrapper)
toolbar.vm.$emit('search')
@@ -182,8 +182,8 @@ describe('InboundOrdersView', () => {
;[...document.querySelectorAll<HTMLButtonElement>('button')].filter((button) => button.textContent?.trim() === '查看').at(-1)?.click()
await flushPromises()
expect(routerPush).toHaveBeenCalledWith({
name: 'inbound-order-detail',
params: { inboundId: '19001' },
name: 'unified-order-detail',
params: { orderId: '18001' },
query: { tenantId: '10002' },
})
expect(wrapper.find('[aria-label="入库单详情抽屉"]').exists()).toBe(false)
@@ -334,7 +334,8 @@ describe('InboundOrdersView', () => {
expect(source).toContain('preference-key="inbound-orders-table"')
expect(columnSource.match(/defaultVisible: false/g)?.length).toBeGreaterThanOrEqual(25)
expect(toolbarSource).toMatch(/@media \(max-width: 720px\)[\s\S]*width: 100%/)
expect(source).toContain('<el-button size="small" type="primary" @click="openDetail')
expect(source).toContain('name: \'unified-order-detail\'')
expect(source).toContain(':disabled="!(row as InboundOrderSummary).orderId"')
expect(source).toContain('<ClipboardCheck :size="15" />处理')
expect(source).toContain('<el-button size="small" plain type="primary"')
expect(source).not.toContain('<el-button link type="primary" @click="openDetail(row as InboundOrderSummary)">')
@@ -283,7 +283,11 @@ function applyFilters() {
}
function openDetail(row: InboundOrderSummary) {
void router.push({ name: 'inbound-order-detail', params: { inboundId: row.id }, query: { tenantId: row.tenantId } })
if (!row.orderId) {
ElMessage.warning('该入库单尚未关联订单,暂时无法查看订单详情')
return
}
void router.push({ name: 'unified-order-detail', params: { orderId: row.orderId }, query: { tenantId: row.tenantId } })
}
async function printCartonLabels(row: InboundOrderSummary) {
if (printingInboundId.value) return
@@ -411,7 +415,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; customerRequestId +
</header>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="result.records" :columns="columns" preference-key="inbound-orders-table" :loading="loading" :total="result.total" :page-sizes="[20, 50, 100]" row-key="id" :empty-text="listEmptyText" aria-label="入库单列表">
<template #pagination-left>当前显示 {{ currentPageCount }} 条 · 共 {{ result.total }} 条</template>
<template #actions="{ row }"><div v-if="access.can('platform:firstleg:inbound:view')" class="row-actions"><el-button size="small" type="primary" @click="openDetail(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button size="small" plain type="primary" :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button size="small" plain type="primary" @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div><span v-else class="muted-action">无查看权限</span></template>
<template #actions="{ row }"><div v-if="access.can('platform:firstleg:inbound:view')" class="row-actions"><el-button size="small" type="primary" :disabled="!(row as InboundOrderSummary).orderId" @click="openDetail(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button size="small" plain type="primary" :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button size="small" plain type="primary" :disabled="!(row as InboundOrderSummary).orderId" @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div><span v-else class="muted-action">无查看权限</span></template>
<template #inbound="{ row }"><div class="primary-cell"><strong>{{ text((row as InboundOrderSummary).inboundCode) }}</strong><span>订单 {{ text((row as InboundOrderSummary).orderNo) }}</span></div></template>
<template #processStatus="{ row }"><el-tag :type="statusType((row as InboundOrderSummary).processStatus)" effect="light">{{ statusLabel((row as InboundOrderSummary).processStatus) }}</el-tag></template>
<template #orderStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).orderStatusSnapshot, inboundOrderStatusLabels) }}</template>
@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import DataTable from '@/shared/components/DataTable.vue'
import { approveMeasurement, getMeasurementReviewDetail, listMeasurementReviewHistory } from './api'
import { approveMeasurement, getMeasurementReviewDetail, listMeasurementReviewHistory, rejectMeasurement } from './api'
import MeasurementReviewPanel from './MeasurementReviewPanel.vue'
vi.mock('./api', () => ({
@@ -19,7 +19,7 @@ vi.mock('./api', () => ({
const detail = {
inboundId: '90001', inboundCode: 'RK90001', companyName: '柏悦客户', channelCode: 'SEA', channelName: '海运',
processStatus: 'MEASURE_PENDING', measureReviewStatus: 'PENDING', recommendedPalletType: 'STANDARD', reviewReason: null, reviewerId: null, reviewerName: null,
measurementId: '70001', sourceType: 'DEVICE', measurementStatus: 'SUBMITTED', version: 4,
measurementId: '70001', sourceType: 'DEVICE', measurementStatus: 'SUBMITTED', inboundVersion: 7, version: 4,
originalDocumentNo: 'ORD-001', originalGrossWeightKg: 100, originalVolumeCbm: 2, originalPackageCount: 10,
actualPackageCount: 10, actualGrossWeightKg: 110, actualVolumeCbm: 2.2, volumeWeightKg: 367.4, actualGoodsName: '测试货物',
items: [{ id: '1', boxCode: 'RK90001#10-1', itemNo: '001', boxSpecification: '60×40×30', packageCount: 10, lengthCm: 60, widthCm: 40, heightCm: 30, girthCm: 240, unitWeightKg: 11, unitVolumeCbm: 0.072, totalWeightKg: 110, totalVolumeCbm: 0.72, volumeWeightKg: 120.24, intelligentImageFileId: '30001', machineNo: 'MJ20210327', measuredAt: '2026-08-04T10:30:00+08:00', overLimitWarning: '周长超过渠道限制,请人工核验尺寸后再作出审核决定', outerLengthCm: 61, outerWidthCm: 41, outerHeightCm: 31, remark: '外箱存在轻微变形' }],
@@ -92,7 +92,26 @@ describe('MeasurementReviewPanel', () => {
'确认测量审核通过',
expect.objectContaining({ confirmButtonText: '通过并完成入库' }),
)
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 4 })
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 7 })
expect(wrapper.emitted('changed')).toHaveLength(1)
})
it('rejects with the inbound optimistic-lock version instead of the measurement version', async () => {
vi.mocked(rejectMeasurement).mockResolvedValue(undefined)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const reject = wrapper.findAll('button').find((button) => button.text().includes('驳回'))
await reject?.trigger('click')
await wrapper.find('textarea').setValue('重量图片需要重新核对')
const confirm = wrapper.findAll('button').find((button) => button.text().includes('确认驳回'))
await confirm?.trigger('click')
await flushPromises()
expect(rejectMeasurement).toHaveBeenCalledWith('10001', '90001', {
measurementId: '70001',
expectedVersion: 7,
reason: '重量图片需要重新核对',
})
})
})
@@ -156,7 +156,7 @@ async function confirmApprove() {
try {
await approveMeasurement(customerId.value, props.inboundId, {
measurementId: detail.value.measurementId,
expectedVersion: detail.value.version,
expectedVersion: detail.value.inboundVersion,
})
ElMessage.success('审核已通过,费用发布和入库正在完成')
await reload()
@@ -187,14 +187,14 @@ async function submitReason() {
if (kind === 'reject') {
await rejectMeasurement(customerId.value, props.inboundId, {
measurementId: detail.value.measurementId,
expectedVersion: detail.value.version,
expectedVersion: detail.value.inboundVersion,
reason: value,
})
ElMessage.success('测量已驳回,已退回测量修正')
} else {
await releaseRejectedMeasurement(customerId.value, props.inboundId, {
measurementId: detail.value.measurementId,
expectedVersion: detail.value.version,
expectedVersion: detail.value.inboundVersion,
reason: value,
})
ElMessage.success('异常放行已提交,后续计费入库将继续处理')
+1
View File
@@ -60,6 +60,7 @@ export interface MeasurementReviewDetail {
measurementId: string
sourceType: string | null
measurementStatus: string | null
inboundVersion: number
version: number
originalDocumentNo: string | null
originalGrossWeightKg: number | null
-10
View File
@@ -31,14 +31,4 @@ export const inboundRoutes = [
permission: 'platform:firstleg:inbound:list',
},
},
{
path: 'inbounds/:inboundId',
name: 'inbound-order-detail',
component: () => import('./pages/InboundOrderDetailView.vue'),
meta: {
title: '入库作业详情',
description: '处理测量、称重、审核并查看费用和托盘库位结果',
permission: 'platform:firstleg:inbound:view',
},
},
] satisfies RouteRecordRaw[]