This commit is contained in:
2026-09-10 11:17:45 +08:00
parent 6a038c89ca
commit 9b5b324fe8
5 changed files with 79 additions and 8 deletions
+15 -1
View File
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { downloadInboundCartonLabels, downloadPlatformInboundHoldEvidence, getPlatformInbound, getPlatformInboundHold, holdPlatformInbound, listPlatformInbounds, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './api'
import { downloadInboundCartonLabels, downloadInboundCartonLabelsBatch, downloadPlatformInboundHoldEvidence, getPlatformInbound, getPlatformInboundHold, holdPlatformInbound, listPlatformInbounds, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './api'
vi.mock('@/shared/api/http', () => ({
ApiResponseError: class ApiResponseError extends Error {
@@ -117,6 +117,20 @@ describe('platform inbound query API', () => {
})
})
it('downloads one PDF for a cross-customer batch of inbound orders', async () => {
const pdf = new Blob(['pdf'], { type: 'application/pdf' })
vi.mocked(http.post).mockResolvedValueOnce(pdf)
const items = [
{ tenantId: '10001', inboundId: '19001' },
{ tenantId: '10002', inboundId: '19002' },
]
await expect(downloadInboundCartonLabelsBatch(items)).resolves.toBe(pdf)
expect(http.post).toHaveBeenCalledWith('/platform/inbounds/carton-label-pdf', items, {
responseType: 'blob', timeout: 120_000,
})
})
it('loads and normalizes the tenant-scoped hold state and immutable history', async () => {
vi.mocked(http.get).mockResolvedValue({ data: {
inboundId: 19001, tenantId: 10002, holdFlag: true, currentHoldReason: ' 待核对货款 ', version: '4',
+19
View File
@@ -238,6 +238,25 @@ export async function downloadInboundCartonLabels(tenantId: string, inboundId: s
return response
}
export interface InboundCartonLabelBatchItem {
tenantId: string
inboundId: string
}
export async function downloadInboundCartonLabelsBatch(items: InboundCartonLabelBatchItem[]) {
const response = await http.post<Blob, Blob>(
`${basePath}/carton-label-pdf`,
items,
{ responseType: 'blob', timeout: 120_000 },
)
if (response.type.includes('json')) {
const error = JSON.parse(await response.text()) as Partial<ApiEnvelope<unknown>>
throw new ApiResponseError(error.message || '批量箱唛生成失败', error.code || 'UNKNOWN', undefined,
error.data, error.requestId, error.traceId)
}
return response
}
export async function downloadDropshipPalletLabel(tenantId: string, inboundId: string, palletId: string) {
return http.get<Blob, Blob>(
`/platform/v1/first-leg/pda-dropship/inbound-orders/${encodeURIComponent(inboundId)}/pallets/${encodeURIComponent(palletId)}/print`,
+1 -1
View File
@@ -1,5 +1,5 @@
export { getInboundMeasurement, saveInboundMeasurementDraft, updateMeasurementCalculationStrategy } from './measurement/api'
export { createInboundHoldIdempotencyKey, downloadPlatformInboundHoldEvidence, getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './core/api'
export { createInboundHoldIdempotencyKey, downloadInboundCartonLabelsBatch, downloadPlatformInboundHoldEvidence, getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './core/api'
export { default as InboundMeasurementPanel } from './measurement/InboundMeasurementPanel.vue'
export { default as InboundMeasurementDialog } from './measurement/InboundMeasurementDialog.vue'
export { default as MeasurementEvidenceThumbnail } from './measurement/components/MeasurementEvidenceThumbnail.vue'
@@ -521,6 +521,14 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain('title="更多操作"')
})
it('batch prints stable carton labels from the list toolbar instead of adding another row action', () => {
expect(source).toContain('downloadInboundCartonLabelsBatch')
expect(source).toContain('const selectedCartonLabelOrders = computed')
expect(source).toContain('@click="printSelectedCartonLabels"')
expect(source).toContain('批量箱唛-${selectedCartonLabelOrders.value.length}单.pdf')
expect(source).not.toContain("type UnifiedOrderRowAction = 'note' | 'approve' | 'printCartonLabels'")
})
it('renders order detail as a routed full page instead of a drawer', () => {
expect(source).toContain('<template v-if="isDetailRoute">')
expect(source).toContain('class="order-detail-header"')
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Download, Eye, FileUp, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, Send, ShieldAlert, SlidersHorizontal, StickyNote, Undo2, WalletCards } from '@lucide/vue'
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Download, Eye, FileUp, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Printer, Ruler, Send, ShieldAlert, SlidersHorizontal, StickyNote, Undo2, WalletCards } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder,
@@ -68,6 +68,7 @@ import {
getLatestInboundFee,
getInboundMeasurement,
getPlatformInboundHold,
downloadInboundCartonLabelsBatch,
downloadPlatformInboundHoldEvidence,
holdPlatformInbound,
releasePlatformInbound,
@@ -170,8 +171,10 @@ const batchApprovalPreviewRows = ref<UnifiedOrderBatchApprovalPreviewItem[]>([])
const batchApprovalSelectedRows = ref<UnifiedOrderBatchApprovalPreviewItem[]>([])
const batchApprovalTableRef = ref<{ toggleRowSelection: (row: UnifiedOrderBatchApprovalPreviewItem, selected?: boolean) => void }>()
const exportSubmitting = ref(false)
const printingCartonLabels = ref(false)
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
const selectedOrders = ref<UnifiedOrderResource[]>([])
const selectedCartonLabelOrders = computed(() => selectedOrders.value.filter((row) => row.inboundId != null))
const coLoadCreateOpen = ref(false)
const coLoadDetailOpen = ref(false)
const coLoadDetailInboundCode = ref('')
@@ -618,8 +621,8 @@ const queryFields = computed(() => showAdvancedFilters.value
: primaryQueryFields.value)
const columns = computed<DataTableColumn[]>(() => [
{ key: 'actions', label: '操作', width: 270, fixed: 'left', slotName: 'actions' },
...(can('platform:order:approve') || can('platform:firstleg:smart-loading:assign') || can('platform:order:export') ? [{
{ key: 'actions', label: '操作', width: 290, fixed: 'left', slotName: 'actions' },
...(can('platform:order:approve') || can('platform:firstleg:smart-loading:assign') || can('platform:order:export') || can('platform:firstleg:inbound:view') ? [{
key: 'selection', type: 'selection', width: 48, fixed: 'left',
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value && !exportSubmitting.value,
} satisfies DataTableColumn] : []),
@@ -1984,9 +1987,9 @@ function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource) {
return `更多订单操作:${String(row.inboundCode || '').trim() || '当前入仓单'}`
}
function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
if (action === 'note') return '添加备注'
if (action === 'approve') return '审核通过'
return '测量作业'
if (action === 'note') return '备注'
if (action === 'approve') return '通过'
return '测量'
}
function unifiedOrderActionType(action: UnifiedOrderRowAction) {
return action === 'approve' ? 'success' : 'primary'
@@ -1997,6 +2000,32 @@ function runMoreAction(command: string | number | object, row: UnifiedOrderResou
if (command === 'measurement') openMeasurement(row)
}
async function printSelectedCartonLabels() {
if (!selectedCartonLabelOrders.value.length || printingCartonLabels.value) return
printingCartonLabels.value = true
try {
const blob = await downloadInboundCartonLabelsBatch(selectedCartonLabelOrders.value.map((row) => ({
tenantId: String(row.tenantId),
inboundId: String(row.inboundId),
})))
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `批量箱唛-${selectedCartonLabelOrders.value.length}单.pdf`
document.body.appendChild(anchor)
anchor.click()
anchor.remove()
URL.revokeObjectURL(url)
const skipped = selectedOrders.value.length - selectedCartonLabelOrders.value.length
if (skipped > 0) ElMessage.warning(`箱唛已开始下载;已跳过 ${skipped} 个尚未生成入库单的订单`)
else ElMessage.success('批量箱唛已生成并开始下载')
} catch (error) {
ElMessage.error(getApiErrorDetails(error, '批量箱唛生成失败,请稍后重试').message)
} finally {
printingCartonLabels.value = false
}
}
function closeDetail() {
releaseCargoImagePreviews()
detail.value = null
@@ -2576,6 +2605,7 @@ onBeforeUnmount(() => {
<div v-if="can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action">
<el-button type="primary" plain :loading="coLoadSaving" :disabled="loading || !canCreateCoLoadGroup" aria-label="合柜报关" @click="openCoLoadCreate"><Link2 :size="16" />合柜报关<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
</div>
<el-button v-if="can('platform:firstleg:inbound:view')" :loading="printingCartonLabels" :disabled="loading || printingCartonLabels || !selectedCartonLabelOrders.length" @click="printSelectedCartonLabels"><Printer :size="16" />打印箱唛<span v-if="selectedCartonLabelOrders.length">({{ selectedCartonLabelOrders.length }})</span></el-button>
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
</template>
<template #actions="{ row }">