优化
This commit is contained in:
@@ -1,14 +1,21 @@
|
|||||||
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 { getPlatformInbound, listPlatformInbounds } from './api'
|
import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from './api'
|
||||||
|
|
||||||
vi.mock('@/shared/api/http', () => ({
|
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,
|
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe('platform inbound query API', () => {
|
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 () => {
|
it('omits the backend tenant filter for an all-customer list query', async () => {
|
||||||
vi.mocked(http.get).mockResolvedValue({
|
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 QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||||||
import DataTable from '@/shared/components/DataTable.vue'
|
import DataTable from '@/shared/components/DataTable.vue'
|
||||||
import { listPlatformWarehouses } from '@/modules/warehouses'
|
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'
|
import type { InboundOrderDetail, InboundOrderSummary } from '../core/types'
|
||||||
|
|
||||||
vi.mock('../core/api', () => ({
|
vi.mock('../core/api', () => ({
|
||||||
|
downloadInboundCartonLabels: vi.fn(),
|
||||||
getPlatformInbound: vi.fn(),
|
getPlatformInbound: vi.fn(),
|
||||||
listPlatformInbounds: vi.fn(),
|
listPlatformInbounds: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@@ -74,6 +75,7 @@ describe('InboundOrdersView', () => {
|
|||||||
setActivePinia(createPinia())
|
setActivePinia(createPinia())
|
||||||
vi.mocked(listPlatformInbounds).mockResolvedValue({ records: [summary], page: 1, size: 20, total: 1, pages: 1 })
|
vi.mocked(listPlatformInbounds).mockResolvedValue({ records: [summary], page: 1, size: 20, total: 1, pages: 1 })
|
||||||
vi.mocked(getPlatformInbound).mockResolvedValue(detail)
|
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(listPlatformTenants).mockResolvedValue({ records: customers, page: 1, size: 200, total: 2, pages: 1 })
|
||||||
vi.mocked(listPlatformWarehouses).mockResolvedValue({
|
vi.mocked(listPlatformWarehouses).mockResolvedValue({
|
||||||
records: [
|
records: [
|
||||||
@@ -171,6 +173,25 @@ describe('InboundOrdersView', () => {
|
|||||||
wrapper.unmount()
|
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 () => {
|
it('uses the app header as the page title and exposes compact accessible status tabs', async () => {
|
||||||
const wrapper = mountPage()
|
const wrapper = mountPage()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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 { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
||||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.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 QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||||
import { listPlatformWarehouses } from '@/modules/warehouses'
|
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 InboundHistoryPanel from '../core/InboundHistoryPanel.vue'
|
||||||
import InboundSummaryPanel from '../core/InboundSummaryPanel.vue'
|
import InboundSummaryPanel from '../core/InboundSummaryPanel.vue'
|
||||||
import type { InboundListTab, InboundOrderDetail, InboundOrderSummary } from '../core/types'
|
import type { InboundListTab, InboundOrderDetail, InboundOrderSummary } from '../core/types'
|
||||||
@@ -43,6 +44,7 @@ const detailLoading = ref(false)
|
|||||||
const detailError = ref<ApiErrorDetails | null>(null)
|
const detailError = ref<ApiErrorDetails | null>(null)
|
||||||
const detail = ref<InboundOrderDetail | null>(null)
|
const detail = ref<InboundOrderDetail | null>(null)
|
||||||
const detailTarget = ref<InboundOrderSummary | null>(null)
|
const detailTarget = ref<InboundOrderSummary | null>(null)
|
||||||
|
const printingInboundId = ref('')
|
||||||
const result = ref({ records: [] as InboundOrderSummary[], total: 0 })
|
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 filters = ref<FilterModel>({ customerId: '', inboundCode: '', orderNo: '', warehouseId: '', deliveryWarehouseId: '', processStatus: '', orderStatus: '', measureReviewStatus: '', customerName: '', destinationCode: '', feeStatus: '', feeReviewStatus: '', holdFlag: '', expectedArrivalRange: [] })
|
||||||
const selectedCustomerId = ref('')
|
const selectedCustomerId = ref('')
|
||||||
@@ -112,7 +114,7 @@ const queryFields = computed(() => showAdvancedFilters.value
|
|||||||
: allQueryFields.value.filter((field) => !advancedFilterKeys.has(field.key)))
|
: allQueryFields.value.filter((field) => !advancedFilterKeys.has(field.key)))
|
||||||
|
|
||||||
const columns: DataTableColumn[] = [
|
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: 'inboundCode', label: '入仓码', minWidth: 166, slotName: 'inbound' },
|
||||||
{ key: 'orderStatus', label: '订单 / 流程状态', minWidth: 156, slotName: 'orderStatus' },
|
{ key: 'orderStatus', label: '订单 / 流程状态', minWidth: 156, slotName: 'orderStatus' },
|
||||||
{ key: 'measureReviewStatus', label: '测量数据审核状态', minWidth: 142, slotName: 'measureReviewStatus' },
|
{ key: 'measureReviewStatus', label: '测量数据审核状态', minWidth: 142, slotName: 'measureReviewStatus' },
|
||||||
@@ -307,6 +309,24 @@ function closeDetail() { resetDetail() }
|
|||||||
function openWorkspace(row: InboundOrderSummary) {
|
function openWorkspace(row: InboundOrderSummary) {
|
||||||
void router.push({ name: 'inbound-order-detail', params: { inboundId: row.id }, query: { tenantId: row.tenantId } })
|
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 formatNumber(value: number | null, digits = 3) { return value === null ? '—' : value.toFixed(digits) }
|
||||||
function formatAggregate(value: number, digits = 3) {
|
function formatAggregate(value: number, digits = 3) {
|
||||||
if (!Number.isFinite(value)) return '0'
|
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="入库单列表">
|
<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-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 #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 #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 #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>
|
<template #measureReviewStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).measureReviewStatus, measureReviewLabels) }}</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user