This commit is contained in:
2026-10-28 10:55:19 +08:00
parent 298f1d9fe3
commit e98718dcf2
6 changed files with 18 additions and 18 deletions
@@ -32,7 +32,7 @@ test('renders unknown pallet service and container type as dashes while preservi
test('shows merchant-owned arrangement facts without a pool request or editable status control', () => { test('shows merchant-owned arrangement facts without a pool request or editable status control', () => {
assert.match(api, /fleetArrangement: import\('\.\.\/fleetArrangement'\)\.FleetArrangement \| null/) assert.match(api, /fleetArrangement: import\('\.\.\/fleetArrangement'\)\.FleetArrangement \| null/)
assert.match(view, /aria-label="承运安排"/) assert.match(view, /aria-label="承运安排"/)
for (const field of ['signedPackages', 'unsignedPackages', 'returnedPackages', 'readOnlyReason']) assert.ok(view.includes(`fleetArrangement.${field}`)) for (const field of ['signedPackages', 'unsignedPackages', 'returnedPackages']) assert.ok(view.includes(`fleetArrangement.${field}`))
assert.doesNotMatch(api, /capacity-offers|fleet\/trips/) assert.doesNotMatch(api, /capacity-offers|fleet\/trips/)
assert.match(view, /if \(query\.page !== 1\) query\.page = 1\s+else void load\(\)/) assert.match(view, /if \(query\.page !== 1\) query\.page = 1\s+else void load\(\)/)
assert.match(view, /if \(reloadDirectly\) void load\(\)/) assert.match(view, /if \(reloadDirectly\) void load\(\)/)
@@ -298,7 +298,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
<section v-else-if="mode === 'detail'" class="business-detail-page" aria-label="订单详情"> <section v-else-if="mode === 'detail'" class="business-detail-page" aria-label="订单详情">
<header class="business-detail-heading"> <header class="business-detail-heading">
<div class="business-detail-heading-main"><el-button link type="primary" @click="returnToList">← 返回列表</el-button><div><h2>{{ detail?.summary.orderNo || '订单详情' }}</h2><p>{{ configuration.title }}订单</p></div></div> <div class="business-detail-heading-main"><el-button link type="primary" @click="returnToList">← 返回列表</el-button><div><h2>{{ detail?.summary.orderNo || '订单详情' }}</h2></div></div>
<el-tag :type="statusTone(detail?.summary.status)" effect="light">{{ detailStatus }}</el-tag> <el-tag :type="statusTone(detail?.summary.status)" effect="light">{{ detailStatus }}</el-tag>
</header> </header>
<div v-if="error" class="business-detail-error" role="alert"><strong>{{ error }}</strong><el-button size="small" @click="loadDetail">重新加载</el-button></div> <div v-if="error" class="business-detail-error" role="alert"><strong>{{ error }}</strong><el-button size="small" @click="loadDetail">重新加载</el-button></div>
@@ -309,7 +309,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
</dl> </dl>
</section> </section>
<section v-if="!error && fleetArrangement" class="business-detail-section" aria-label="承运安排"> <section v-if="!error && fleetArrangement" class="business-detail-section" aria-label="承运安排">
<h3>承运安排</h3><p>{{ fleetArrangement.readOnlyReason }}</p> <h3>承运安排</h3>
<dl class="detail-grid arrangement-grid"> <dl class="detail-grid arrangement-grid">
<div><dt>预订号</dt><dd>{{ fleetArrangement.bookingNo }}</dd></div> <div><dt>预订号</dt><dd>{{ fleetArrangement.bookingNo }}</dd></div>
<div><dt>计划发车</dt><dd>{{ dateTime(fleetArrangement.plannedDepartureAt) }}</dd></div> <div><dt>计划发车</dt><dd>{{ dateTime(fleetArrangement.plannedDepartureAt) }}</dd></div>
@@ -289,7 +289,7 @@ onMounted(() => { if (props.detailPage) void Promise.all([loadCartonDetail(), lo
</template> </template>
<article v-else v-loading="loading" class="carton-detail-surface"> <article v-else v-loading="loading" class="carton-detail-surface">
<template v-if="cartonDetail"> <template v-if="cartonDetail">
<header class="carton-detail-header"><button type="button" class="carton-detail-back" @click="returnToCartons"><ArrowLeft :size="16" />返回列表</button><div><strong>{{ cartonDetail.cartonMark }}</strong><span>{{ warehouseLabel(cartonDetail.overseasWarehouseId) }} · 退货箱明细</span></div><div class="carton-detail-actions"><el-tag effect="light">{{ cartonStatusLabel(cartonDetail.status) }}</el-tag><el-button type="primary" @click="printLabel(cartonDetail.cartonMark)"><Printer :size="16" />打印箱唛</el-button></div></header> <header class="carton-detail-header"><button type="button" class="carton-detail-back" @click="returnToCartons"><ArrowLeft :size="16" />返回列表</button><div><strong>{{ cartonDetail.cartonMark }}</strong><span>{{ warehouseLabel(cartonDetail.overseasWarehouseId) }}</span></div><div class="carton-detail-actions"><el-tag effect="light">{{ cartonStatusLabel(cartonDetail.status) }}</el-tag><el-button type="primary" @click="printLabel(cartonDetail.cartonMark)"><Printer :size="16" />打印箱唛</el-button></div></header>
<section class="carton-detail-section"><h3>箱号概览</h3><dl class="carton-detail-grid"><div><dt>来源退货单</dt><dd>{{ cartonDetail.sourceReturnNo || '—' }}</dd></div><div><dt>签收批次</dt><dd>{{ cartonDetail.sourceBatchNo || '—' }}</dd></div><div><dt>SKU 种类</dt><dd>{{ cartonDetail.skuTypes }}</dd></div><div><dt>箱内总件数</dt><dd>{{ cartonDetail.totalQuantity }}</dd></div><div><dt>箱规</dt><dd>{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}</dd></div><div><dt>重量</dt><dd>{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}</dd></div><div><dt>换标进度</dt><dd>{{ returnRelabelStatusLabel(cartonDetail.relabelStatus) }}</dd></div><div><dt>实际完成(件)</dt><dd>{{ cartonDetail.relabelledQuantity ?? '—' }}</dd></div><div><dt>处理中占用(件)</dt><dd>{{ cartonDetail.relabelReservedQuantity ?? '—' }}</dd></div><div><dt>签收时间</dt><dd>{{ formatInventoryTime(cartonDetail.receivedAt) }}</dd></div></dl></section> <section class="carton-detail-section"><h3>箱号概览</h3><dl class="carton-detail-grid"><div><dt>来源退货单</dt><dd>{{ cartonDetail.sourceReturnNo || '—' }}</dd></div><div><dt>签收批次</dt><dd>{{ cartonDetail.sourceBatchNo || '—' }}</dd></div><div><dt>SKU 种类</dt><dd>{{ cartonDetail.skuTypes }}</dd></div><div><dt>箱内总件数</dt><dd>{{ cartonDetail.totalQuantity }}</dd></div><div><dt>箱规</dt><dd>{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}</dd></div><div><dt>重量</dt><dd>{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}</dd></div><div><dt>换标进度</dt><dd>{{ returnRelabelStatusLabel(cartonDetail.relabelStatus) }}</dd></div><div><dt>实际完成(件)</dt><dd>{{ cartonDetail.relabelledQuantity ?? '—' }}</dd></div><div><dt>处理中占用(件)</dt><dd>{{ cartonDetail.relabelReservedQuantity ?? '—' }}</dd></div><div><dt>签收时间</dt><dd>{{ formatInventoryTime(cartonDetail.receivedAt) }}</dd></div></dl></section>
<section class="carton-detail-section"><h3>箱内 SKU</h3><DataTable :data="cartonDetail.items" :columns="cartonItemColumns" row-key="skuCode" empty-text="暂无箱内 SKU" /></section> <section class="carton-detail-section"><h3>箱内 SKU</h3><DataTable :data="cartonDetail.items" :columns="cartonItemColumns" row-key="skuCode" empty-text="暂无箱内 SKU" /></section>
<section class="carton-detail-section"><h3>SKU 换标明细</h3><DataTable :data="cartonDetail.skuConversions" :columns="conversionColumns" empty-text="暂无 SKU 换标记录"><template #conversionStatus="{ row }">{{ conversionStatusLabel(row.status) }}</template></DataTable></section> <section class="carton-detail-section"><h3>SKU 换标明细</h3><DataTable :data="cartonDetail.skuConversions" :columns="conversionColumns" empty-text="暂无 SKU 换标记录"><template #conversionStatus="{ row }">{{ conversionStatusLabel(row.status) }}</template></DataTable></section>
@@ -378,7 +378,7 @@ onBeforeUnmount(clearPhotoUrls)
<template v-if="selected"> <template v-if="selected">
<header class="return-detail-header"> <header class="return-detail-header">
<button type="button" class="return-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button> <button type="button" class="return-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button>
<div class="return-detail-title"><strong>{{ selected.order.returnNo }}</strong><span>{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }} · 退货签收</span></div> <div class="return-detail-title"><strong>{{ selected.order.returnNo }}</strong><span>{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }}</span></div>
<div class="return-detail-actions"><el-button v-if="canCreateRelabel && canApplyRelabel(selected.order)" type="primary" plain @click="openRelabel(selected.order)">贴标申请</el-button><el-tag :type="statusType(selected.order.status, selected.order.needRelabel)" :class="statusClass(selected.order.status, selected.order.needRelabel)" effect="light">{{ statusLabel(selected.order.status, selected.order.needRelabel) }}</el-tag></div> <div class="return-detail-actions"><el-button v-if="canCreateRelabel && canApplyRelabel(selected.order)" type="primary" plain @click="openRelabel(selected.order)">贴标申请</el-button><el-tag :type="statusType(selected.order.status, selected.order.needRelabel)" :class="statusClass(selected.order.status, selected.order.needRelabel)" effect="light">{{ statusLabel(selected.order.status, selected.order.needRelabel) }}</el-tag></div>
</header> </header>
<ReviewRejectionNotice :status="selected.order.status" :reason="selected.remark" /> <ReviewRejectionNotice :status="selected.order.status" :reason="selected.remark" />
@@ -1889,7 +1889,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<template v-else> <template v-else>
<header class="warehouse-detail-header"> <header class="warehouse-detail-header">
<button type="button" class="warehouse-detail-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button> <button type="button" class="warehouse-detail-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button>
<div class="warehouse-detail-title"><strong>{{ props.service === 'labeling' ? selectedRelabelDetail?.application.relabelNo : selectedDetail?.transferNo || selectedDetail?.['0'] || `${config.title}详情` }}</strong><span>{{ config.title }}详情</span></div> <div class="warehouse-detail-title"><strong>{{ props.service === 'labeling' ? selectedRelabelDetail?.application.relabelNo : selectedDetail?.transferNo || selectedDetail?.['0'] || `${config.title}详情` }}</strong></div>
<div class="warehouse-detail-actions"><el-button v-if="canCreate && props.service === 'labeling' && selectedRelabelDetail?.application.status === 'REJECTED'" type="primary" @click="resubmitRejectedRelabel">修改后重新提交</el-button><el-button v-if="canConfirmRelabel && props.service === 'labeling' && selectedRelabelDetail?.application.status === 'CUSTOMER_CONFIRM_PENDING' && !selectedRelabelDetail?.application.customerConfirmedAt" type="primary" @click="confirmSelectedRelabel">确认换标完成</el-button><el-button v-if="canConfirmDiscard && props.service === 'disposal' && selectedDetail?.status === 'PDA_COMPLETED'" type="primary" @click="openCompletionConfirmation(String(selectedDetail?.discardNo || ''), 'disposal')">废弃确认</el-button><el-button v-if="canCancelDiscard && props.service === 'disposal' && selectedDetail?.status === 'PENDING_REVIEW'" type="danger" @click="cancelSelectedDiscard">取消申请</el-button><el-button v-if="canCreate && props.service === 'transfer' && ['DRAFT', 'REJECTED'].includes(selectedDetail?.status || '')" type="primary" @click="openEdit(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))">编辑</el-button><el-button v-if="canConfirmTransfer && props.service === 'transfer' && selectedDetail?.status === 'WAIT_CUSTOMER_CONFIRMATION'" type="primary" @click="confirmTransferMeasurements">确认测量数据</el-button><el-tag v-if="props.service !== 'transfer'" :type="props.service === 'labeling' ? relabelStatusTagType(selectedRelabelDetail?.application.status || '') : undefined" effect="light">{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}</el-tag></div> <div class="warehouse-detail-actions"><el-button v-if="canCreate && props.service === 'labeling' && selectedRelabelDetail?.application.status === 'REJECTED'" type="primary" @click="resubmitRejectedRelabel">修改后重新提交</el-button><el-button v-if="canConfirmRelabel && props.service === 'labeling' && selectedRelabelDetail?.application.status === 'CUSTOMER_CONFIRM_PENDING' && !selectedRelabelDetail?.application.customerConfirmedAt" type="primary" @click="confirmSelectedRelabel">确认换标完成</el-button><el-button v-if="canConfirmDiscard && props.service === 'disposal' && selectedDetail?.status === 'PDA_COMPLETED'" type="primary" @click="openCompletionConfirmation(String(selectedDetail?.discardNo || ''), 'disposal')">废弃确认</el-button><el-button v-if="canCancelDiscard && props.service === 'disposal' && selectedDetail?.status === 'PENDING_REVIEW'" type="danger" @click="cancelSelectedDiscard">取消申请</el-button><el-button v-if="canCreate && props.service === 'transfer' && ['DRAFT', 'REJECTED'].includes(selectedDetail?.status || '')" type="primary" @click="openEdit(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))">编辑</el-button><el-button v-if="canConfirmTransfer && props.service === 'transfer' && selectedDetail?.status === 'WAIT_CUSTOMER_CONFIRMATION'" type="primary" @click="confirmTransferMeasurements">确认测量数据</el-button><el-tag v-if="props.service !== 'transfer'" :type="props.service === 'labeling' ? relabelStatusTagType(selectedRelabelDetail?.application.status || '') : undefined" effect="light">{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}</el-tag></div>
</header> </header>
<div v-if="detailError" class="warehouse-detail-error" role="alert"><strong>{{ detailError }}</strong><el-button @click="loadDetailPage">重新加载</el-button></div> <div v-if="detailError" class="warehouse-detail-error" role="alert"><strong>{{ detailError }}</strong><el-button @click="loadDetailPage">重新加载</el-button></div>
@@ -1898,13 +1898,13 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<TransferPickupProgressPanel v-if="!detailError && props.service === 'transfer' && selectedDetail?.deliveryMethod === 'WAREHOUSE_PICKUP'" :transfer-no="String(selectedDetail.transferNo || '')" /> <TransferPickupProgressPanel v-if="!detailError && props.service === 'transfer' && selectedDetail?.deliveryMethod === 'WAREHOUSE_PICKUP'" :transfer-no="String(selectedDetail.transferNo || '')" />
<div v-if="props.service === 'disposal'" class="warehouse-card warehouse-form-section"> <div v-if="props.service === 'disposal'" class="warehouse-card warehouse-form-section">
<h3>本次废弃箱子</h3><div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>入仓码</th><th>SKU</th><th>品名</th><th>申请数量</th><th>所选箱唛</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="index"><td>{{ item.inboundCode || '—' }}</td><td>{{ item.skuCode || '—' }}</td><td>{{ item.productName || '—' }}</td><td>{{ item.discardPackages ?? '—' }} {{ item.quantityUnit || '单位未记录' }}<br>实际:{{ item.actualDiscardPackages == null ? '未记录' : `${item.actualDiscardPackages} ${item.quantityUnit || '单位未记录'}` }}</td><td>{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}</td></tr></tbody></table></div> <h3>本次废弃箱子</h3><div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>入仓码</th><th>SKU</th><th>品名</th><th>申请数量</th><th>所选箱唛</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="index"><td>{{ item.inboundCode || '—' }}</td><td>{{ item.skuCode || '—' }}</td><td>{{ item.productName || '—' }}</td><td>{{ item.discardPackages ?? '—' }} {{ item.quantityUnit || '单位未记录' }}<br>实际:{{ item.actualDiscardPackages == null ? '未记录' : `${item.actualDiscardPackages} ${item.quantityUnit || '单位未记录'}` }}</td><td>{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}</td></tr></tbody></table></div>
<h3>废弃现场照片</h3><div v-if="selectedDetail?.photos?.length" class="warehouse-photo-grid"><div v-for="photo in selectedDetail.photos" :key="photo.fileId" class="warehouse-photo-item"><el-image v-if="discardPhotoUrls[photo.fileId]" :src="discardPhotoUrls[photo.fileId]" :preview-src-list="discardPhotoPreviewUrls" :initial-index="discardPhotoPreviewUrls.indexOf(discardPhotoUrls[photo.fileId])" fit="cover" preview-teleported /><div v-else-if="discardPhotoLoading[photo.fileId]" class="warehouse-photo-placeholder">照片加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadDiscardPhoto(photo.fileId)">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button><small>{{ formatDateTime(photo.submittedAt) }}</small></div></div><div v-else class="warehouse-empty">暂无已提交的废弃现场照片</div> <template v-if="selectedDetail?.photos?.length"><h3>废弃现场照片</h3><div class="warehouse-photo-grid"><div v-for="photo in selectedDetail.photos" :key="photo.fileId" class="warehouse-photo-item"><el-image v-if="discardPhotoUrls[photo.fileId]" :src="discardPhotoUrls[photo.fileId]" :preview-src-list="discardPhotoPreviewUrls" :initial-index="discardPhotoPreviewUrls.indexOf(discardPhotoUrls[photo.fileId])" fit="cover" preview-teleported /><div v-else-if="discardPhotoLoading[photo.fileId]" class="warehouse-photo-placeholder">照片加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadDiscardPhoto(photo.fileId)">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button><small>{{ formatDateTime(photo.submittedAt) }}</small></div></div></template>
</div> </div>
<div v-if="props.service === 'transfer' && selectedDetail?.status !== 'DRAFT'" class="warehouse-card warehouse-form-section"> <div v-if="props.service === 'transfer' && selectedDetail?.status !== 'DRAFT'" class="warehouse-card warehouse-form-section">
<div class="warehouse-section-heading warehouse-transfer-receipt-heading"><h3>预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)</h3><div class="warehouse-transfer-receipt-actions"><el-button v-if="canIncreaseForecast" type="primary" size="small" @click="openIncreaseForecast"><Plus :size="14" />增加预报箱数</el-button><el-button type="primary" size="small" @click="printTransferCartons(String(selectedDetail?.transferNo || ''))"><Printer :size="14" />打印预报箱唛</el-button><el-button size="small" @click="showForecastCartons = true">查看预报箱唛</el-button></div></div> <div class="warehouse-section-heading warehouse-transfer-receipt-heading"><h3>预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)</h3><div class="warehouse-transfer-receipt-actions"><el-button v-if="canIncreaseForecast" type="primary" size="small" @click="openIncreaseForecast"><Plus :size="14" />增加预报箱数</el-button><el-button type="primary" size="small" @click="printTransferCartons(String(selectedDetail?.transferNo || ''))"><Printer :size="14" />打印预报箱唛</el-button><el-button size="small" @click="showForecastCartons = true">查看预报箱唛</el-button></div></div>
<el-dialog v-model="showForecastCartons" title="预报箱唛明细" width="900px" align-center destroy-on-close append-to-body><el-table :data="selectedDetail?.cartons || []" size="small" border><el-table-column prop="cartonMark" label="仓库唛头" min-width="300" /><el-table-column label="预报SKU" min-width="240"><template #default="{ row }">{{ selectedDetail?.items?.find(item => String(item.id) === String(row.transferItemId))?.skuCode || '—' }}</template></el-table-column><el-table-column label="清点状态" width="130"><template #default="{ row }">{{ row.status === 'COUNTED' ? '已清点' : row.status === 'NOT_RECEIVED' ? '未收到' : '待清点' }}</template></el-table-column></el-table><template #footer><el-button @click="showForecastCartons = false">关闭</el-button></template></el-dialog> <el-dialog v-model="showForecastCartons" title="预报箱唛明细" width="900px" align-center destroy-on-close append-to-body><el-table :data="selectedDetail?.cartons || []" size="small" border><el-table-column prop="cartonMark" label="仓库唛头" min-width="300" /><el-table-column label="预报SKU" min-width="240"><template #default="{ row }">{{ selectedDetail?.items?.find(item => String(item.id) === String(row.transferItemId))?.skuCode || '—' }}</template></el-table-column><el-table-column label="清点状态" width="130"><template #default="{ row }">{{ row.status === 'COUNTED' ? '已清点' : row.status === 'NOT_RECEIVED' ? '未收到' : '待清点' }}</template></el-table-column></el-table><template #footer><el-button @click="showForecastCartons = false">关闭</el-button></template></el-dialog>
<el-dialog v-model="increaseForecastOpen" title="增加预报箱数" width="560px" align-center destroy-on-close append-to-body :close-on-click-modal="!increasingForecast" :show-close="!increasingForecast"> <el-dialog v-model="increaseForecastOpen" title="增加预报箱数" width="560px" align-center destroy-on-close append-to-body :close-on-click-modal="!increasingForecast" :show-close="!increasingForecast">
<el-form label-width="110px"><el-form-item label="新增箱数"><el-input-number v-model="increaseForecastForm.additionalCount" placeholder="" :disabled="increasingForecast" :min="1" :max="Math.max(1, 5000 - Number(forecastSnapshot?.packageCount || 0))" :precision="0" /></el-form-item></el-form><p class="warehouse-form-hint">保留原箱唛,仅补生成新增箱唛;箱内SKU和件数以PDA清点为准,SKU预报总件数保持不变。</p> <el-form label-width="110px"><el-form-item label="新增箱数"><el-input-number v-model="increaseForecastForm.additionalCount" placeholder="" :disabled="increasingForecast" :min="1" :max="Math.max(1, 5000 - Number(forecastSnapshot?.packageCount || 0))" :precision="0" /></el-form-item></el-form>
<template #footer><el-button :disabled="increasingForecast" @click="increaseForecastOpen = false">取消</el-button><el-button type="primary" :loading="increasingForecast" @click="saveIncreaseForecast">确认增加</el-button></template> <template #footer><el-button :disabled="increasingForecast" @click="increaseForecastOpen = false">取消</el-button><el-button type="primary" :loading="increasingForecast" @click="saveIncreaseForecast">确认增加</el-button></template>
</el-dialog> </el-dialog>
</div> </div>
@@ -1912,7 +1912,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div class="warehouse-section-heading"><h3>预报与实际签收</h3></div> <div class="warehouse-section-heading"><h3>预报与实际签收</h3></div>
<div class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-comparison-table"><colgroup><col style="width:500px" /><col style="width:140px" /><col style="width:130px" /><col style="width:100px" /><col style="width:130px" /><col style="width:130px" /></colgroup><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预报数量(件)</th><th>预计箱数</th><th>实际签收件数</th><th>实际签收箱数</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="String(item.id || index)"><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.quantity ?? '未记录' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ receivedItemQuantity(item.id) }}</td><td>{{ receivedItemCartons(item.id).length }}</td></tr><tr v-if="!selectedDetail?.items?.length"><td colspan="6"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div> <div class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-comparison-table"><colgroup><col style="width:500px" /><col style="width:140px" /><col style="width:130px" /><col style="width:100px" /><col style="width:130px" /><col style="width:130px" /></colgroup><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预报数量(件)</th><th>预计箱数</th><th>实际签收件数</th><th>实际签收箱数</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="String(item.id || index)"><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.quantity ?? '未记录' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ receivedItemQuantity(item.id) }}</td><td>{{ receivedItemCartons(item.id).length }}</td></tr><tr v-if="!selectedDetail?.items?.length"><td colspan="6"><div class="warehouse-empty">暂无货物明细</div></td></tr></tbody></table></div>
<details class="warehouse-transfer-pallet-group" open> <details class="warehouse-transfer-pallet-group" open>
<summary class="warehouse-section-heading warehouse-transfer-pallet-heading warehouse-transfer-pallet-summary"><h3>PDA 打托实测数据</h3><span class="warehouse-transfer-pallet-count">{{ selectedDetail?.pallets?.length || 0 }} 托</span><span class="warehouse-transfer-pallet-toggle"><span class="when-collapsed">展开托盘</span><span class="when-expanded">收起托盘</span><ChevronDown :size="14" /></span></summary> <summary class="warehouse-section-heading warehouse-transfer-pallet-heading warehouse-transfer-pallet-summary"><h3>托盘实测数据</h3><span class="warehouse-transfer-pallet-count">{{ selectedDetail?.pallets?.length || 0 }} 托</span><span class="warehouse-transfer-pallet-toggle"><span class="when-collapsed">展开托盘</span><span class="when-expanded">收起托盘</span><ChevronDown :size="14" /></span></summary>
<div v-if="selectedDetail?.pallets?.length" class="warehouse-transfer-pallet-list"> <div v-if="selectedDetail?.pallets?.length" class="warehouse-transfer-pallet-list">
<article v-for="pallet in selectedDetail.pallets" :key="String(pallet.id || pallet.sequenceNo)" class="warehouse-transfer-pallet-card"> <article v-for="pallet in selectedDetail.pallets" :key="String(pallet.id || pallet.sequenceNo)" class="warehouse-transfer-pallet-card">
<header class="warehouse-transfer-pallet-header"> <header class="warehouse-transfer-pallet-header">
@@ -1925,13 +1925,13 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div class="warehouse-transfer-pallet-meta"><span>库位:{{ pallet.warehouseLocationCode || '—' }}</span><span>完成时间:{{ formatDateTime(pallet.completedAt) }}</span></div> <div class="warehouse-transfer-pallet-meta"><span>库位:{{ pallet.warehouseLocationCode || '—' }}</span><span>完成时间:{{ formatDateTime(pallet.completedAt) }}</span></div>
</article> </article>
</div> </div>
<div v-else class="warehouse-empty">暂无 PDA 打托测量数据</div> <div v-else class="warehouse-empty">暂无托盘数据</div>
</details> </details>
<div class="warehouse-evidence warehouse-measurement-evidence"><span>测量打托现场照片</span><div v-if="selectedDetail?.measurementPhotos?.length" class="warehouse-photo-grid warehouse-measurement-photo-grid"><div v-for="(photo, index) in selectedDetail.measurementPhotos" :key="photo.fileId" class="warehouse-photo-item warehouse-measurement-photo-item" :title="photo.fileName || `测量照片 ${index + 1}`"><el-image v-if="measurementPhotoUrls[photo.fileId]" :src="measurementPhotoUrls[photo.fileId]" :preview-src-list="measurementPhotoPreviewUrls" :initial-index="measurementPhotoPreviewUrls.indexOf(measurementPhotoUrls[photo.fileId])" :alt="photo.fileName || `测量照片 ${index + 1}`" fit="cover" preview-teleported /><div v-else-if="measurementPhotoLoading[photo.fileId]" class="warehouse-photo-placeholder">照片 {{ index + 1 }} 加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadMeasurementPhoto(photo.fileId)">{{ measurementPhotoErrors[photo.fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div><strong v-else>仓库尚未上传测量打托照片</strong></div> <div v-if="selectedDetail?.measurementPhotos?.length" class="warehouse-evidence warehouse-measurement-evidence"><span>现场照片</span><div v-if="selectedDetail?.measurementPhotos?.length" class="warehouse-photo-grid warehouse-measurement-photo-grid"><div v-for="(photo, index) in selectedDetail.measurementPhotos" :key="photo.fileId" class="warehouse-photo-item warehouse-measurement-photo-item" :title="photo.fileName || `测量照片 ${index + 1}`"><el-image v-if="measurementPhotoUrls[photo.fileId]" :src="measurementPhotoUrls[photo.fileId]" :preview-src-list="measurementPhotoPreviewUrls" :initial-index="measurementPhotoPreviewUrls.indexOf(measurementPhotoUrls[photo.fileId])" :alt="photo.fileName || `测量照片 ${index + 1}`" fit="cover" preview-teleported /><div v-else-if="measurementPhotoLoading[photo.fileId]" class="warehouse-photo-placeholder">照片 {{ index + 1 }} 加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadMeasurementPhoto(photo.fileId)">{{ measurementPhotoErrors[photo.fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div></div>
</div> </div>
<div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-received-card"> <div v-if="props.service === 'transfer'" class="warehouse-card warehouse-form-section warehouse-transfer-received-card">
<div class="warehouse-section-heading warehouse-transfer-receipt-heading"><h3>签收箱号明细({{ countedTransferCartons().length }} 箱)</h3><div class="warehouse-transfer-receipt-actions"><el-button type="primary" size="small" :disabled="!countedTransferCartons().length" @click="showTransferCartons = true">查看箱号明细</el-button></div></div> <div class="warehouse-section-heading warehouse-transfer-receipt-heading"><h3>签收箱号明细({{ countedTransferCartons().length }} 箱)</h3><div class="warehouse-transfer-receipt-actions"><el-button type="primary" size="small" :disabled="!countedTransferCartons().length" @click="showTransferCartons = true">查看箱号明细</el-button></div></div>
<div v-if="!countedTransferCartons().length" class="warehouse-empty">暂无实际签收箱,等待 PDA 清点。</div> <div v-if="!countedTransferCartons().length" class="warehouse-empty">暂无签收箱号</div>
<el-dialog v-model="showTransferCartons" title="签收箱号明细" width="1120px" align-center destroy-on-close append-to-body> <el-dialog v-model="showTransferCartons" title="签收箱号明细" width="1120px" align-center destroy-on-close append-to-body>
<div class="warehouse-cartons-dialog-body"> <div class="warehouse-cartons-dialog-body">
<el-collapse> <el-collapse>
@@ -1963,7 +1963,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div> <div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
<div v-if="selectedRelabelDetail"><RelabelDocumentsPanel inline :relabel-no="selectedRelabelDetail.application.relabelNo" :detail="selectedRelabelDetail" /></div> <div v-if="selectedRelabelDetail"><RelabelDocumentsPanel inline :relabel-no="selectedRelabelDetail.application.relabelNo" :detail="selectedRelabelDetail" /></div>
</div> </div>
<div class="warehouse-evidence"><span>仓库完工照片</span><div v-if="selectedRelabelDetail?.completionEvidence?.photoFileIds?.length" class="warehouse-photo-grid"><div v-for="(fileId, index) in selectedRelabelDetail.completionEvidence.photoFileIds" :key="fileId" class="warehouse-photo-item"><el-image v-if="completionPhotoUrls[fileId]" :src="completionPhotoUrls[fileId]" :preview-src-list="completionPhotoPreviewUrls" :initial-index="completionPhotoPreviewUrls.indexOf(completionPhotoUrls[fileId])" fit="cover" preview-teleported /><div v-else-if="completionPhotoLoading[fileId]" class="warehouse-photo-placeholder">照片 {{ index + 1 }} 加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadCompletionPhoto(fileId)">{{ completionPhotoErrors[fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div><strong v-else>仓库尚未上传完工照片</strong></div> <div v-if="selectedRelabelDetail?.completionEvidence?.photoFileIds?.length" class="warehouse-evidence"><span>仓库完工照片</span><div v-if="selectedRelabelDetail?.completionEvidence?.photoFileIds?.length" class="warehouse-photo-grid"><div v-for="(fileId, index) in selectedRelabelDetail.completionEvidence.photoFileIds" :key="fileId" class="warehouse-photo-item"><el-image v-if="completionPhotoUrls[fileId]" :src="completionPhotoUrls[fileId]" :preview-src-list="completionPhotoPreviewUrls" :initial-index="completionPhotoPreviewUrls.indexOf(completionPhotoUrls[fileId])" fit="cover" preview-teleported /><div v-else-if="completionPhotoLoading[fileId]" class="warehouse-photo-placeholder">照片 {{ index + 1 }} 加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadCompletionPhoto(fileId)">{{ completionPhotoErrors[fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div></div>
<p v-if="selectedRelabelDetail?.completionEvidence?.completionRemark" class="warehouse-completion-remark">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p> <p v-if="selectedRelabelDetail?.completionEvidence?.completionRemark" class="warehouse-completion-remark">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p>
</div> </div>
<div v-if="props.service === 'transfer' && selectedDetail?.relabelNo" class="warehouse-card warehouse-form-section"><RelabelDocumentsPanel :relabel-no="selectedDetail.relabelNo" /></div> <div v-if="props.service === 'transfer' && selectedDetail?.relabelNo" class="warehouse-card warehouse-form-section"><RelabelDocumentsPanel :relabel-no="selectedDetail.relabelNo" /></div>
@@ -268,7 +268,7 @@ test('PDA 打托完成后客户可从列表或详情确认并查看实测数据'
assert.match(view, /item\.__status === 'WAIT_CUSTOMER_CONFIRMATION'/) assert.match(view, /item\.__status === 'WAIT_CUSTOMER_CONFIRMATION'/)
assert.match(view, /confirmTransferFromList\(item\['0'\]\)/) assert.match(view, /confirmTransferFromList\(item\['0'\]\)/)
assert.match(view, /selectedDetail\?\.status === 'WAIT_CUSTOMER_CONFIRMATION'/) assert.match(view, /selectedDetail\?\.status === 'WAIT_CUSTOMER_CONFIRMATION'/)
assert.match(view, /PDA 打托实测数据/) assert.match(view, /托盘实测数据/)
for (const label of ['托盘号', '库位', '箱数', 'SKU 汇总', '毛重(kg)', '体积(m³)', '完成时间']) assert.equal(view.includes(label), true) for (const label of ['托盘号', '库位', '箱数', 'SKU 汇总', '毛重(kg)', '体积(m³)', '完成时间']) assert.equal(view.includes(label), true)
assert.match(view, /pallet\.warehouseLocationCode/) assert.match(view, /pallet\.warehouseLocationCode/)
assert.match(view, /palletCartons\(pallet\)\.length/) assert.match(view, /palletCartons\(pallet\)\.length/)
@@ -281,7 +281,7 @@ test('仓储转运详情按订单概览、货物预报、PDA 实测数据排序
const detailStart = view.indexOf('<header class="warehouse-detail-header">') const detailStart = view.indexOf('<header class="warehouse-detail-header">')
const detailSource = view.slice(detailStart) const detailSource = view.slice(detailStart)
const overviewIndex = detailSource.indexOf('<h3>订单概览</h3>') const overviewIndex = detailSource.indexOf('<h3>订单概览</h3>')
const measurementIndex = detailSource.indexOf('<h3>PDA 打托实测数据</h3>') const measurementIndex = detailSource.indexOf('<h3>托盘实测数据</h3>')
const cargoIndex = detailSource.indexOf('<h3>预报与实际签收</h3>') const cargoIndex = detailSource.indexOf('<h3>预报与实际签收</h3>')
assert.ok(overviewIndex >= 0 && overviewIndex < measurementIndex) assert.ok(overviewIndex >= 0 && overviewIndex < measurementIndex)
assert.ok(cargoIndex < measurementIndex) assert.ok(cargoIndex < measurementIndex)
@@ -297,7 +297,7 @@ test('仓储转运详情自动加载 PDA 测量照片并释放 Blob URL', () =>
assert.match(view, /measurementPhotos\?: TransferMeasurementPhoto\[\]/) assert.match(view, /measurementPhotos\?: TransferMeasurementPhoto\[\]/)
assert.match(view, /fileId: string \| number/) assert.match(view, /fileId: string \| number/)
assert.match(view, /if \(props\.service === 'transfer'\) void loadMeasurementPhotos\(\)/) assert.match(view, /if \(props\.service === 'transfer'\) void loadMeasurementPhotos\(\)/)
assert.match(view, /测量打托现场照片/) assert.match(view, /v-if="selectedDetail\?\.measurementPhotos\?\.length" class="warehouse-evidence warehouse-measurement-evidence"/)
assert.match(view, /:preview-src-list="measurementPhotoPreviewUrls"/) assert.match(view, /:preview-src-list="measurementPhotoPreviewUrls"/)
assert.match(view, /加载失败,点击重试/) assert.match(view, /加载失败,点击重试/)
assert.match(view, /URL\.createObjectURL\(blob\)/) assert.match(view, /URL\.createObjectURL\(blob\)/)
@@ -386,7 +386,7 @@ test('转运客户详情保留预报并显示实际签收箱内明细和托盘
const receiptSection = view.slice(view.indexOf('warehouse-transfer-received-card', view.indexOf('<header class="warehouse-detail-header">')), view.indexOf('v-if="props.service === \'transfer\' && selectedDetail?.needRelabel"')) const receiptSection = view.slice(view.indexOf('warehouse-transfer-received-card', view.indexOf('<header class="warehouse-detail-header">')), view.indexOf('v-if="props.service === \'transfer\' && selectedDetail?.needRelabel"'))
assert.doesNotMatch(receiptSection.split('<el-dialog')[0], /<table|<el-table|<el-collapse/) assert.doesNotMatch(receiptSection.split('<el-dialog')[0], /<table|<el-table|<el-collapse/)
assert.match(view, /暂无实际签收箱,等待 PDA 清点/) assert.match(view, /暂无签收箱号/)
assert.doesNotMatch(view, /<td>\{\{ \[pallet.lengthCm/) assert.doesNotMatch(view, /<td>\{\{ \[pallet.lengthCm/)
}) })
@@ -395,7 +395,7 @@ test('预报实收同区对照、托盘卡片换行、签收按钮紧邻标题',
const start = view.indexOf('class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card"', view.indexOf('<header class="warehouse-detail-header">')) const start = view.indexOf('class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card"', view.indexOf('<header class="warehouse-detail-header">'))
const section = view.slice(start, view.indexOf('warehouse-transfer-received-card', start)) const section = view.slice(start, view.indexOf('warehouse-transfer-received-card', start))
assert.ok(section.includes('预报与实际签收')) assert.ok(section.includes('预报与实际签收'))
assert.ok(section.includes('PDA 打托实测数据')) assert.ok(section.includes('托盘实测数据'))
assert.ok(section.includes('receivedItemQuantity(item.id)')) assert.ok(section.includes('receivedItemQuantity(item.id)'))
assert.ok(section.includes('receivedItemCartons(item.id).length')) assert.ok(section.includes('receivedItemCartons(item.id).length'))
assert.ok(section.includes('class="warehouse-transfer-pallet-card"')) assert.ok(section.includes('class="warehouse-transfer-pallet-card"'))