优化
This commit is contained in:
@@ -1,12 +1,19 @@
|
|||||||
import { apiRequest } from '@/api/http'
|
import { apiRequest } from '@/api/http'
|
||||||
|
|
||||||
export interface RelabelQuantity {
|
export interface RelabelQuantity {
|
||||||
|
sourcePalletCount?: number | string | null
|
||||||
|
targetPalletCount?: number | string | null
|
||||||
|
sourceUnitCounts?: Record<string, number | string> | null
|
||||||
labelPieceCount: number | string | null
|
labelPieceCount: number | string | null
|
||||||
cartonCount: number | string | null
|
cartonCount: number | string | null
|
||||||
skuCount: number | string
|
skuCount: number | string
|
||||||
duplicateCarton: boolean
|
duplicateCarton: boolean
|
||||||
}
|
}
|
||||||
export interface RelabelQuantityTotals {
|
export interface RelabelQuantityTotals {
|
||||||
|
knownSourcePalletCount?: number | string
|
||||||
|
unknownSourcePalletOrders?: number | string
|
||||||
|
knownTargetPalletCount?: number | string
|
||||||
|
unknownTargetPalletOrders?: number | string
|
||||||
orderCount: number | string
|
orderCount: number | string
|
||||||
skuCount: number | string
|
skuCount: number | string
|
||||||
knownLabelPieceCount: number | string
|
knownLabelPieceCount: number | string
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { apiRequest, apiBlobRequest } from '@/api/http'
|
|||||||
import type { PageResult } from './warehouseServices'
|
import type { PageResult } from './warehouseServices'
|
||||||
|
|
||||||
export interface ReturnOrderSummary {
|
export interface ReturnOrderSummary {
|
||||||
|
remainingQuantity?: number
|
||||||
|
receivedBoxCount?: number
|
||||||
|
receivedParcelCount?: number
|
||||||
|
receivedPalletCount?: number
|
||||||
relabelApplications?: Array<{ relabelNo: string; status: string; quantity: number; quantityUnit?: string; createdAt: string }>
|
relabelApplications?: Array<{ relabelNo: string; status: string; quantity: number; quantityUnit?: string; createdAt: string }>
|
||||||
returnNo: string; overseasWarehouseId: string; warehouseContactName?: string; warehouseContactPhone?: string; customerReferenceNo?: string; status: string
|
returnNo: string; overseasWarehouseId: string; warehouseContactName?: string; warehouseContactPhone?: string; customerReferenceNo?: string; status: string
|
||||||
skuTypes: number; expectedQuantity: number; receivedQuantity: number; needRelabel: boolean; relabelNo?: string; relabelStatus?: string; createdAt: string
|
skuTypes: number; expectedQuantity: number; receivedQuantity: number; needRelabel: boolean; relabelNo?: string; relabelStatus?: string; createdAt: string
|
||||||
@@ -30,6 +34,13 @@ export interface ReturnOrderCreate {
|
|||||||
export function listReturnOrders(params: Record<string, unknown>) {
|
export function listReturnOrders(params: Record<string, unknown>) {
|
||||||
return apiRequest<PageResult<ReturnOrderSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/return-orders', params })
|
return apiRequest<PageResult<ReturnOrderSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/return-orders', params })
|
||||||
}
|
}
|
||||||
|
export interface ReturnOrderStatistics {
|
||||||
|
totals: { orderCount: number; skuTypes: number; expectedQuantity: number; receivedQuantity: number; remainingQuantity: number; receivedBoxCount: number; receivedParcelCount: number; receivedPalletCount: number }
|
||||||
|
pageDimensions: Array<{ returnNo: string; remainingQuantity: number; receivedBoxCount: number; receivedParcelCount: number; receivedPalletCount: number }>
|
||||||
|
}
|
||||||
|
export function getReturnOrderStatistics(params: Record<string, unknown>) {
|
||||||
|
return apiRequest<ReturnOrderStatistics>({ method: 'GET', url: '/merchant/v1/last-leg/return-orders/statistics', params })
|
||||||
|
}
|
||||||
export function getReturnOrder(returnNo: string) {
|
export function getReturnOrder(returnNo: string) {
|
||||||
return apiRequest<ReturnOrderDetail>({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}` })
|
return apiRequest<ReturnOrderDetail>({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}` })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { apiRequest } from '@/api/http'
|
||||||
|
|
||||||
|
export interface TransferQuantityTotals {
|
||||||
|
orderCount: number | string
|
||||||
|
declaredPieceCount: number | string | null
|
||||||
|
knownDeclaredPieceCount: number | string
|
||||||
|
unknownDeclaredOrderCount: number | string
|
||||||
|
receivedPieceCount: number | string | null
|
||||||
|
knownReceivedPieceCount: number | string
|
||||||
|
unknownReceivedOrderCount: number | string
|
||||||
|
declaredCartonCount: number | string
|
||||||
|
receivedCartonCount: number | string
|
||||||
|
receivedPalletCount: number | string
|
||||||
|
availableCartonCount: number | string
|
||||||
|
shippedCartonCount: number | string
|
||||||
|
remainingPalletCount: number | string
|
||||||
|
shippedPalletCount: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 客户范围由会话决定,汇总使用本次列表完整筛选。 */
|
||||||
|
export function getTransferQuantities(params: Record<string, unknown>) {
|
||||||
|
return apiRequest<TransferQuantityTotals>({ method: 'GET', url: '/merchant/v1/last-leg/warehouse-transfers/quantities', params })
|
||||||
|
}
|
||||||
@@ -25,8 +25,8 @@ export interface RelabelDetail {
|
|||||||
tailOrders?: Array<{ id: string; orderNo: string; overseasWarehouseId: string; packageCount: number; status: string }>
|
tailOrders?: Array<{ id: string; orderNo: string; overseasWarehouseId: string; packageCount: number; status: string }>
|
||||||
completionEvidence?: { taskId: string; photoFileIds: string[]; completionRemark?: string; reviewerName?: string; reviewRemark?: string; completedAt?: string; customerConfirmedAt?: string }
|
completionEvidence?: { taskId: string; photoFileIds: string[]; completionRemark?: string; reviewerName?: string; reviewRemark?: string; completedAt?: string; customerConfirmedAt?: string }
|
||||||
}
|
}
|
||||||
export interface DiscardSummary { id: string; discardNo: string; outboundNo?: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string; completedAt?: string; customerConfirmedAt?: string }
|
export interface DiscardSummary { id: string; discardNo: string; outboundNo?: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string; completedAt?: string; customerConfirmedAt?: string; skuCodes: string[]; skuCount: number; requestedPackages: number; actualPackages: number | null }
|
||||||
export interface DiscardBox { cartonId: string; cartonMark: string; palletNo?: string; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> }
|
export interface DiscardBox { cartonId: string; cartonMark: string; palletNo?: string; skus: Array<{ skuCode: string; productName: string; availablePackages: number; availablePieces?: number | null }> }
|
||||||
export interface DiscardCandidate { cartons?: DiscardBox[]; cartonSelectionSupported?: boolean; cartonSelectionBlockReason?: string; orderId: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'INVENTORY'; inventoryPositionId?: string; palletNo?: string; returnCartonId?: string; cartonMark?: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> }
|
export interface DiscardCandidate { cartons?: DiscardBox[]; cartonSelectionSupported?: boolean; cartonSelectionBlockReason?: string; orderId: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'INVENTORY'; inventoryPositionId?: string; palletNo?: string; returnCartonId?: string; cartonMark?: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> }
|
||||||
export interface RelabelInventoryCandidate {
|
export interface RelabelInventoryCandidate {
|
||||||
cartonSkuItems: Array<{ skuCode: string; quantity: number }>; cartonAvailablePackages: number | string
|
cartonSkuItems: Array<{ skuCode: string; quantity: number }>; cartonAvailablePackages: number | string
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ watch(() => [props.query, props.relabelNo], async () => {
|
|||||||
<span>{{ data.orderCount }} 票</span><span>{{ data.skuCount }} 种 SKU</span>
|
<span>{{ data.orderCount }} 票</span><span>{{ data.skuCount }} 种 SKU</span>
|
||||||
<span>贴标件数:{{ dimension(Number(data.knownLabelPieceCount), Number(data.unknownPieceOrders), '件') }}</span>
|
<span>贴标件数:{{ dimension(Number(data.knownLabelPieceCount), Number(data.unknownPieceOrders), '件') }}</span>
|
||||||
<span>箱数:{{ dimension(Number(data.knownCartonCount), Number(data.unknownCartonOrders), '箱') }}</span>
|
<span>箱数:{{ dimension(Number(data.knownCartonCount), Number(data.unknownCartonOrders), '箱') }}</span>
|
||||||
|
<span>原托盘:{{ dimension(Number(data.knownSourcePalletCount ?? 0), Number(data.unknownSourcePalletOrders ?? data.orderCount), '托') }}</span>
|
||||||
|
<span>本轮目标托盘:{{ dimension(Number(data.knownTargetPalletCount ?? 0), Number(data.unknownTargetPalletOrders ?? data.orderCount), '托') }}</span>
|
||||||
<p v-if="relabelNo && data.orders[relabelNo]?.duplicateCarton" role="status">存在重复箱唛关联,箱数已去重,贴标件数待核查。</p>
|
<p v-if="relabelNo && data.orders[relabelNo]?.duplicateCarton" role="status">存在重复箱唛关联,箱数已去重,贴标件数待核查。</p>
|
||||||
</template>
|
</template>
|
||||||
<span v-else>数量加载中…</span>
|
<span v-else>数量加载中…</span>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { getTransferQuantities, type TransferQuantityTotals } from '../api/transferQuantities'
|
||||||
|
const props = defineProps<{ query: Record<string, unknown> }>()
|
||||||
|
const data = ref<TransferQuantityTotals>()
|
||||||
|
const error = ref('')
|
||||||
|
let generation = 0
|
||||||
|
watch(() => JSON.stringify(props.query), async () => {
|
||||||
|
const current = ++generation
|
||||||
|
data.value = undefined; error.value = ''
|
||||||
|
try { const result = await getTransferQuantities(props.query); if (current === generation) data.value = result }
|
||||||
|
catch { if (current === generation) error.value = '转运数量汇总加载失败,请重新查询' }
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<section class="transfer-quantity-panel" aria-label="仓储转运全筛选汇总">
|
||||||
|
<p v-if="error" role="alert">{{ error }}</p>
|
||||||
|
<template v-else-if="data">
|
||||||
|
<strong>全部筛选结果 · {{ data.orderCount }} 票</strong>
|
||||||
|
<span>预报:{{ data.declaredPieceCount ?? `未记录(已知 ${data.knownDeclaredPieceCount} 件,${data.unknownDeclaredOrderCount} 票未知)` }}{{ data.declaredPieceCount == null ? '' : ' 件' }} / {{ data.declaredCartonCount }} 箱</span>
|
||||||
|
<span>历史实收:{{ data.receivedPieceCount ?? `未记录(已知 ${data.knownReceivedPieceCount} 件,${data.unknownReceivedOrderCount} 票未知)` }}{{ data.receivedPieceCount == null ? '' : ' 件' }} / {{ data.receivedCartonCount }} 箱 / {{ data.receivedPalletCount }} 托</span>
|
||||||
|
<span>当前已清点 {{ data.availableCartonCount }} 箱 · 剩余 {{ data.remainingPalletCount }} 托 · 已出库 {{ data.shippedCartonCount }} 箱 / {{ data.shippedPalletCount }} 托(托盘可重叠)</span>
|
||||||
|
</template>
|
||||||
|
<span v-else>转运数量加载中…</span>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.transfer-quantity-panel { display: flex; flex-wrap: wrap; gap: 12px; padding: 12px 16px; margin-bottom: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface, white); }
|
||||||
|
.transfer-quantity-panel p { margin: 0; }
|
||||||
|
</style>
|
||||||
@@ -49,3 +49,14 @@ test('相同仓位重复关联只计一次,零值与未知可用量不进入
|
|||||||
assert.equal(groupDiscardDocuments([source, source])[0].availablePackages, 2)
|
assert.equal(groupDiscardDocuments([source, source])[0].availablePackages, 2)
|
||||||
for (const availablePackages of [0, null, undefined, 'invalid']) assert.equal(groupDiscardDocuments([{ ...source, availablePackages }]).length, 0)
|
for (const availablePackages of [0, null, undefined, 'invalid']) assert.equal(groupDiscardDocuments([{ ...source, availablePackages }]).length, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('相同来源的混箱分片保留全部SKU和实箱,且不修改接口原始对象', () => {
|
||||||
|
const a = { ...returned('1'), sourceType: 'INVENTORY', inventoryPositionId: 'P', skus: [{ skuCode: 'A', availablePackages: 1 }] }
|
||||||
|
const b = { ...a, skus: [{ skuCode: 'B', availablePackages: 1 }], cartons: [{ ...a.cartons[0], skus: [{ skuCode: 'B', availablePackages: 1 }] }] }
|
||||||
|
const doc = groupDiscardDocuments([a, b])[0]
|
||||||
|
assert.equal(doc.availablePackages, 1)
|
||||||
|
assert.equal(doc.sources.length, 1)
|
||||||
|
assert.deepEqual(doc.sources[0].skus.map(sku => sku.skuCode), ['A', 'B'])
|
||||||
|
assert.deepEqual(doc.sources[0].cartons[0].skus.map(sku => sku.skuCode), ['A', 'B'])
|
||||||
|
assert.equal(a.skus.length, 1)
|
||||||
|
})
|
||||||
|
|||||||
@@ -7,15 +7,28 @@ export interface DiscardDocument extends DiscardCandidate {
|
|||||||
/** 下拉按来源单据和操作仓汇总;原始库存身份保留给选箱与提交。 */
|
/** 下拉按来源单据和操作仓汇总;原始库存身份保留给选箱与提交。 */
|
||||||
export function groupDiscardDocuments(candidates: DiscardCandidate[]): DiscardDocument[] {
|
export function groupDiscardDocuments(candidates: DiscardCandidate[]): DiscardDocument[] {
|
||||||
const groups = new Map<string, DiscardDocument>()
|
const groups = new Map<string, DiscardDocument>()
|
||||||
const identities = new Set<string>()
|
const identities = new Map<string, DiscardCandidate>()
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const available = Number(candidate.availablePackages)
|
const available = Number(candidate.availablePackages)
|
||||||
if (candidate.availablePackages == null || !Number.isFinite(available) || available <= 0) continue
|
if (candidate.availablePackages == null || !Number.isFinite(available) || available <= 0) continue
|
||||||
// 按真实来源身份去重;缺身份的旧接口行仍独立保留,不合并不同库存。
|
// 按真实来源身份去重;缺身份的旧接口行仍独立保留,不合并不同库存。
|
||||||
const identity = candidate.sourceType === 'RETURN_CARTON' ? candidate.returnCartonId : candidate.inventoryPositionId || candidate.orderId
|
const identity = candidate.sourceType === 'RETURN_CARTON' ? candidate.returnCartonId : candidate.inventoryPositionId || candidate.orderId
|
||||||
const sourceKey = identity ? JSON.stringify([candidate.overseasWarehouseId, candidate.sourceType, identity]) : null
|
const sourceKey = identity ? JSON.stringify([candidate.overseasWarehouseId, candidate.sourceType, identity]) : null
|
||||||
if (sourceKey && identities.has(sourceKey)) continue
|
const existing = sourceKey ? identities.get(sourceKey) : undefined
|
||||||
if (sourceKey) identities.add(sourceKey)
|
if (existing) {
|
||||||
|
// 同一仓位的分片响应合并 SKU 和箱,重复关联不会丢掉箱内商品。
|
||||||
|
existing.skus = [...new Map([...existing.skus, ...candidate.skus].map(sku => [sku.skuCode, sku])).values()]
|
||||||
|
const cartons = new Map((existing.cartons || []).map(box => [box.cartonId, box]))
|
||||||
|
for (const box of candidate.cartons || []) {
|
||||||
|
const old = cartons.get(box.cartonId)
|
||||||
|
cartons.set(box.cartonId, old ? { ...old, skus: [...new Map([...old.skus, ...box.skus].map(sku => [sku.skuCode, sku])).values()] } : { ...box })
|
||||||
|
}
|
||||||
|
existing.cartons = [...cartons.values()]
|
||||||
|
existing.availablePackages = Math.max(Number(existing.availablePackages), available)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const source = { ...candidate, skus: [...candidate.skus], cartons: candidate.cartons?.map(box => ({ ...box, skus: [...box.skus] })) }
|
||||||
|
if (sourceKey) identities.set(sourceKey, source)
|
||||||
const documentNo = candidate.inboundOrderNo || candidate.inboundCode
|
const documentNo = candidate.inboundOrderNo || candidate.inboundCode
|
||||||
const key = JSON.stringify([candidate.overseasWarehouseId, candidate.sourceType, documentNo || candidate.orderId])
|
const key = JSON.stringify([candidate.overseasWarehouseId, candidate.sourceType, documentNo || candidate.orderId])
|
||||||
let document = groups.get(key)
|
let document = groups.get(key)
|
||||||
@@ -23,7 +36,7 @@ export function groupDiscardDocuments(candidates: DiscardCandidate[]): DiscardDo
|
|||||||
document = { ...candidate, orderId: key, sources: [], availablePackages: 0 }
|
document = { ...candidate, orderId: key, sources: [], availablePackages: 0 }
|
||||||
groups.set(key, document)
|
groups.set(key, document)
|
||||||
}
|
}
|
||||||
document.sources.push(candidate)
|
document.sources.push(source)
|
||||||
}
|
}
|
||||||
for (const document of groups.values()) {
|
for (const document of groups.values()) {
|
||||||
const boxes = new Set<string>()
|
const boxes = new Set<string>()
|
||||||
|
|||||||
@@ -6,6 +6,45 @@ import ts from 'typescript'
|
|||||||
const view = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
|
const view = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
|
||||||
const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'utf8')
|
const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'utf8')
|
||||||
|
|
||||||
|
function listHarness(list, summary) {
|
||||||
|
const state = { query: { page: 1, size: 1, skuCode: 'FILTER' }, loading: { value: false }, listError: { value: '' }, statistics: { value: undefined }, rows: { value: [] }, total: { value: 0 } }
|
||||||
|
const source = view.slice(view.indexOf('async function load()'), view.indexOf('\nfunction changePage('))
|
||||||
|
const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||||
|
const load = new Function(...Object.keys(state), 'listReturnOrders', 'getReturnOrderStatistics', `let listRequest=0; ${js}; return load`)(...Object.values(state), list, summary)
|
||||||
|
return { ...state, load }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('全筛选统计使用服务器总计,分页行只按单号映射载体,不重算SKU', async () => {
|
||||||
|
const calls = []
|
||||||
|
const totals = { orderCount: 3, skuTypes: 3, expectedQuantity: 19, receivedQuantity: 14, remainingQuantity: 5 }
|
||||||
|
const h = listHarness(async p => { calls.push(p); return { total: '3', records: [{ returnNo: 'RT-1', receivedQuantity: 8 }] } }, async p => { calls.push(p); return { totals, pageDimensions: [{ returnNo: 'RT-1', receivedParcelCount: 1, receivedBoxCount: 1 }] } })
|
||||||
|
await h.load()
|
||||||
|
assert.deepEqual(h.statistics.value, totals)
|
||||||
|
assert.equal(h.rows.value[0].receivedParcelCount, 1)
|
||||||
|
h.query.page = 2
|
||||||
|
await h.load()
|
||||||
|
assert.equal(h.statistics.value.receivedQuantity, 14)
|
||||||
|
assert.deepEqual(calls[2], calls[3])
|
||||||
|
assert.equal(calls[3].page, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('统计失败清空旧列表和汇总,并忽略较旧查询的迟到结果', async () => {
|
||||||
|
let fail = false, finish
|
||||||
|
const h = listHarness(async () => ({ total: 1, records: [{ returnNo: 'RT-1' }] }), async p => {
|
||||||
|
if (fail) throw Error('统计服务不可用')
|
||||||
|
if (p.page === 1) return new Promise(resolve => { finish = resolve })
|
||||||
|
return { totals: { orderCount: 2 }, pageDimensions: [] }
|
||||||
|
})
|
||||||
|
const old = h.load(); h.query.page = 2; await h.load()
|
||||||
|
finish({ totals: { orderCount: 999 }, pageDimensions: [] }); await old
|
||||||
|
assert.equal(h.statistics.value.orderCount, 2)
|
||||||
|
fail = true; await h.load()
|
||||||
|
assert.equal(h.statistics.value, undefined)
|
||||||
|
assert.deepEqual(h.rows.value, [])
|
||||||
|
assert.equal(h.listError.value, '统计服务不可用')
|
||||||
|
assert.equal(h.loading.value, false)
|
||||||
|
})
|
||||||
|
|
||||||
function submitHarness(failure) {
|
function submitHarness(failure) {
|
||||||
const source = view.slice(view.indexOf('async function submit()'), view.indexOf('\nfunction statusType('))
|
const source = view.slice(view.indexOf('async function submit()'), view.indexOf('\nfunction statusType('))
|
||||||
const state = { requests: [], loads: 0, routes: [], errors: [], successes: [] }
|
const state = { requests: [], loads: 0, routes: [], errors: [], successes: [] }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-l
|
|||||||
import { createIdempotencyKey } from '@/utils/idempotencyKey'
|
import { createIdempotencyKey } from '@/utils/idempotencyKey'
|
||||||
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||||
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
|
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
|
||||||
|
import { getReturnOrderStatistics, type ReturnOrderStatistics } from '../api/returnOrders'
|
||||||
import { getReturnInventoryLabel } from '../api/returnInventory'
|
import { getReturnInventoryLabel } from '../api/returnInventory'
|
||||||
import { formatReturnVolume, receiptBoxDetails, receiptPhysicalDetails, receiptPrintableBoxCount, receiptTrackingNo } from '../utils/returnReceiptDisplay'
|
import { formatReturnVolume, receiptBoxDetails, receiptPhysicalDetails, receiptPrintableBoxCount, receiptTrackingNo } from '../utils/returnReceiptDisplay'
|
||||||
|
|
||||||
@@ -40,6 +41,9 @@ const printableBoxCount = computed(() => receiptPrintableBoxCount(printableRecei
|
|||||||
const photoUrls = reactive<Record<string, string>>({})
|
const photoUrls = reactive<Record<string, string>>({})
|
||||||
const rows = ref<ReturnOrderSummary[]>([])
|
const rows = ref<ReturnOrderSummary[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
const statistics = ref<ReturnOrderStatistics['totals']>()
|
||||||
|
const listError = ref('')
|
||||||
|
let listRequest = 0
|
||||||
const operationWarehouses = ref<MerchantWarehouse[]>([])
|
const operationWarehouses = ref<MerchantWarehouse[]>([])
|
||||||
const operationWarehousesLoading = ref(false)
|
const operationWarehousesLoading = ref(false)
|
||||||
const operationWarehousesError = ref('')
|
const operationWarehousesError = ref('')
|
||||||
@@ -62,8 +66,13 @@ const tableColumns: DataTableColumn[] = [
|
|||||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 220, fixed: 'left' },
|
{ key: 'actions', label: '操作', slotName: 'actions', width: 220, fixed: 'left' },
|
||||||
{ prop: 'returnNo', label: '退货单号', minWidth: 190 }, { prop: 'customerReferenceNo', label: '客户参考号', minWidth: 160 },
|
{ prop: 'returnNo', label: '退货单号', minWidth: 190 }, { prop: 'customerReferenceNo', label: '客户参考号', minWidth: 160 },
|
||||||
{ key: 'warehouse', label: '操作仓', slotName: 'warehouse', minWidth: 200 }, { key: 'relabel', label: '换标', slotName: 'relabel', width: 100 },
|
{ key: 'warehouse', label: '操作仓', slotName: 'warehouse', minWidth: 200 }, { key: 'relabel', label: '换标', slotName: 'relabel', width: 100 },
|
||||||
{ prop: 'skuTypes', label: 'SKU 种类', width: 105 }, { prop: 'expectedQuantity', label: '预报数量', width: 110 },
|
{ prop: 'skuTypes', label: 'SKU 种类', width: 105 }, { prop: 'expectedQuantity', label: '预报(件)', width: 110 },
|
||||||
{ prop: 'receivedQuantity', label: '实收数量', width: 110 }, { key: 'status', label: '状态', slotName: 'status', width: 150 },
|
{ prop: 'receivedQuantity', label: '实收(件)', width: 110 },
|
||||||
|
{ prop: 'remainingQuantity', label: '待收(件)', width: 110 },
|
||||||
|
{ prop: 'receivedBoxCount', label: '签收箱数', width: 110 },
|
||||||
|
{ prop: 'receivedParcelCount', label: '签收包裹数', width: 115 },
|
||||||
|
{ prop: 'receivedPalletCount', label: '签收托数', width: 110 },
|
||||||
|
{ key: 'status', label: '状态', slotName: 'status', width: 150 },
|
||||||
{ key: 'relabelApplications', label: '换标子单', slotName: 'relabelApplications', minWidth: 340 },
|
{ key: 'relabelApplications', label: '换标子单', slotName: 'relabelApplications', minWidth: 340 },
|
||||||
{ key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 180 },
|
{ key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 180 },
|
||||||
]
|
]
|
||||||
@@ -147,9 +156,21 @@ async function openEdit(returnNo: string) {
|
|||||||
function addItem() { form.items.push(emptySku()) }
|
function addItem() { form.items.push(emptySku()) }
|
||||||
function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) }
|
function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) }
|
||||||
async function load() {
|
async function load() {
|
||||||
|
const request = ++listRequest
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try { const response = await listReturnOrders({ ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }); rows.value = response.records; total.value = Number(response.total); if (!Number.isFinite(total.value)) total.value = response.records.length }
|
listError.value = ''; statistics.value = undefined
|
||||||
finally { loading.value = false }
|
try {
|
||||||
|
const params = { ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }
|
||||||
|
const [response, summary] = await Promise.all([listReturnOrders(params), getReturnOrderStatistics(params)])
|
||||||
|
if (request !== listRequest) return
|
||||||
|
const dimensions = new Map(summary.pageDimensions.map(row => [row.returnNo, row]))
|
||||||
|
rows.value = response.records.map(row => ({ ...row, ...dimensions.get(row.returnNo) }))
|
||||||
|
total.value = Number(response.total); statistics.value = summary.totals
|
||||||
|
} catch (error) {
|
||||||
|
if (request !== listRequest) return
|
||||||
|
rows.value = []; total.value = 0
|
||||||
|
listError.value = error instanceof Error ? error.message : '退货列表及汇总加载失败'
|
||||||
|
} finally { if (request === listRequest) loading.value = false }
|
||||||
}
|
}
|
||||||
function changePage(page: number) {
|
function changePage(page: number) {
|
||||||
if (page === query.page) return
|
if (page === query.page) return
|
||||||
@@ -323,6 +344,11 @@ onBeforeUnmount(clearPhotoUrls)
|
|||||||
|
|
||||||
<template v-else-if="!props.detailPage">
|
<template v-else-if="!props.detailPage">
|
||||||
<QueryToolbar v-model="queryModel" class="return-query" layout="grid" ariaLabel="退货签收筛选" :fields="queryFields" @search="search" />
|
<QueryToolbar v-model="queryModel" class="return-query" layout="grid" ariaLabel="退货签收筛选" :fields="queryFields" @search="search" />
|
||||||
|
<el-alert v-if="listError" type="error" :closable="false" :title="listError"><el-button @click="load">重新加载</el-button></el-alert>
|
||||||
|
<div v-if="statistics && !loading" class="return-filter-summary" role="status" aria-label="退货全筛选汇总">
|
||||||
|
全筛选:{{ statistics.orderCount }} 票 · {{ statistics.skuTypes }} 种 SKU · 预报 {{ statistics.expectedQuantity }} 件 · 实收 {{ statistics.receivedQuantity }} 件 · 待收 {{ statistics.remainingQuantity }} 件 · 签收 {{ statistics.receivedBoxCount }} 箱 / {{ statistics.receivedParcelCount }} 包裹 / {{ statistics.receivedPalletCount }} 托
|
||||||
|
<span>预报箱/包裹/托数:未提供</span>
|
||||||
|
</div>
|
||||||
<div class="return-table-card">
|
<div class="return-table-card">
|
||||||
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="rows" :columns="tableColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="rowActions" row-key="returnNo" empty-text="暂无退货预报">
|
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="rows" :columns="tableColumns" :loading="loading" :total="total" :page-sizes="[15, 30, 50, 100]" :row-actions="rowActions" row-key="returnNo" empty-text="暂无退货预报">
|
||||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新建退货预报</el-button></template>
|
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新建退货预报</el-button></template>
|
||||||
@@ -364,7 +390,7 @@ onBeforeUnmount(clearPhotoUrls)
|
|||||||
<section class="return-detail-section">
|
<section class="return-detail-section">
|
||||||
<h3>签收概览</h3>
|
<h3>签收概览</h3>
|
||||||
<div class="return-detail-grid">
|
<div class="return-detail-grid">
|
||||||
<div><span>实收 / 预报</span><strong>{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}</strong></div>
|
<div><span>预报(件)</span><strong>{{ selected.order.expectedQuantity }}</strong></div><div><span>实收(件)</span><strong>{{ selected.order.receivedQuantity }}</strong></div><div><span>待收(件)</span><strong>{{ Math.max(selected.order.expectedQuantity - selected.order.receivedQuantity, 0) }}</strong></div>
|
||||||
<div><span>SKU 种类</span><strong>{{ selected.order.skuTypes }}</strong></div>
|
<div><span>SKU 种类</span><strong>{{ selected.order.skuTypes }}</strong></div>
|
||||||
<div><span>是否换标</span><strong>{{ selected.order.needRelabel ? '需要换标' : '否' }}</strong></div>
|
<div><span>是否换标</span><strong>{{ selected.order.needRelabel ? '需要换标' : '否' }}</strong></div>
|
||||||
<div><span>客户参考号</span><strong>{{ selected.order.customerReferenceNo || '—' }}</strong></div>
|
<div><span>客户参考号</span><strong>{{ selected.order.customerReferenceNo || '—' }}</strong></div>
|
||||||
@@ -475,6 +501,8 @@ onBeforeUnmount(clearPhotoUrls)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.return-filter-summary { padding:12px 16px; margin-bottom:12px; border:1px solid var(--client-border); border-radius:8px; background:var(--client-surface,#fff); line-height:1.8; }
|
||||||
|
.return-filter-summary span { margin-left:16px; color:var(--client-text-secondary,#64748b); }
|
||||||
.return-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(4,minmax(0,280px)) max-content; gap:14px 18px; align-items:end; }
|
.return-query.query-toolbar--grid { display:grid; grid-template-columns:repeat(4,minmax(0,280px)) max-content; gap:14px 18px; align-items:end; }
|
||||||
.return-query :deep(.query-toolbar__filters) { display:contents; }
|
.return-query :deep(.query-toolbar__filters) { display:contents; }
|
||||||
.return-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
|
.return-query :deep(.query-toolbar__field) { display:flex; flex-direction:column; align-items:stretch; gap:6px; }
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -28,7 +28,7 @@ test('废弃来源包含通用仓位,选择后带出全部库存且不把选
|
|||||||
function selectionRows(boxes, selectedKeys) {
|
function selectionRows(boxes, selectedKeys) {
|
||||||
const body = view.match(/function syncDiscardSelection\(\) \{([\s\S]*?)\n\}/)[1].replace('new Map<string, CargoRow>()', 'new Map()')
|
const body = view.match(/function syncDiscardSelection\(\) \{([\s\S]*?)\n\}/)[1].replace('new Map<string, CargoRow>()', 'new Map()')
|
||||||
const rows = { value: [] }
|
const rows = { value: [] }
|
||||||
new Function('discardSelectableBoxes', 'discardSelectedBoxKeys', 'cargoRows', 'createCargoRow', body)({ value: boxes }, { value: selectedKeys }, rows, () => ({}))
|
new Function('discardSourceBoxes', 'discardSelectedBoxKeys', 'cargoRows', 'createCargoRow', body)({ value: boxes }, { value: selectedKeys }, rows, () => ({}))
|
||||||
return rows.value
|
return rows.value
|
||||||
}
|
}
|
||||||
test('部分废弃只提交勾选的箱子,数量由箱子汇总,清空后无明细', () => {
|
test('部分废弃只提交勾选的箱子,数量由箱子汇总,清空后无明细', () => {
|
||||||
@@ -48,3 +48,12 @@ test('混合SKU箱按实物箱只计一次,退货箱仍整箱携带全部SKU',
|
|||||||
const returnRows = selectionRows([{ key: 'RETURN_CARTON:2', cartonId: '2', source: returned, skus }], ['RETURN_CARTON:2'])
|
const returnRows = selectionRows([{ key: 'RETURN_CARTON:2', cartonId: '2', source: returned, skus }], ['RETURN_CARTON:2'])
|
||||||
assert.deepEqual(returnRows.map(row => row.quantity), [2, 3]); assert.ok(returnRows.every(row => row.returnCartonId === '2' && row.discardCartonIds === undefined))
|
assert.deepEqual(returnRows.map(row => row.quantity), [2, 3]); assert.ok(returnRows.every(row => row.returnCartonId === '2' && row.discardCartonIds === undefined))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('通用混箱同仓位保留全部SKU,跨仓位相同SKU保持独立来源', () => {
|
||||||
|
const skus = [{ skuCode: 'A', availablePackages: 1 }, { skuCode: 'B', availablePackages: 1 }]
|
||||||
|
const source = { orderId: null, inventoryPositionId: '42', sourceType: 'INVENTORY', availablePackages: 1, skus }
|
||||||
|
const boxes = [{ key: 'INVENTORY:1', cartonId: '1', source, skus }, { key: 'INVENTORY:2', cartonId: '2', source: { ...source, inventoryPositionId: '43' }, skus: [skus[0]] }]
|
||||||
|
const rows = selectionRows(boxes, ['INVENTORY:1', 'INVENTORY:2'])
|
||||||
|
assert.equal(rows.length, 3)
|
||||||
|
assert.deepEqual(rows.map(row => [row.sourceInventoryPositionId, row.sku, row.quantity, row.discardCartonIds]), [['42', 'A', 1, ['1']], ['42', 'B', 1, ['1']], ['43', 'A', 1, ['2']]])
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user