优化
This commit is contained in:
@@ -157,6 +157,16 @@ export async function changeRelabelOrderStatus(relabelNo: string, status: string
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function getRelabelReviewTask(relabelNo: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<{ version: number; taskStatus: string }>, ApiEnvelope<{ version: number; taskStatus: string }>>(`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/review-task`)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function returnRelabelForRework(relabelNo: string, expectedVersion: number, remark: string, idempotencyKey: string) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/rework`, { decision: 'REWORK', expectedVersion, remark, idempotencyKey })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function uploadRelabelPdf(relabelNo: string, file: File, scope: 'TOTAL' | 'ITEM', itemId?: string) {
|
||||
const body = new FormData()
|
||||
body.append('file', file)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatOverseasDateTime, inventoryBusinessTypeLabel, lastMileOrderReference, toLastMileOrderListItem } from './lastMilePresentation'
|
||||
import { formatOverseasDateTime, inventoryBusinessTypeLabel, lastMileOrderReference, relabelBusinessStatusLabel, toLastMileOrderListItem } from './lastMilePresentation'
|
||||
|
||||
describe('last-mile order presentation adapter', () => {
|
||||
it('formats overseas timestamps with zero-padded seconds', () => {
|
||||
@@ -89,6 +89,7 @@ describe('last-mile order presentation adapter', () => {
|
||||
['WAREHOUSE_TRANSFER', '仓储转运'],
|
||||
['RETURN_RELABEL', '退货换标'],
|
||||
['LOCAL_LOOSEBOARD', '本土散板'],
|
||||
['CARGO_DISCARD', '货物废弃订单'],
|
||||
])('maps %s to its Chinese display name', (orderType, expected) => {
|
||||
const row = toLastMileOrderListItem({ id: '1', customerId: '100', orderNo: 'LL-1', orderType })
|
||||
expect(row.orderType).toBe(expected)
|
||||
@@ -102,4 +103,14 @@ describe('last-mile order presentation adapter', () => {
|
||||
|
||||
expect(row.palletMergeRelation).toBe('NYEXX2608280001、NYEXX2608280002')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['PENDING_PROCESSING', '待处理'],
|
||||
['PROCESSING', '处理中'],
|
||||
['CUSTOMER_CONFIRM_PENDING', '待处理'],
|
||||
['READY_FOR_OUTBOUND', '换标完成'],
|
||||
['COMPLETED', '换标完成'],
|
||||
])('collapses internal relabel node %s into business status %s', (status, expected) => {
|
||||
expect(relabelBusinessStatusLabel(status)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,11 +8,14 @@ const orderTypeLabels: Record<string, string> = {
|
||||
WAREHOUSE_TRANSFER: '仓储转运',
|
||||
RETURN_RELABEL: '退货换标',
|
||||
LOCAL_LOOSEBOARD: '本土散板',
|
||||
CARGO_DISCARD: '货物废弃订单',
|
||||
REJECTED: '拒收订单',
|
||||
EXPRESS: '快递订单',
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
PENDING_REVIEW: '待审核',
|
||||
APPROVED: '审核通过',
|
||||
PENDING_ASSIGNMENT: '待分配',
|
||||
DESTINATION_ASSIGNED: '已分配目的仓',
|
||||
INBOUND_PLANNED: '已生成入库计划',
|
||||
@@ -58,6 +61,14 @@ export function lastMileStatusLabel(value: unknown) {
|
||||
return statusLabels[code] || text(code)
|
||||
}
|
||||
|
||||
// 认领后显示处理中,可出库节点统一显示换标完成。
|
||||
export function relabelBusinessStatusLabel(value: unknown) {
|
||||
const code = String(value ?? '').trim()
|
||||
if (code === 'PROCESSING') return '处理中'
|
||||
if (!code || code === 'NOT_REQUIRED') return '—'
|
||||
return ['READY_FOR_OUTBOUND', 'COMPLETED'].includes(code) ? '换标完成' : '待处理'
|
||||
}
|
||||
|
||||
export function lastMileOrderTypeLabel(value: unknown) {
|
||||
const code = String(value ?? '').trim()
|
||||
return orderTypeLabels[code] || text(code)
|
||||
@@ -135,7 +146,7 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO
|
||||
outboundOrderNo: text(dto.outboundOrderNo),
|
||||
dispatchOrderNo: text(dto.deliveryOrderNo),
|
||||
relabelOrderNo: text(dto.relabelOrderNo),
|
||||
relabelStatus: lastMileStatusLabel(dto.relabelStatus),
|
||||
relabelStatus: relabelBusinessStatusLabel(dto.relabelStatus),
|
||||
consolidated: palletMergeLabel(dto.palletMergeFlag),
|
||||
palletMergeFlag: String(dto.palletMergeFlag ?? '').trim(),
|
||||
palletMergeRelation: Array.isArray(dto.palletMergeInboundNos) && dto.palletMergeInboundNos.length
|
||||
|
||||
@@ -65,6 +65,7 @@ const orderFields = computed<DetailField[]>(() => {
|
||||
{ label: '是否暂扣', value: yesNo(order?.detained) },
|
||||
{ label: '是否异常', value: yesNo(order?.abnormal) },
|
||||
{ label: '异常原因', value: order?.abnormal ? text(order.subOrderStatus) : '—' },
|
||||
{ label: '仓库异常作业备注', value: text(order?.warehouseRemark) },
|
||||
{ label: '备注(客户)', value: text(order?.customerRemark) },
|
||||
]
|
||||
})
|
||||
|
||||
@@ -122,6 +122,7 @@ const allFields: QueryField[] = [
|
||||
{ label: '头程订单', value: 'FIRST_LEG' }, { label: '换标下单', value: 'RELABEL_APPLICATION' },
|
||||
{ label: '仓储转运', value: 'WAREHOUSE_TRANSFER' }, { label: '本土散板', value: 'LOCAL_LOOSEBOARD' },
|
||||
{ label: '退货换标', value: 'RETURN_RELABEL' }, { label: '一件代发', value: 'ONE_CLICK_DROPSHIP' },
|
||||
{ label: '货物废弃订单', value: 'CARGO_DISCARD' },
|
||||
] },
|
||||
{ key: 'warehouse', type: 'input', label: '海外仓', width: 210, placeholder: '请输入仓库名称或编码' },
|
||||
{ key: 'sourceOrderNo', type: 'input', label: '来源订单号', width: 190, placeholder: '请输入来源订单号' },
|
||||
@@ -723,6 +724,7 @@ function orderTypeText(orderType: string) {
|
||||
LOCAL_LOOSEBOARD: '本土散板',
|
||||
RETURN_RELABEL: '退货换标',
|
||||
ONE_CLICK_DROPSHIP: '一件代发',
|
||||
CARGO_DISCARD: '货物废弃订单',
|
||||
}
|
||||
return labels[orderType] || orderType
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, onBeforeUnmount } from 'vue'
|
||||
import { Eye, FileUp } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Eye, FileUp, Undo2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { getRelabelReviewTask, returnRelabelForRework } from '../api/relabelOrders'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
@@ -16,6 +19,19 @@ const router = useRouter()
|
||||
const orderId = computed(() => String(route.params.orderId || ''))
|
||||
const detail = ref<RelabelDetailDto | null>(null)
|
||||
const loading = ref(false)
|
||||
const access = useAccessPreviewStore()
|
||||
const returning = ref(false)
|
||||
async function returnForRework() {
|
||||
if (returning.value) return
|
||||
returning.value = true
|
||||
try {
|
||||
const task = await getRelabelReviewTask(orderId.value)
|
||||
const { value } = await ElMessageBox.prompt('填写退回原因,PDA 将展示此备注并重新逐箱作业、上传照片。', '退回重新贴标', { inputType: 'textarea', inputValidator: (value) => Boolean(value?.trim()) && value.trim().length <= 1000 || '请填写 1 至 1000 字退回原因', confirmButtonText: '确认退回' })
|
||||
await returnRelabelForRework(orderId.value, task.version, value.trim(), crypto.randomUUID())
|
||||
ElMessage.success('已退回 PDA 重新贴标'); await load()
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '退回失败,请刷新后重试').message) }
|
||||
finally { returning.value = false }
|
||||
}
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const uploading = ref(false)
|
||||
const previewUrl = ref('')
|
||||
@@ -29,8 +45,9 @@ const groups = [
|
||||
]
|
||||
function text(value: unknown) { return String(value ?? '').trim() || '—' }
|
||||
function status(value: unknown) {
|
||||
const labels: Record<string, string> = { PENDING_PROCESSING: '待处理', PROCESSING: '处理中', CUSTOMER_CONFIRM_PENDING: '待客户确认', READY_FOR_OUTBOUND: '待出库', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' }
|
||||
return labels[String(value || '')] || text(value)
|
||||
if (value === 'CUSTOMER_CONFIRM_PENDING') return '待客户确认'
|
||||
if (value === 'PROCESSING') return '处理中'
|
||||
return ['READY_FOR_OUTBOUND', 'COMPLETED'].includes(String(value || '')) ? '换标完成' : '待处理'
|
||||
}
|
||||
function walletStatus(value: unknown) { return ({ FREEZE_PENDING: '冻结中', FROZEN: '已冻结', SETTLE_PENDING: '结算中', SETTLED: '已结算', RELEASE_PENDING: '释放中', RELEASED: '已释放', FREEZE_FAILED: '冻结失败', SETTLE_FAILED: '结算失败', RELEASE_FAILED: '释放失败' } as Record<string,string>)[String(value || '')] || text(value) }
|
||||
const referenceValue = computed(() => text(detail.value?.application.relabelNo || '换标单号待补充'))
|
||||
@@ -90,6 +107,7 @@ async function previewCompletionPhoto(fileId: string) {
|
||||
<OverseasReadOnlyDetail title="换标单详情" reference-label="换标单号" :reference-value="referenceValue" back-label="返回换标单" domain="换标单详情" :groups="groups" :field-values="fieldValues" :pending="false" :status-text="detail ? status(detail.application.status) : '加载中'" :loading="loading" :show-content="!loadError" @back="back">
|
||||
<template #state><LoadErrorState v-if="loadError" title="换标单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<template #content-after>
|
||||
<section v-if="access.can(permission.relabelOrderStatus) && ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(detail?.application.status || '') && detail?.completionEvidence?.photoFileIds?.length" class="relabel-document-panel"><el-button type="warning" :loading="returning" @click="returnForRework"><Undo2 :size="15" />退回重新贴标</el-button></section>
|
||||
<section class="relabel-document-panel" aria-label="PDA换标完工证据"><div class="relabel-mark-list__head"><h2>PDA 换标完工证据</h2><span>{{ detail?.completionEvidence?.reviewerName ? `复核人:${detail.completionEvidence.reviewerName}` : '等待仓库作业' }}</span></div><div v-if="detail?.completionEvidence?.photoFileIds?.length" class="document-row"><strong>共 {{ detail.completionEvidence.photoFileIds.length }} 张完工照片</strong><span class="document-actions"><el-button v-for="(fileId, index) in detail.completionEvidence.photoFileIds" :key="fileId" size="small" plain @click="previewCompletionPhoto(fileId)"><Eye :size="14" />照片 {{ index + 1 }}</el-button></span></div><div v-else class="attachment-empty">暂未上传完工照片</div></section>
|
||||
<section class="relabel-document-panel" aria-label="总换标文件"><div class="relabel-mark-list__head"><h2>总换标 PDF</h2><label class="file-picker"><FileUp :size="15" />{{ uploading ? '上传中' : (detail?.application.totalFileName ? '更换PDF' : '上传PDF') }}<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'TOTAL')" /></label></div><div v-if="detail?.application.totalFileId" class="document-row"><strong>{{ detail.application.totalFileName }}</strong><el-button size="small" plain @click="previewPdf(detail.application.totalFileId, detail.application.totalFileName || '总换标PDF', 'TOTAL')"><Eye :size="14" />直接预览</el-button></div><div v-else class="attachment-empty">暂未上传总换标PDF</div></section>
|
||||
<section class="relabel-mark-list" aria-label="箱唛换标信息">
|
||||
|
||||
@@ -15,7 +15,7 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/compone
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import { changeRelabelOrderStatus, createRelabelOrder, listRelabelOrders, listRelabelWarehouseOptions, type RelabelWarehouseOptionDto } from '../api/relabelOrders'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import { formatOverseasDateTime, relabelBusinessStatusLabel } from '../lastMilePresentation'
|
||||
import type { RelabelOrderListItem } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -34,7 +34,7 @@ const filters = ref<QueryModel>({ relabelNo: '', sku: '', status: '' })
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'relabelNo', type: 'input', label: '换标单号', placeholder: '请输入换标单号', width: 220 },
|
||||
{ key: 'sku', type: 'input', label: '商品SKU', placeholder: '请输入商品SKU', width: 180 },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '已完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }] },
|
||||
])
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
@@ -69,7 +69,7 @@ function openForm(mode: 'create' | 'edit', source?: object) {
|
||||
Object.assign(form, { orderNo: '', customerId: '', relatedOrderNo: '', sku: '', productName: '', cartons: undefined, budgetTotal: undefined, status: 'PENDING_PROCESSING', labelRequirement: '', relabelCost: undefined, remark: '', warehouseId: '', labelDeadline: '', contactName: '', contactPhone: '', shipmentNo: '', newSkuCode: '' })
|
||||
editSourceStatus.value = ''
|
||||
if (mode === 'edit' && row) {
|
||||
editSourceStatus.value = relabelStatusCode(row.status)
|
||||
editSourceStatus.value = row.rawStatus || relabelStatusCode(row.status)
|
||||
Object.assign(form, { orderNo: String(row.orderNo || ''), relatedOrderNo: String(row.relatedOrderNo || ''), sku: String(row.sku || ''), productName: String(row.productName || ''), cartons: optionalNumber(row.cartons), budgetTotal: optionalNumber(row.budgetTotal), status: '', labelRequirement: String(row.labelRequirement || ''), relabelCost: optionalNumber(row.relabelCost), remark: String(row.remark || '') })
|
||||
}
|
||||
if (mode === 'create') {
|
||||
@@ -143,17 +143,24 @@ function openDetail(source: object) {
|
||||
}
|
||||
|
||||
function relabelStatus(value: unknown) {
|
||||
const labels: Record<string, string> = { PENDING_PROCESSING: '待处理', PROCESSING: '处理中', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' }
|
||||
return labels[String(value || '')] || String(value || '—')
|
||||
return relabelBusinessStatusLabel(value)
|
||||
}
|
||||
function relabelAuditStatusLabel(value: string) {
|
||||
return ({
|
||||
PROCESSING: '处理中',
|
||||
REJECTED: '已驳回',
|
||||
CANCELLED: '已取消',
|
||||
COMPLETED: '已完成',
|
||||
} as Record<string, string>)[value] || relabelStatus(value)
|
||||
}
|
||||
function relabelStatusCode(value: unknown) {
|
||||
const codes: Record<string, string> = { 待处理: 'PENDING_PROCESSING', 处理中: 'PROCESSING', 已完成: 'COMPLETED', 已驳回: 'REJECTED', 已取消: 'CANCELLED' }
|
||||
const codes: Record<string, string> = { 待处理: 'PENDING_PROCESSING', 处理中: 'PROCESSING', 换标完成: 'COMPLETED' }
|
||||
return codes[String(value || '')] || String(value || 'PENDING_PROCESSING')
|
||||
}
|
||||
function walletStatus(value: unknown) { return ({ FREEZE_PENDING: '冻结中', FROZEN: '已冻结', SETTLE_PENDING: '结算中', SETTLED: '已结算', RELEASE_PENDING: '释放中', RELEASED: '已释放', FREEZE_FAILED: '冻结失败', SETTLE_FAILED: '结算失败', RELEASE_FAILED: '释放失败' } as Record<string,string>)[String(value || '')] || String(value || '—') }
|
||||
function canAudit(source: object) {
|
||||
const row = source as RelabelOrderListItem
|
||||
return ['PENDING_PROCESSING', 'PROCESSING'].includes(relabelStatusCode(row.status))
|
||||
return ['PENDING_PROCESSING', 'PROCESSING'].includes(row.rawStatus || relabelStatusCode(row.status))
|
||||
}
|
||||
async function load(model: QueryModel = filters.value) {
|
||||
Object.assign(filters.value, model)
|
||||
@@ -162,7 +169,7 @@ async function load(model: QueryModel = filters.value) {
|
||||
const result = await listRelabelOrders({ page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined })
|
||||
rows.value = result.records.map((item) => ({
|
||||
customerId: '', orderNo: String(item.relabelNo), relatedOrderNo: String(item.lastlegOrderNo || item.customerOrderNo || '—'), sku: String(item.skuCodes || '—'),
|
||||
productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), remark: '',
|
||||
productName: '—', cartons: Number(item.totalPackageCount || 0), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '',
|
||||
createdAt: formatOverseasDateTime(item.createdAt),
|
||||
}))
|
||||
total.value = result.total
|
||||
@@ -197,7 +204,7 @@ watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
|
||||
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="截止时间" required><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></el-form-item></el-col>
|
||||
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="联系人" required><el-input v-model="form.contactName" placeholder="请输入联系人" /></el-form-item></el-col>
|
||||
<el-col v-if="dialogMode === 'create'" :span="12"><el-form-item label="联系电话" required><el-input v-model="form.contactPhone" placeholder="请输入联系电话" /></el-form-item></el-col>
|
||||
<el-col v-if="dialogMode === 'edit'" :span="12"><el-form-item label="目标状态"><el-select v-model="form.status" placeholder="请选择目标状态"><el-option v-for="value in allowedStatusOptions" :key="value" :label="relabelStatus(value)" :value="value" /></el-select></el-form-item></el-col>
|
||||
<el-col v-if="dialogMode === 'edit'" :span="12"><el-form-item label="目标状态"><el-select v-model="form.status" placeholder="请选择目标状态"><el-option v-for="value in allowedStatusOptions" :key="value" :label="relabelAuditStatusLabel(value)" :value="value" /></el-select></el-form-item></el-col>
|
||||
</el-row><h3>作业要求</h3><el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="贴标要求" required><el-select v-model="form.labelRequirement" :disabled="dialogMode === 'edit'" placeholder="请选择贴标要求"><el-option v-for="value in ['换箱唛', '换SKU标', '换箱唛和SKU标']" :key="value" :label="value" :value="value" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="货件号" required><el-input v-model="form.shipmentNo" :disabled="dialogMode === 'edit'" placeholder="请输入货件号" /></el-form-item></el-col>
|
||||
|
||||
@@ -144,6 +144,7 @@ export interface RelabelOrderListItem {
|
||||
walletErrorMessage?: string
|
||||
fileName: string
|
||||
status: string
|
||||
rawStatus?: string
|
||||
remark: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user