优化
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/shared/api/http'
|
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', () => ({
|
vi.mock('@/shared/api/http', () => ({
|
||||||
ApiResponseError: class ApiResponseError extends Error {
|
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 () => {
|
it('loads and normalizes the tenant-scoped hold state and immutable history', async () => {
|
||||||
vi.mocked(http.get).mockResolvedValue({ data: {
|
vi.mocked(http.get).mockResolvedValue({ data: {
|
||||||
inboundId: 19001, tenantId: 10002, holdFlag: true, currentHoldReason: ' 待核对货款 ', version: '4',
|
inboundId: 19001, tenantId: 10002, holdFlag: true, currentHoldReason: ' 待核对货款 ', version: '4',
|
||||||
|
|||||||
@@ -238,6 +238,25 @@ export async function downloadInboundCartonLabels(tenantId: string, inboundId: s
|
|||||||
return response
|
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) {
|
export async function downloadDropshipPalletLabel(tenantId: string, inboundId: string, palletId: string) {
|
||||||
return http.get<Blob, Blob>(
|
return http.get<Blob, Blob>(
|
||||||
`/platform/v1/first-leg/pda-dropship/inbound-orders/${encodeURIComponent(inboundId)}/pallets/${encodeURIComponent(palletId)}/print`,
|
`/platform/v1/first-leg/pda-dropship/inbound-orders/${encodeURIComponent(inboundId)}/pallets/${encodeURIComponent(palletId)}/print`,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export { getInboundMeasurement, saveInboundMeasurementDraft, updateMeasurementCalculationStrategy } from './measurement/api'
|
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 InboundMeasurementPanel } from './measurement/InboundMeasurementPanel.vue'
|
||||||
export { default as InboundMeasurementDialog } from './measurement/InboundMeasurementDialog.vue'
|
export { default as InboundMeasurementDialog } from './measurement/InboundMeasurementDialog.vue'
|
||||||
export { default as MeasurementEvidenceThumbnail } from './measurement/components/MeasurementEvidenceThumbnail.vue'
|
export { default as MeasurementEvidenceThumbnail } from './measurement/components/MeasurementEvidenceThumbnail.vue'
|
||||||
|
|||||||
@@ -521,6 +521,14 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
expect(source).not.toContain('title="更多操作"')
|
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', () => {
|
it('renders order detail as a routed full page instead of a drawer', () => {
|
||||||
expect(source).toContain('<template v-if="isDetailRoute">')
|
expect(source).toContain('<template v-if="isDetailRoute">')
|
||||||
expect(source).toContain('class="order-detail-header"')
|
expect(source).toContain('class="order-detail-header"')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||||
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
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 { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
getPlatformUnifiedOrder,
|
getPlatformUnifiedOrder,
|
||||||
@@ -68,6 +68,7 @@ import {
|
|||||||
getLatestInboundFee,
|
getLatestInboundFee,
|
||||||
getInboundMeasurement,
|
getInboundMeasurement,
|
||||||
getPlatformInboundHold,
|
getPlatformInboundHold,
|
||||||
|
downloadInboundCartonLabelsBatch,
|
||||||
downloadPlatformInboundHoldEvidence,
|
downloadPlatformInboundHoldEvidence,
|
||||||
holdPlatformInbound,
|
holdPlatformInbound,
|
||||||
releasePlatformInbound,
|
releasePlatformInbound,
|
||||||
@@ -170,8 +171,10 @@ const batchApprovalPreviewRows = ref<UnifiedOrderBatchApprovalPreviewItem[]>([])
|
|||||||
const batchApprovalSelectedRows = ref<UnifiedOrderBatchApprovalPreviewItem[]>([])
|
const batchApprovalSelectedRows = ref<UnifiedOrderBatchApprovalPreviewItem[]>([])
|
||||||
const batchApprovalTableRef = ref<{ toggleRowSelection: (row: UnifiedOrderBatchApprovalPreviewItem, selected?: boolean) => void }>()
|
const batchApprovalTableRef = ref<{ toggleRowSelection: (row: UnifiedOrderBatchApprovalPreviewItem, selected?: boolean) => void }>()
|
||||||
const exportSubmitting = ref(false)
|
const exportSubmitting = ref(false)
|
||||||
|
const printingCartonLabels = ref(false)
|
||||||
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
|
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
|
||||||
const selectedOrders = ref<UnifiedOrderResource[]>([])
|
const selectedOrders = ref<UnifiedOrderResource[]>([])
|
||||||
|
const selectedCartonLabelOrders = computed(() => selectedOrders.value.filter((row) => row.inboundId != null))
|
||||||
const coLoadCreateOpen = ref(false)
|
const coLoadCreateOpen = ref(false)
|
||||||
const coLoadDetailOpen = ref(false)
|
const coLoadDetailOpen = ref(false)
|
||||||
const coLoadDetailInboundCode = ref('')
|
const coLoadDetailInboundCode = ref('')
|
||||||
@@ -618,8 +621,8 @@ const queryFields = computed(() => showAdvancedFilters.value
|
|||||||
: primaryQueryFields.value)
|
: primaryQueryFields.value)
|
||||||
|
|
||||||
const columns = computed<DataTableColumn[]>(() => [
|
const columns = computed<DataTableColumn[]>(() => [
|
||||||
{ key: 'actions', label: '操作', width: 270, fixed: 'left', slotName: 'actions' },
|
{ 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: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',
|
key: 'selection', type: 'selection', width: 48, fixed: 'left',
|
||||||
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value && !exportSubmitting.value,
|
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value && !exportSubmitting.value,
|
||||||
} satisfies DataTableColumn] : []),
|
} satisfies DataTableColumn] : []),
|
||||||
@@ -1984,9 +1987,9 @@ function moreUnifiedOrderActionsLabel(row: UnifiedOrderResource) {
|
|||||||
return `更多订单操作:${String(row.inboundCode || '').trim() || '当前入仓单'}`
|
return `更多订单操作:${String(row.inboundCode || '').trim() || '当前入仓单'}`
|
||||||
}
|
}
|
||||||
function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
|
function unifiedOrderActionLabel(action: UnifiedOrderRowAction) {
|
||||||
if (action === 'note') return '添加备注'
|
if (action === 'note') return '备注'
|
||||||
if (action === 'approve') return '审核通过'
|
if (action === 'approve') return '通过'
|
||||||
return '测量作业'
|
return '测量'
|
||||||
}
|
}
|
||||||
function unifiedOrderActionType(action: UnifiedOrderRowAction) {
|
function unifiedOrderActionType(action: UnifiedOrderRowAction) {
|
||||||
return action === 'approve' ? 'success' : 'primary'
|
return action === 'approve' ? 'success' : 'primary'
|
||||||
@@ -1997,6 +2000,32 @@ function runMoreAction(command: string | number | object, row: UnifiedOrderResou
|
|||||||
if (command === 'measurement') openMeasurement(row)
|
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() {
|
function closeDetail() {
|
||||||
releaseCargoImagePreviews()
|
releaseCargoImagePreviews()
|
||||||
detail.value = null
|
detail.value = null
|
||||||
@@ -2576,6 +2605,7 @@ onBeforeUnmount(() => {
|
|||||||
<div v-if="can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action">
|
<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>
|
<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>
|
</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>
|
<el-button v-if="can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" />智能装柜</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ row }">
|
<template #actions="{ row }">
|
||||||
|
|||||||
Reference in New Issue
Block a user