优化
This commit is contained in:
@@ -1,11 +1,45 @@
|
||||
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', () => {
|
||||
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(2)
|
||||
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'")
|
||||
@@ -19,5 +53,9 @@ describe('InboundOrderDetailView', () => {
|
||||
expect(source).toContain('class="surface-panel detail-page"')
|
||||
expect(source).toContain('class="detail-page-actions"')
|
||||
expect(source).toContain('返回入库单列表')
|
||||
expect(source).toContain('class="detail-breadcrumb"')
|
||||
expect(source).toContain('class="detail-page-footer"')
|
||||
expect(source).toContain('@media (max-width: 720px)')
|
||||
expect(source).toContain('@media (max-width: 360px)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, RefreshCw } from '@lucide/vue'
|
||||
import { ArrowLeft, ChevronRight, Home } from '@lucide/vue'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||||
import FormSection from '@/shared/components/FormSection.vue'
|
||||
@@ -22,10 +22,21 @@ 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="surface-panel detail-page" aria-label="入库单详情">
|
||||
<template #header>
|
||||
<PageToolbar title="入库单详情" description="查看基本信息、货品与测量、费用信息、状态历史及其他记录。">
|
||||
@@ -46,18 +57,28 @@ function backToList() {
|
||||
:description="inboundId ? '缺少所属企业信息,请从入库单列表重新打开' : '缺少有效的入库单标识'"
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
<footer class="detail-page-footer">
|
||||
<el-button @click="backToList"><ArrowLeft :size="16" />返回入库单列表</el-button>
|
||||
</footer>
|
||||
</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 { overflow: hidden; padding: 20px 24px; }
|
||||
.detail-page-actions { display: flex; min-width: 0; align-items: center; justify-content: flex-end; gap: 12px; }
|
||||
.detail-page-footer { display: flex; justify-content: flex-start; padding: 20px 0 0; border-top: 1px solid var(--cc-border); }
|
||||
@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; }
|
||||
.detail-page-footer :deep(.el-button) { width: 100%; min-height: 44px; }
|
||||
}
|
||||
@media (max-width: 360px) { .detail-page { padding: 12px; } }
|
||||
</style>
|
||||
|
||||
@@ -10,12 +10,18 @@ import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||||
import DataTable from '@/shared/components/DataTable.vue'
|
||||
import { listPlatformWarehouses } from '@/modules/warehouses'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from '../core/api'
|
||||
import type { InboundOrderDetail, InboundOrderSummary } from '../core/types'
|
||||
import { downloadInboundCartonLabels, listPlatformInbounds } from '../core/api'
|
||||
import type { InboundOrderSummary } from '../core/types'
|
||||
|
||||
const routerPush = vi.fn()
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => ({
|
||||
...await importOriginal<typeof import('vue-router')>(),
|
||||
useRouter: () => ({ push: routerPush }),
|
||||
}))
|
||||
|
||||
vi.mock('../core/api', () => ({
|
||||
downloadInboundCartonLabels: vi.fn(),
|
||||
getPlatformInbound: vi.fn(),
|
||||
listPlatformInbounds: vi.fn(),
|
||||
}))
|
||||
|
||||
@@ -46,13 +52,6 @@ const summary: InboundOrderSummary = {
|
||||
createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-01T10:00:00',
|
||||
}
|
||||
|
||||
const detail: InboundOrderDetail = {
|
||||
summary,
|
||||
publicRemark: '请优先安排测量',
|
||||
palletLocations: [{ allocationNo: 1, warehouseId: '20001', palletId: '21001', palletCode: 'P-001', palletCapacityCbm: 2, allocatedVolumeCbm: 1.2, allocatedPackageCount: 6, locationId: '22001', locationCode: 'A-01', reservationNo: 'RES-1', allocationStatus: 'PREPARED' }],
|
||||
files: [], internalNotes: [], statusHistory: [{ fromStatus: null, toStatus: 'WAIT_MEASURE', action: 'CREATE', reason: null, operatorId: '2', operatorName: '管理员', occurredAt: '2026-08-01T10:00:00' }],
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
function mountPage() {
|
||||
@@ -76,7 +75,6 @@ describe('InboundOrdersView', () => {
|
||||
setActivePinia(createPinia())
|
||||
useAuthStore().permissions = ['platform:firstleg:inbound:view']
|
||||
vi.mocked(listPlatformInbounds).mockResolvedValue({ records: [summary], page: 1, size: 20, total: 1, pages: 1 })
|
||||
vi.mocked(getPlatformInbound).mockResolvedValue(detail)
|
||||
vi.mocked(downloadInboundCartonLabels).mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' }))
|
||||
vi.mocked(listPlatformTenants).mockResolvedValue({ records: customers, page: 1, size: 200, total: 2, pages: 1 })
|
||||
vi.mocked(listPlatformWarehouses).mockResolvedValue({
|
||||
@@ -171,7 +169,7 @@ describe('InboundOrdersView', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('passes the selected backend tab and opens an aggregate read-only detail', async () => {
|
||||
it('passes the selected backend tab and routes view to the independent detail page', async () => {
|
||||
const wrapper = mountPage()
|
||||
const toolbar = await selectCustomer(wrapper)
|
||||
toolbar.vm.$emit('search')
|
||||
@@ -183,9 +181,12 @@ describe('InboundOrdersView', () => {
|
||||
expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ tab: 'WAIT_REVIEW', page: 1 }))
|
||||
;[...document.querySelectorAll<HTMLButtonElement>('button')].filter((button) => button.textContent?.trim() === '查看').at(-1)?.click()
|
||||
await flushPromises()
|
||||
expect(getPlatformInbound).toHaveBeenCalledWith('10002', '19001')
|
||||
expect(document.body.textContent).toContain('托盘与库位')
|
||||
expect(document.body.textContent).toContain('请优先安排测量')
|
||||
expect(routerPush).toHaveBeenCalledWith({
|
||||
name: 'inbound-order-detail',
|
||||
params: { inboundId: '19001' },
|
||||
query: { tenantId: '10002' },
|
||||
})
|
||||
expect(wrapper.find('[aria-label="入库单详情抽屉"]').exists()).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
|
||||
@@ -6,21 +6,16 @@ import { ChevronDown, ChevronUp, ClipboardCheck, Eye, Printer, RefreshCw, Slider
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||||
import FormSection from '@/shared/components/FormSection.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { firstLegBusinessTypeLabel, firstLegContainerModeLabel, firstLegDestinationTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listPlatformWarehouses } from '@/modules/warehouses'
|
||||
import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from '../core/api'
|
||||
import InboundHistoryPanel from '../core/InboundHistoryPanel.vue'
|
||||
import InboundSummaryPanel from '../core/InboundSummaryPanel.vue'
|
||||
import type { InboundListTab, InboundOrderDetail, InboundOrderSummary } from '../core/types'
|
||||
import { downloadInboundCartonLabels, listPlatformInbounds } from '../core/api'
|
||||
import type { InboundListTab, InboundOrderSummary } from '../core/types'
|
||||
import { inboundFeeStatusLabels, inboundLoadingStatusLabels, inboundOrderStatusLabels, inboundProcessStatusLabels, inboundReviewStatusLabels, inboundStatusLabel } from '../status'
|
||||
|
||||
type FilterModel = QueryModel & {
|
||||
@@ -47,11 +42,6 @@ const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const loading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref<ApiErrorDetails | null>(null)
|
||||
const detail = ref<InboundOrderDetail | null>(null)
|
||||
const detailTarget = ref<InboundOrderSummary | null>(null)
|
||||
const printingInboundId = ref('')
|
||||
const result = ref({ records: [] as InboundOrderSummary[], total: 0 })
|
||||
const filters = ref<FilterModel>({ customerId: '', inboundCode: '', orderNo: '', warehouseId: '', deliveryWarehouseId: '', processStatus: '', orderStatus: '', measureReviewStatus: '', customerName: '', destinationCode: '', feeStatus: '', feeReviewStatus: '', holdFlag: '', expectedArrivalRange: [] })
|
||||
@@ -66,7 +56,6 @@ const warehousesLoading = ref(false)
|
||||
const warehousesUnavailable = ref(false)
|
||||
const showAdvancedFilters = ref(false)
|
||||
let loadRequestId = 0
|
||||
let detailRequestId = 0
|
||||
let customerRequestId = 0
|
||||
let warehouseRequestId = 0
|
||||
let disposed = false
|
||||
@@ -293,35 +282,7 @@ function applyFilters() {
|
||||
else page.value = 1
|
||||
}
|
||||
|
||||
function resetDetail() {
|
||||
detailRequestId += 1
|
||||
detail.value = null
|
||||
detailTarget.value = null
|
||||
detailError.value = null
|
||||
detailLoading.value = false
|
||||
}
|
||||
|
||||
async function openDetail(row: InboundOrderSummary) {
|
||||
const requestId = ++detailRequestId
|
||||
detailOpen.value = true
|
||||
detailTarget.value = row
|
||||
detailLoading.value = true
|
||||
detailError.value = null
|
||||
detail.value = null
|
||||
try {
|
||||
const response = await getPlatformInbound(row.tenantId, row.id)
|
||||
if (disposed || requestId !== detailRequestId) return
|
||||
detail.value = response
|
||||
} catch (error) {
|
||||
if (disposed || requestId !== detailRequestId) return
|
||||
detailError.value = getApiErrorDetails(error, '入库单详情加载失败')
|
||||
} finally {
|
||||
if (!disposed && requestId === detailRequestId) detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeDetail() { resetDetail() }
|
||||
function openWorkspace(row: InboundOrderSummary) {
|
||||
function openDetail(row: InboundOrderSummary) {
|
||||
void router.push({ name: 'inbound-order-detail', params: { inboundId: row.id }, query: { tenantId: row.tenantId } })
|
||||
}
|
||||
async function printCartonLabels(row: InboundOrderSummary) {
|
||||
@@ -372,8 +333,6 @@ function resetFilters() {
|
||||
filters.value = { ...filters.value, customerId: '', warehouseId: '', deliveryWarehouseId: '' }
|
||||
warehouseOptions.value = []
|
||||
warehousesUnavailable.value = false
|
||||
resetDetail()
|
||||
detailOpen.value = false
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
@@ -397,14 +356,12 @@ watch(() => filters.value.customerId, (customerId) => {
|
||||
if (customerId === selectedCustomerId.value) return
|
||||
selectedCustomerId.value = customerId
|
||||
filters.value = { ...filters.value, warehouseId: '', deliveryWarehouseId: '' }
|
||||
resetDetail()
|
||||
detailOpen.value = false
|
||||
void loadWarehouseOptions()
|
||||
})
|
||||
watch(tab, () => { if (page.value === 1) void loadInbounds(); else page.value = 1 })
|
||||
watch([page, pageSize], () => { void loadInbounds() })
|
||||
onMounted(() => { void loadCustomers(); void loadInbounds() })
|
||||
onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId += 1; customerRequestId += 1; warehouseRequestId += 1 })
|
||||
onBeforeUnmount(() => { disposed = true; loadRequestId += 1; customerRequestId += 1; warehouseRequestId += 1 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -454,7 +411,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
|
||||
</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 link type="primary" @click="openWorkspace(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button link :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button link @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 link type="primary" @click="openDetail(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button link :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button link @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>
|
||||
@@ -499,15 +456,6 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
|
||||
<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">
|
||||
<DetailPageLayout aria-label="入库单详情抽屉">
|
||||
<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)"><RefreshCw :size="16" />重新加载</el-button></section>
|
||||
<div v-else-if="detail" class="detail-content">
|
||||
<FormSection title="入库概览"><InboundSummaryPanel :summary="detail.summary" :public-remark="detail.publicRemark" /></FormSection>
|
||||
<FormSection title="资源与状态记录" :bordered="false"><InboundHistoryPanel :pallet-locations="detail.palletLocations" :files="detail.files" :internal-notes="detail.internalNotes" :status-history="detail.statusHistory" :public-remark="null" /></FormSection>
|
||||
</div>
|
||||
</DetailPageLayout>
|
||||
</AppDrawer>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
@@ -535,11 +483,6 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
|
||||
.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: #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: 0 10px; } .inbound-stats-row { min-height: 0; align-items: stretch; flex-direction: column; gap: 10px; padding: 10px; } .inbound-stats-row .total-info { justify-content: flex-start; } }
|
||||
@media (max-width: 360px) { .inbound-tab { padding-inline: 12px; } .row-actions { align-items: flex-start; flex-direction: column; } }
|
||||
|
||||
Reference in New Issue
Block a user