diff --git a/src/modules/overseas-business/PROTOTYPE_MATRIX.md b/src/modules/overseas-business/PROTOTYPE_MATRIX.md index 9a32ffa6..36509701 100644 --- a/src/modules/overseas-business/PROTOTYPE_MATRIX.md +++ b/src/modules/overseas-business/PROTOTYPE_MATRIX.md @@ -130,3 +130,6 @@ | 本土散板 | 新增、编辑 | 既有独立路由,与卡车复用运输表单 | 同步布局修复,保留运输双列、货物四列及散板专属字段 | 同上 | 本次仅调整 Admin 运输表单展示,不改采购编辑的宽度或 Client 表单。在 `apps/admin-web` 经 `local-check.ps1` 执行:`pnpm exec vitest run src/modules/overseas-business/pages/OverseasBusinessViews.test.ts src/modules/overseas-business/contract.test.ts src/styles/adminDensity.test.ts`,3 文件 115 项通过;`pnpm typecheck`、`pnpm build` 通过。共享完整 `pnpm test`(任务名 `Admin restored dispatch full regression`,13:55:33–14:05:50)退出码 0,覆盖本次未再变化的运输表单源码;其他在途排车变更由对应任务验证,不据此推定其最新快照通过。视觉仍待人工截图确认。 + + +2026-10-29 用户截图调整:采购详情显示客户驳回原因,收货批次提供重新上传照片;各批旧图全部替换前不可再次采购完成。本土卡车新增/编辑数量改托数,托数和预算均须大于0。自动检查与人工视觉验收分别记录;待人工1920×1080、100%截图确认。 diff --git a/src/modules/overseas-business/api/procurementWorkflow.ts b/src/modules/overseas-business/api/procurementWorkflow.ts index 86877e18..587d0aca 100644 --- a/src/modules/overseas-business/api/procurementWorkflow.ts +++ b/src/modules/overseas-business/api/procurementWorkflow.ts @@ -4,9 +4,10 @@ export interface ProcurementWorkflowReceipt { id: string; quantity: number; file export interface ProcurementOutbound { id: string; outboundNo: string; quantity: number; trackingNo: string; status: string; fileIds: string[]; createdAt: string; completedAt: string | null } export interface ProcurementWorkflow { orderNo: string; version: number; status: string; receivedQuantity: number; stockQuantity: number; reservedQuantity: number; availableQuantity: number + rejectionReason: string | null; photosNeedUpdate: boolean receipts: ProcurementWorkflowReceipt[]; outbounds: ProcurementOutbound[] } -export interface WorkflowCommand { version: number; idempotencyKey: string; quantity?: number; trackingNo?: string; fileIds?: string[] } +export interface WorkflowCommand { version: number; idempotencyKey: string; receiptId?: string; quantity?: number; trackingNo?: string; fileIds?: string[] } const base = '/platform/v1/last-leg/overseas-procurements' export async function getProcurementWorkflow(orderNo: string, customerId: string) { return unwrapApiResponse(await http.get, ApiEnvelope>(`${base}/${encodeURIComponent(orderNo)}/workflow`, { params: { customerId } })) diff --git a/src/modules/overseas-business/components/ProcurementWorkflowPanel.vue b/src/modules/overseas-business/components/ProcurementWorkflowPanel.vue index ff727cc6..3c3c6de9 100644 --- a/src/modules/overseas-business/components/ProcurementWorkflowPanel.vue +++ b/src/modules/overseas-business/components/ProcurementWorkflowPanel.vue @@ -21,8 +21,8 @@ const open = ref(false), saving = ref(false), uploading = ref(false), checkingFe const action = ref(''), batchId = ref(''), warehouses = ref([]), warehouseLoading = ref(false) const form = reactive({ quantity: 1, trackingNo: '', fileIds: [] as string[], warehouseId: '', decision: 'APPROVE' as 'APPROVE' | 'REJECT', reason: '' }) const uploadedNames = ref([]) -const title = computed(() => ({ review: '审核采购单', receipts: '登记收货', 'request-confirmation': '采购完成', outbounds: '创建出库单', 'edit-outbound': '编辑出库单', dispatch: '开始派送', cancel: '取消出库单', complete: '上传签收凭证' })[action.value] || '采购操作') -const permission = computed(() => `platform:overseas-business:purchase:${action.value === 'review' ? 'review' : ['receipts', 'request-confirmation'].includes(action.value) ? 'receive-overseas' : 'ship-customer'}`) +const title = computed(() => ({ review: '审核采购单', receipts: '登记收货', 'receipt-photos': '重新上传收货照片', 'request-confirmation': '采购完成', outbounds: '创建出库单', 'edit-outbound': '编辑出库单', dispatch: '开始派送', cancel: '取消出库单', complete: '上传签收凭证' })[action.value] || '采购操作') +const permission = computed(() => `platform:overseas-business:purchase:${action.value === 'review' ? 'review' : ['receipts', 'receipt-photos', 'request-confirmation'].includes(action.value) ? 'receive-overseas' : 'ship-customer'}`) const can = (name: string) => access.can(`platform:overseas-business:purchase:${name}`) const permissionHint = computed(() => props.workflow.status === 'RECEIVING' && !can('receive-overseas') ? '当前账号未开通采购收货权限,请联系管理员开通。' @@ -33,9 +33,9 @@ let fingerprint = '', requestKey = '', openedScope = '', dialogGeneration = 0 const scope = () => `${props.customerId}:${props.workflow.orderNo}` watch(scope, () => { dialogGeneration++; open.value = false; error.value = null }) watch(open, value => { if (!value) dialogGeneration++ }) -async function begin(name: string, batch?: ProcurementOutbound) { +async function begin(name: string, batch?: ProcurementOutbound | ProcurementWorkflowReceipt) { if (saving.value || uploading.value || checkingFees.value) return - const required = name === 'review' ? 'review' : ['receipts', 'request-confirmation'].includes(name) ? 'receive-overseas' : 'ship-customer' + const required = name === 'review' ? 'review' : ['receipts', 'receipt-photos', 'request-confirmation'].includes(name) ? 'receive-overseas' : 'ship-customer' if (!can(required)) return const current = ++dialogGeneration if (name === 'outbounds') { @@ -49,7 +49,7 @@ async function begin(name: string, batch?: ProcurementOutbound) { } action.value = name; batchId.value = batch?.id || ''; error.value = null; warehouseLoading.value = false Object.assign(form, { quantity: 1, trackingNo: '', fileIds: [], warehouseId: '', decision: 'APPROVE', reason: '' }); uploadedNames.value = [] - if (name === 'edit-outbound' && batch) Object.assign(form, { quantity: batch.quantity, trackingNo: batch.trackingNo }) + if (name === 'edit-outbound' && batch && 'trackingNo' in batch) Object.assign(form, { quantity: batch.quantity, trackingNo: batch.trackingNo }) fingerprint = ''; requestKey = ''; openedScope = scope(); open.value = true; captureBaseline() if (name === 'review') { warehouseLoading.value = true @@ -64,7 +64,7 @@ async function upload(event: Event) { const current = dialogGeneration; const orderNo = props.workflow.orderNo; const customer = props.customerId; uploading.value = true; error.value = null try { for (const file of files) { - if (action.value === 'receipts' && !file.type.startsWith('image/')) throw new Error('收货凭证请选择图片') + if (['receipts', 'receipt-photos'].includes(action.value) && !file.type.startsWith('image/')) throw new Error('收货凭证请选择图片') const result = await uploadProcurementFile(orderNo, customer, 'RECEIPT', file) if (current !== dialogGeneration) return form.fileIds.push(result.fileId); uploadedNames.value.push(file.name) @@ -79,8 +79,8 @@ async function save() { if (['receipts', 'outbounds', 'edit-outbound'].includes(action.value) && (!Number.isSafeInteger(form.quantity) || form.quantity < 1)) { ElMessage.warning('请输入有效数量'); return } if (action.value === 'receipts' && form.quantity > props.detail.customerView.order.purchaseQuantity - props.workflow.receivedQuantity) { ElMessage.warning('收货数量不能超过剩余采购数量'); return } if (['outbounds', 'edit-outbound'].includes(action.value) && (!form.trackingNo.trim() || form.quantity > props.workflow.availableQuantity + (action.value === 'edit-outbound' ? (props.workflow.outbounds.find(row => row.id === batchId.value)?.quantity || 0) : 0))) { ElMessage.warning('请填写物流跟踪号,数量不能超过可出库库存'); return } - if (['receipts', 'complete'].includes(action.value) && !form.fileIds.length) { ElMessage.warning(action.value === 'receipts' ? '请上传收货照片' : '请上传签收文件或图片'); return } - const data = { version: props.workflow.version, ...(action.value === 'receipts' ? { quantity: form.quantity, fileIds: [...form.fileIds] } : ['outbounds', 'edit-outbound'].includes(action.value) ? { quantity: form.quantity, trackingNo: form.trackingNo.trim() } : action.value === 'complete' ? { fileIds: [...form.fileIds] } : {}) } + if (['receipts', 'receipt-photos', 'complete'].includes(action.value) && !form.fileIds.length) { ElMessage.warning(action.value === 'receipts' ? '请上传收货照片' : '请上传签收文件或图片'); return } + const data = { version: props.workflow.version, ...(action.value === 'receipts' ? { quantity: form.quantity, fileIds: [...form.fileIds] } : ['outbounds', 'edit-outbound'].includes(action.value) ? { quantity: form.quantity, trackingNo: form.trackingNo.trim() } : action.value === 'receipt-photos' ? { receiptId: batchId.value, fileIds: [...form.fileIds] } : action.value === 'complete' ? { fileIds: [...form.fileIds] } : {}) } const next = JSON.stringify([action.value, batchId.value, form.warehouseId, form.decision, form.reason, data]) if (fingerprint !== next || !requestKey) { fingerprint = next; requestKey = crypto.randomUUID() } const current = dialogGeneration @@ -88,7 +88,7 @@ async function save() { try { if (action.value === 'review') await submitProcurementCommand(props.workflow.orderNo, props.customerId, 'review', { version: props.workflow.version, idempotencyKey: requestKey, decision: form.decision, purchaseWarehouseId: form.decision === 'APPROVE' ? form.warehouseId : undefined, reason: form.reason.trim() || undefined }) else if (action.value === 'edit-outbound') await editProcurementOutbound(props.workflow.orderNo, props.customerId, batchId.value, { ...data, idempotencyKey: requestKey }) - else await submitProcurementWorkflow(props.workflow.orderNo, props.customerId, batchId.value ? `outbounds/${encodeURIComponent(batchId.value)}/${action.value}` : action.value, { ...data, idempotencyKey: requestKey }) + else await submitProcurementWorkflow(props.workflow.orderNo, props.customerId, batchId.value && action.value !== 'receipt-photos' ? `outbounds/${encodeURIComponent(batchId.value)}/${action.value}` : action.value, { ...data, idempotencyKey: requestKey }) if (current !== dialogGeneration) return captureBaseline(); open.value = false; ElMessage.success('操作成功'); emit('changed') } catch (failure) { if (current === dialogGeneration) error.value = getApiErrorDetails(failure, '操作失败') } @@ -108,21 +108,22 @@ const fileName = (id: string) => props.detail.files.find(file => file.fileId ===