Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
8 changed files with 76 additions and 15 deletions
Showing only changes of commit ea30e3f37a - Show all commits
@@ -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>
+1
View File
@@ -144,6 +144,7 @@ export interface RelabelOrderListItem {
walletErrorMessage?: string
fileName: string
status: string
rawStatus?: string
remark: string
createdAt: string
}