This commit is contained in:
2026-08-21 18:16:42 +08:00
parent b77428b453
commit 0e67cb9d65
8 changed files with 233 additions and 8 deletions
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { overseasHttp } from '@/shared/api/overseasHttp'
import { listDispatchCandidates, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet } from './dispatches' import { getDispatch, listDispatchCandidates, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet } from './dispatches'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } }))
@@ -28,6 +28,14 @@ describe('dispatch overseas API', () => {
}) })
}) })
it('loads a platform dispatch detail by encoded dispatch number', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC/1' } })
await getDispatch('PC/1')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1')
})
it('loads candidates and creates a pre-arrange dispatch in an explicit customer scope', async () => { it('loads candidates and creates a pre-arrange dispatch in an explicit customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: [] }) vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: [] })
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC-1' } }) vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC-1' } })
@@ -84,6 +84,25 @@ export interface DispatchSummaryDto {
remainingPackageCount?: number | null remainingPackageCount?: number | null
partialCompletionReason?: string | null partialCompletionReason?: string | null
createdAt?: string | null createdAt?: string | null
items?: DispatchItemDto[]
}
export interface DispatchItemDto {
id: string
planPalletId: string
warehousePalletId?: string | null
palletNo?: string | null
overseasWarehouseId?: string | null
overseasWarehouseName?: string | null
orderNo?: string | null
orderRefs?: string | null
inboundCode?: string | null
destinationWarehouseCode?: string | null
destinationWarehouseName?: string | null
fbaWarehouseCode?: string | null
packageCount?: number | null
volumeCbm?: number | null
sourceStage?: string | null
} }
export async function listDispatches(query: DispatchQuery) { export async function listDispatches(query: DispatchQuery) {
@@ -94,6 +113,13 @@ export async function listDispatches(query: DispatchQuery) {
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function getDispatch(dispatchNo: string) {
const response = await overseasHttp.get<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`,
)
return unwrapApiResponse(response)
}
export async function listDispatchCandidates(query: DispatchCandidateQuery) { export async function listDispatchCandidates(query: DispatchCandidateQuery) {
const response = await overseasHttp.get<ApiEnvelope<DispatchPalletCandidateDto[]>, ApiEnvelope<DispatchPalletCandidateDto[]>>( const response = await overseasHttp.get<ApiEnvelope<DispatchPalletCandidateDto[]>, ApiEnvelope<DispatchPalletCandidateDto[]>>(
'/platform/v1/last-leg/dispatches/candidates', '/platform/v1/last-leg/dispatches/candidates',
@@ -0,0 +1,167 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getDispatch, type DispatchItemDto, type DispatchSummaryDto } from '../api/dispatches'
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
import { formatOverseasDateTime } from '../lastMilePresentation'
interface DetailField { label: string; value: string | number }
interface RelatedOrderRow {
orderNo: string
weight: string
palletCount: number
fbaWarehouse: string
volume: number
containerNo: string
}
const route = useRoute()
const router = useRouter()
const dispatchNo = computed(() => String(route.params.dispatchNo || ''))
const detail = ref<DispatchSummaryDto | null>(null)
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const dispatchStatusLabels: Record<string, string> = {
PENDING_DEPARTURE: '待发车',
PICKING: '拣货中',
READY_TO_DEPART: '待发车(拣货完成)',
PARTIAL_READY_TO_DEPART: '待发车(部分完成)',
DEPARTED: '已发车',
PARTIAL_DEPARTED: '已发车(部分完成)',
CANCELLED: '已取消',
}
const appointmentStatusLabels: Record<string, string> = {
PENDING: '待预约',
BOOKED: '已预约',
RESERVED: '已预约',
CONFIRMED: '已确认',
CANCELLED: '已取消',
}
function text(value: unknown) { return String(value ?? '').trim() || '—' }
function dispatchStatus(value: unknown) {
const code = String(value ?? '').trim()
return dispatchStatusLabels[code] || text(code)
}
function appointmentStatus(value: unknown) {
const code = String(value ?? '').trim()
return appointmentStatusLabels[code] || text(code)
}
function orderReferences(item: DispatchItemDto) {
const references = String(item.orderRefs || '').split(/[,,;;\s]+/).map((value) => value.trim()).filter(Boolean)
return references.length ? references : [text(item.orderNo)]
}
const vehicleFields = computed<DetailField[]>(() => [
{ label: '车牌号', value: text(detail.value?.vehiclePlate) },
{ label: '司机', value: text(detail.value?.driverName) },
{ label: '联系电话', value: text(detail.value?.driverPhone) },
{ label: '拖车公司名称', value: text(detail.value?.carrierName) },
{ label: '车辆状态', value: dispatchStatus(detail.value?.status) },
])
const dispatchFields = computed<DetailField[]>(() => [
{ label: '路线', value: text(detail.value?.route) },
{ label: '发车时间', value: formatOverseasDateTime(detail.value?.departAt) },
{ label: '预计到达时间', value: formatOverseasDateTime(detail.value?.estimatedArrivalAt) },
{ label: 'FBA预约', value: appointmentStatus(detail.value?.fbaAppointmentStatus) },
{ label: '车辆预约', value: appointmentStatus(detail.value?.vehicleAppointmentStatus) },
])
const documentFields = computed<DetailField[]>(() => [
{ label: '排车单号', value: text(detail.value?.dispatchNo || dispatchNo.value) },
{ label: '关联订单', value: text(detail.value?.orderNos) },
{ label: '创建时间', value: formatOverseasDateTime(detail.value?.createdAt) },
{ label: '更新时间', value: '—' },
{ label: '备注', value: '—' },
...(detail.value?.partialCompletionReason
? [{ label: '部分完成原因', value: text(detail.value.partialCompletionReason) }]
: []),
])
const relatedOrders = computed<RelatedOrderRow[]>(() => {
const grouped = new Map<string, { pallets: Set<string>; destinations: Set<string>; volume: number }>()
for (const item of detail.value?.items || []) {
for (const orderNo of orderReferences(item)) {
const current = grouped.get(orderNo) || { pallets: new Set<string>(), destinations: new Set<string>(), volume: 0 }
current.pallets.add(String(item.warehousePalletId || item.planPalletId || item.id))
const destination = text(item.fbaWarehouseCode || item.destinationWarehouseCode || item.destinationWarehouseName)
if (destination !== '—') current.destinations.add(destination)
current.volume += Number(item.volumeCbm || 0)
grouped.set(orderNo, current)
}
}
return [...grouped.entries()].map(([orderNo, value]) => ({
orderNo,
weight: '—',
palletCount: value.pallets.size,
fbaWarehouse: [...value.destinations].join('、') || '—',
volume: Number(value.volume.toFixed(3)),
containerNo: '—',
}))
})
const orderColumns: DataTableColumn[] = [
{ key: 'orderNo', prop: 'orderNo', label: '订单号', minWidth: 210 },
{ key: 'weight', prop: 'weight', label: '重量(kg)', minWidth: 130, align: 'right' },
{ key: 'palletCount', prop: 'palletCount', label: '托数', minWidth: 100, align: 'right' },
{ key: 'fbaWarehouse', prop: 'fbaWarehouse', label: '目的FBA仓', minWidth: 150 },
{ key: 'volume', prop: 'volume', label: '体积(m³)', minWidth: 130, align: 'right' },
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 170 },
]
async function load() {
if (!dispatchNo.value) {
loadError.value = { message: '详情缺少排车单号,请从排车列表重新进入' }
return
}
loading.value = true
loadError.value = null
try { detail.value = await getDispatch(dispatchNo.value) }
catch (error) { loadError.value = getApiErrorDetails(error, '排车详情加载失败,请重试') }
finally { loading.value = false }
}
function back() { void router.push({ name: 'overseas-dispatches' }) }
onMounted(load)
</script>
<template>
<OverseasDetailShell title="排车详情" reference-label="排车单号" :reference-value="detail?.dispatchNo || dispatchNo" back-label="返回排车列表" domain="排车详情" :pending="false" :status-text="detail ? dispatchStatus(detail.status) : '加载中'" @back="back">
<LoadErrorState v-if="loadError" title="排车详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<div v-else v-loading="loading" class="dispatch-detail" :aria-busy="loading">
<div class="summary-grid">
<section class="detail-section" aria-labelledby="dispatch-vehicle-heading"><h2 id="dispatch-vehicle-heading">车辆信息</h2><dl class="detail-fields"><div v-for="field in vehicleFields" :key="field.label"><dt>{{ field.label }}</dt><dd>{{ field.value }}</dd></div></dl></section>
<section class="detail-section" aria-labelledby="dispatch-info-heading"><h2 id="dispatch-info-heading">排车信息</h2><dl class="detail-fields"><div v-for="field in dispatchFields" :key="field.label"><dt>{{ field.label }}</dt><dd>{{ field.value }}</dd></div></dl></section>
<section class="detail-section document-section" aria-labelledby="dispatch-document-heading"><h2 id="dispatch-document-heading">单据信息</h2><dl class="detail-fields"><div v-for="field in documentFields" :key="field.label"><dt>{{ field.label }}</dt><dd>{{ field.value }}</dd></div></dl></section>
</div>
<section class="detail-section order-section" aria-labelledby="dispatch-orders-heading"><h2 id="dispatch-orders-heading">关联订单明细</h2><DataTable :data="relatedOrders" :columns="orderColumns" column-settings-mode="hide" row-key="orderNo" empty-text="暂无关联订单明细" /></section>
</div>
</OverseasDetailShell>
</template>
<style scoped>
.dispatch-detail { min-width: 0; min-height: 260px; }
.summary-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--cc-border); border-bottom: 0; background: #fff; }
.detail-section { min-width: 0; padding: 18px; }
.summary-grid > .detail-section { border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
.summary-grid > .detail-section:nth-child(2) { border-right: 0; }
.document-section { grid-column: 1 / 2; }
.detail-section h2 { margin: 0 0 14px; color: var(--el-color-primary); font-size: 16px; }
.detail-fields { margin: 0; border-top: 1px solid var(--cc-border); }
.detail-fields > div { display: grid; min-height: 48px; grid-template-columns: 180px minmax(0, 1fr); border-bottom: 1px solid var(--cc-border); }
.detail-fields dt, .detail-fields dd { display: flex; min-width: 0; align-items: center; padding: 9px 12px; }
.detail-fields dt { color: var(--cc-muted); }
.detail-fields dd { margin: 0; color: var(--cc-text); font-weight: 600; overflow-wrap: anywhere; }
.order-section { overflow: hidden; border: 1px solid var(--cc-border); border-top: 0; background: #fff; }
.order-section h2 { margin-right: 4px; margin-left: 4px; }
@media (max-width: 960px) {
.summary-grid { grid-template-columns: 1fr; }
.summary-grid > .detail-section, .summary-grid > .detail-section:nth-child(2) { border-right: 0; }
.document-section { grid-column: auto; }
.detail-fields > div { grid-template-columns: 132px minmax(0, 1fr); }
}
</style>
@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { Eye } from '@lucide/vue'
import { useRouter } from 'vue-router'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue' import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -11,6 +13,7 @@ import StatusBadge from '@/shared/components/StatusBadge.vue'
import { listDispatches, type DispatchSummaryDto } from '../api/dispatches' import { listDispatches, type DispatchSummaryDto } from '../api/dispatches'
import type { DispatchListItem } from '../types' import type { DispatchListItem } from '../types'
const router = useRouter()
const rows = ref<DispatchListItem[]>([]) const rows = ref<DispatchListItem[]>([])
const loading = ref(false) const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null) const loadError = ref<ApiErrorDetails | null>(null)
@@ -37,6 +40,7 @@ const fields = computed<QueryField[]>(() => [
]) ])
const columns: DataTableColumn[] = [ const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 170, fixed: 'left' }, { key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 170, fixed: 'left' },
{ key: 'vehiclePlate', prop: 'vehiclePlate', label: '车牌号', minWidth: 130 }, { key: 'vehiclePlate', prop: 'vehiclePlate', label: '车牌号', minWidth: 130 },
{ key: 'driverName', prop: 'driverName', label: '司机', minWidth: 120 }, { key: 'driverName', prop: 'driverName', label: '司机', minWidth: 120 },
@@ -132,6 +136,7 @@ async function load(model: QueryModel = filters.value) {
function search(model: QueryModel) { page.value = 1; void load(model) } function search(model: QueryModel) { page.value = 1; void load(model) }
function reset(model: QueryModel) { page.value = 1; void load(model) } function reset(model: QueryModel) { page.value = 1; void load(model) }
function openDetail(row: DispatchListItem) { void router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } }) }
onMounted(load) onMounted(load)
</script> </script>
@@ -141,6 +146,7 @@ onMounted(load)
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="排车列表筛选" preference-key="overseas-dispatches-query" @search="search" @reset="reset" /></template> <template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="排车列表筛选" preference-key="overseas-dispatches-query" @search="search" @reset="reset" /></template>
<template v-if="loadError" #state><LoadErrorState title="排车列表加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template> <template v-if="loadError" #state><LoadErrorState title="排车列表加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
<section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-dispatches" row-key="id" empty-text="暂无排车记录" @update:page="load()" @update:page-size="page = 1; load()"> <section v-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-dispatches" row-key="id" empty-text="暂无排车记录" @update:page="load()" @update:page-size="page = 1; load()">
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row as DispatchListItem)"><Eye :size="15" />查看</el-button></template>
<template #status="{ row }"><StatusBadge :status="String((row as DispatchListItem).status || '—')" /></template> <template #status="{ row }"><StatusBadge :status="String((row as DispatchListItem).status || '—')" /></template>
<template #pagination-left>共 {{ total }} 条排车记录</template> <template #pagination-left>共 {{ total }} 条排车记录</template>
</DataTable></section> </DataTable></section>
@@ -53,8 +53,6 @@ const columns: DataTableColumn[] = [
{ key: 'held', prop: 'held', label: '暂扣库存', width: 105, align: 'right' }, { key: 'held', prop: 'held', label: '暂扣库存', width: 105, align: 'right' },
{ key: 'looseboard', prop: 'looseboard', label: '美本土散板', width: 115, align: 'right' }, { key: 'looseboard', prop: 'looseboard', label: '美本土散板', width: 115, align: 'right' },
{ key: 'inTransit', prop: 'inTransit', label: '在途库存', width: 105, align: 'right' }, { key: 'inTransit', prop: 'inTransit', label: '在途库存', width: 105, align: 'right' },
{ key: 'purchase', prop: 'purchase', label: '采购库存', width: 105, align: 'right' },
{ key: 'status', prop: 'status', label: '库存状态', width: 110 },
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150 }, { key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150 },
] ]
@@ -41,7 +41,7 @@ describe('overseas warehouse first-batch prototype contract', () => {
}) })
it('keeps the operation column first on every business list', () => { it('keeps the operation column first on every business list', () => {
for (const fileName of ['LastMileOrdersView.vue', 'InboundOrdersView.vue', 'InventoryView.vue', 'RelabelOrdersView.vue', 'PalletsView.vue', 'CartonsView.vue', 'ProductsView.vue']) { for (const fileName of ['LastMileOrdersView.vue', 'InboundOrdersView.vue', 'DispatchesView.vue', 'InventoryView.vue', 'RelabelOrdersView.vue', 'PalletsView.vue', 'CartonsView.vue', 'ProductsView.vue']) {
const value = source(fileName) const value = source(fileName)
const columns = value.slice(value.indexOf('const columns: DataTableColumn[]')) const columns = value.slice(value.indexOf('const columns: DataTableColumn[]'))
expect(columns.match(/\{\s*key: '([^']+)'/)?.[1]).toBe('actions') expect(columns.match(/\{\s*key: '([^']+)'/)?.[1]).toBe('actions')
@@ -105,8 +105,8 @@ describe('overseas warehouse first-batch prototype contract', () => {
expect(pallets).toContain('ElMessage.success') expect(pallets).toContain('ElMessage.success')
}) })
it('implements three prototype-defined details as formal pages', () => { it('implements prototype-defined details as formal pages', () => {
for (const fileName of ['LastMileOrderDetailView.vue', 'InboundOrderDetailView.vue', 'InventoryDetailView.vue']) { for (const fileName of ['LastMileOrderDetailView.vue', 'InboundOrderDetailView.vue', 'DispatchDetailView.vue', 'InventoryDetailView.vue']) {
const value = source(fileName) const value = source(fileName)
expect(value).toContain('<OverseasDetailShell') expect(value).toContain('<OverseasDetailShell')
expect(value).toContain('@back="back"') expect(value).toContain('@back="back"')
@@ -270,6 +270,7 @@ describe('overseas warehouse first-batch prototype contract', () => {
const expected = [ const expected = [
['LastMileOrdersView.vue', 'overseas-last-mile-order-detail'], ['LastMileOrdersView.vue', 'overseas-last-mile-order-detail'],
['InboundOrdersView.vue', 'overseas-inbound-order-detail'], ['InboundOrdersView.vue', 'overseas-inbound-order-detail'],
['DispatchesView.vue', 'overseas-dispatch-detail'],
['InventoryView.vue', 'overseas-inventory-detail'], ['InventoryView.vue', 'overseas-inventory-detail'],
['RelabelOrdersView.vue', 'overseas-relabel-order-detail'], ['RelabelOrdersView.vue', 'overseas-relabel-order-detail'],
['PalletsView.vue', 'overseas-pallet-detail'], ['PalletsView.vue', 'overseas-pallet-detail'],
@@ -283,6 +284,18 @@ describe('overseas warehouse first-batch prototype contract', () => {
} }
}) })
it('connects the dispatch prototype detail to the real overseas API', () => {
const detail = source('DispatchDetailView.vue')
expect(detail).toContain('getDispatch')
expect(detail).toContain('车辆信息')
expect(detail).toContain('排车信息')
expect(detail).toContain('单据信息')
expect(detail).toContain('关联订单明细')
expect(detail).toContain(':data="relatedOrders"')
expect(detail).toContain("router.push({ name: 'overseas-dispatches' })")
expect(detail).not.toContain('ElMessage.success')
})
it('preserves last-mile status tabs and read-only outbound association columns without links', () => { it('preserves last-mile status tabs and read-only outbound association columns without links', () => {
const value = source('LastMileOrdersView.vue') const value = source('LastMileOrdersView.vue')
expect(value).toContain('aria-label="尾程订单状态"') expect(value).toContain('aria-label="尾程订单状态"')
@@ -317,7 +330,7 @@ describe('overseas warehouse first-batch prototype contract', () => {
]) ])
expect(columnKeys('InventoryView.vue')).toEqual([ expect(columnKeys('InventoryView.vue')).toEqual([
'actions', 'owner', 'warehouse', 'sku', 'productName', 'location', 'onHand', 'available', 'held', 'looseboard', 'actions', 'owner', 'warehouse', 'sku', 'productName', 'location', 'onHand', 'available', 'held', 'looseboard',
'inTransit', 'purchase', 'status', 'updatedAt', 'inTransit', 'updatedAt',
]) ])
expect(columnKeys('RelabelOrdersView.vue')).toEqual([ expect(columnKeys('RelabelOrdersView.vue')).toEqual([
'actions', 'orderNo', 'sku', 'productName', 'cartons', 'budgetTotal', 'labelRequirement', 'fileName', 'status', 'actions', 'orderNo', 'sku', 'productName', 'cartons', 'budgetTotal', 'labelRequirement', 'fileName', 'status',
@@ -10,6 +10,7 @@ describe('overseas warehouse routes', () => {
['overseas-warehouse/inbound-orders', permission.inboundOrderList], ['overseas-warehouse/inbound-orders', permission.inboundOrderList],
['overseas-warehouse/inbound-orders/:orderId', permission.inboundOrderList], ['overseas-warehouse/inbound-orders/:orderId', permission.inboundOrderList],
['overseas-warehouse/dispatches', permission.dispatchList], ['overseas-warehouse/dispatches', permission.dispatchList],
['overseas-warehouse/dispatches/:dispatchNo', permission.dispatchList],
['overseas-warehouse/inventory', permission.inventoryList], ['overseas-warehouse/inventory', permission.inventoryList],
['overseas-warehouse/inventory-transactions', permission.inventoryList], ['overseas-warehouse/inventory-transactions', permission.inventoryList],
['overseas-warehouse/inventory/:inventoryId', permission.inventoryList], ['overseas-warehouse/inventory/:inventoryId', permission.inventoryList],
@@ -22,7 +23,7 @@ describe('overseas warehouse routes', () => {
it('keeps all details as formal parameterized pages', () => { it('keeps all details as formal parameterized pages', () => {
const details = overseasWarehouseRoutes.filter((route) => route.path.includes('/:')) const details = overseasWarehouseRoutes.filter((route) => route.path.includes('/:'))
expect(details).toHaveLength(5) expect(details).toHaveLength(6)
expect(details.every((route) => route.meta.showPageHeading === false)).toBe(true) expect(details.every((route) => route.meta.showPageHeading === false)).toBe(true)
}) })
+6
View File
@@ -32,6 +32,12 @@ export const overseasWarehouseRoutes = [
component: () => import('./pages/DispatchesView.vue'), component: () => import('./pages/DispatchesView.vue'),
meta: { title: '排车列表', description: '查看海外仓车辆调度、预约与发车状态', permission: permission.dispatchList }, meta: { title: '排车列表', description: '查看海外仓车辆调度、预约与发车状态', permission: permission.dispatchList },
}, },
{
path: 'overseas-warehouse/dispatches/:dispatchNo',
name: 'overseas-dispatch-detail',
component: () => import('./pages/DispatchDetailView.vue'),
meta: { title: '排车详情', description: '查看排车、车辆和关联订单明细', permission: permission.dispatchList, showPageHeading: false },
},
{ {
path: 'overseas-warehouse/inventory', path: 'overseas-warehouse/inventory',
name: 'overseas-inventory', name: 'overseas-inventory',