This commit is contained in:
2026-08-07 17:16:23 +08:00
parent dbaf4c09df
commit 6849aedd3b
3 changed files with 74 additions and 8 deletions
+28 -3
View File
@@ -1,14 +1,21 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { getPlatformInbound, listPlatformInbounds } from './api'
import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from './api'
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn() },
ApiResponseError: class ApiResponseError extends Error {
code: string
constructor(message: string, code: string) { super(message); this.code = code }
},
http: { get: vi.fn(), post: vi.fn() },
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
describe('platform inbound query API', () => {
beforeEach(() => vi.mocked(http.get).mockReset())
beforeEach(() => {
vi.mocked(http.get).mockReset()
vi.mocked(http.post).mockReset()
})
it('omits the backend tenant filter for an all-customer list query', async () => {
vi.mocked(http.get).mockResolvedValue({
@@ -83,4 +90,22 @@ describe('platform inbound query API', () => {
},
})
})
it('downloads stable carton labels and rejects JSON business errors', async () => {
const pdf = new Blob(['pdf'], { type: 'application/pdf' })
vi.mocked(http.post).mockResolvedValueOnce(pdf)
await expect(downloadInboundCartonLabels('10001', '19001', [1, 3])).resolves.toBe(pdf)
expect(http.post).toHaveBeenCalledWith('/platform/inbounds/19001/carton-label-pdf', null, {
params: { tenantId: '10001', cartonIndex: [1, 3] }, responseType: 'blob', timeout: 60_000,
})
vi.mocked(http.post).mockResolvedValueOnce({
type: 'application/json',
text: async () => JSON.stringify({ code: 'A0422', message: '箱号映射不完整' }),
} as Blob)
await expect(downloadInboundCartonLabels('10001', '19001')).rejects.toMatchObject({
message: '箱号映射不完整', code: 'A0422',
})
})
})
@@ -9,10 +9,11 @@ import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import DataTable from '@/shared/components/DataTable.vue'
import { listPlatformWarehouses } from '@/modules/warehouses'
import { getPlatformInbound, listPlatformInbounds } from '../core/api'
import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from '../core/api'
import type { InboundOrderDetail, InboundOrderSummary } from '../core/types'
vi.mock('../core/api', () => ({
downloadInboundCartonLabels: vi.fn(),
getPlatformInbound: vi.fn(),
listPlatformInbounds: vi.fn(),
}))
@@ -74,6 +75,7 @@ describe('InboundOrdersView', () => {
setActivePinia(createPinia())
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({
records: [
@@ -171,6 +173,25 @@ describe('InboundOrdersView', () => {
wrapper.unmount()
})
it('downloads carton labels from the stable server-side mapping', async () => {
const createObjectURL = vi.fn(() => 'blob:labels')
const revokeObjectURL = vi.fn()
vi.stubGlobal('URL', { createObjectURL, revokeObjectURL })
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
const wrapper = mountPage()
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('打印箱唛'))?.trigger('click')
await flushPromises()
expect(downloadInboundCartonLabels).toHaveBeenCalledWith('10002', '19001')
expect(createObjectURL).toHaveBeenCalledOnce()
expect(revokeObjectURL).toHaveBeenCalledWith('blob:labels')
expect(click).toHaveBeenCalledOnce()
click.mockRestore()
vi.unstubAllGlobals()
})
it('uses the app header as the page title and exposes compact accessible status tabs', async () => {
const wrapper = mountPage()
await flushPromises()
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ChevronDown, ChevronUp, ClipboardCheck, Eye, RefreshCw, SlidersHorizontal } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { ChevronDown, ChevronUp, ClipboardCheck, Eye, Printer, RefreshCw, SlidersHorizontal } from '@lucide/vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -10,7 +11,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformWarehouses } from '@/modules/warehouses'
import { getPlatformInbound, listPlatformInbounds } from '../core/api'
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'
@@ -43,6 +44,7 @@ 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: [] })
const selectedCustomerId = ref('')
@@ -112,7 +114,7 @@ const queryFields = computed(() => showAdvancedFilters.value
: allQueryFields.value.filter((field) => !advancedFilterKeys.has(field.key)))
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 152, fixed: 'left', slotName: 'actions' },
{ key: 'actions', label: '操作', width: 228, fixed: 'left', slotName: 'actions' },
{ key: 'inboundCode', label: '入仓码', minWidth: 166, slotName: 'inbound' },
{ key: 'orderStatus', label: '订单 / 流程状态', minWidth: 156, slotName: 'orderStatus' },
{ key: 'measureReviewStatus', label: '测量数据审核状态', minWidth: 142, slotName: 'measureReviewStatus' },
@@ -307,6 +309,24 @@ function closeDetail() { resetDetail() }
function openWorkspace(row: InboundOrderSummary) {
void router.push({ name: 'inbound-order-detail', params: { inboundId: row.id }, query: { tenantId: row.tenantId } })
}
async function printCartonLabels(row: InboundOrderSummary) {
if (printingInboundId.value) return
printingInboundId.value = row.id
try {
const blob = await downloadInboundCartonLabels(row.tenantId, row.id)
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `${row.inboundCode || row.id}-${row.totalPackageCount || '整单'}件-箱唛.pdf`
anchor.click()
URL.revokeObjectURL(url)
ElMessage.success('箱唛已生成,重复打印将继续使用原箱号')
} catch (error) {
ElMessage.error(getApiErrorDetails(error, '箱唛生成失败').message)
} finally {
printingInboundId.value = ''
}
}
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'
@@ -421,7 +441,7 @@ onBeforeUnmount(() => { disposed = true; loadRequestId += 1; detailRequestId +=
<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 #toolbar-start><div><h2>入库单列表</h2><p>所属企业 {{ selectedCustomerName }}</p></div></template>
<template #toolbar-actions><el-button :loading="loading" @click="loadInbounds"><RefreshCw :size="16" />刷新</el-button></template>
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" @click="openWorkspace(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button link @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div></template>
<template #actions="{ row }"><div 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></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 #orderStatus="{ row }"><div class="status-stack"><el-tag :type="statusType((row as InboundOrderSummary).processStatus)" effect="light">{{ statusLabel((row as InboundOrderSummary).processStatus) }}</el-tag><span>订单:{{ text((row as InboundOrderSummary).orderStatusSnapshot) }}</span></div></template>
<template #measureReviewStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).measureReviewStatus, measureReviewLabels) }}</template>