解决客户工作台合并冲突
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
|
||||
## 模块快速查询
|
||||
|
||||
退货预报与签收结果:`src/modules/overseas-warehouse/views/ReturnOrdersView.vue`、`api/returnOrders.ts`;菜单仍名为“退货换标”,正式路由 `return-relabel` 已使用独立页面,不再由 `WarehouseServiceView.vue` 承载。客户退货库存/箱号共用 `views/ReturnInventoryView.vue`、`api/returnInventory.ts`,`return-cartons` 传 `cartonOnly=true`。Backend 为 Lastleg `returnreceipt/`,退货单签收完成不等于换标或退货出库完成;后续换标、出库须按各自业务状态核对。
|
||||
退货预报与签收结果:`src/modules/overseas-warehouse/views/ReturnOrdersView.vue`、`api/returnOrders.ts`;菜单仍名为“退货换标”。列表路由 `return-relabel`、独立新建路由 `return-relabel/create` 与详情路由均复用该页面,并分别由 `createPage/detailPage` 明确模式;新建页直接填写海外仓、联系人、客户参考号及 SKU 预报明细,取消或提交后返回列表,不再由 `WarehouseServiceView.vue` 或列表内弹框承载。客户退货库存/箱号共用 `views/ReturnInventoryView.vue`、`api/returnInventory.ts`,`return-cartons` 传 `cartonOnly=true`。Backend 为 Lastleg `returnreceipt/`,退货单签收完成不等于换标或退货出库完成;后续换标、出库须按各自业务状态核对。
|
||||
|
||||
先从 `src/modules/registry.ts` 确认模块,再按目标模块的 `routes.ts → views/ → api/、store/、contracts.ts 或 public.ts` 查询。页面是否正式开放以 `routes.ts` 的 `release` 为准,不从任务或进度文档推断。
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
## UI 与交互
|
||||
|
||||
- 修改已有 Client 原型页面前先读 `../../docs/Admin-Client前端原型还原验收规则.md`,按页面和操作形成原型核对矩阵;本节维护 Client 的实现约束,不复制跨前端验收文档全文。
|
||||
- 视觉还原、布局/交互承载方式变更或真实页面验收时读取 `../../docs/Admin-Client前端原型还原验收规则.md`,只为受影响操作形成原型核对矩阵;纯文案、逻辑或测试修改不自动读取。自动检查与人工视觉验收分开报告,无视觉变化时标记“无需视觉验收”。
|
||||
- 已向客户讲解或确认过的产品原型是页面视觉与布局实现的直接事实来源。字段顺序、字段分组、字段摆放、列宽、控件宽高、对齐方式、换行方式、区块层级、上下左右边距、内边距、间隙、留白、圆角、边框、阴影、背景色、文字颜色和字号均必须按原型还原,不得只参考大致风格后自行重新排版。
|
||||
- 原型中的按钮位置、顺序、尺寸、圆角、边框、背景色、文字色、图标、悬停态、激活态、禁用态和危险语义必须直接按原型实现;不得擅自改为文字链接、不同颜色、不同按钮类型、折叠菜单或自创交互效果,也不得增加原型没有的上浮、缩放、渐变线、装饰图标或其他视觉元素。
|
||||
- 原型已经给出明确图标、字符图标、图片资源或按钮内图标时,直接使用原型中的同一方案和资源,不得以 Lucide、Element Plus 默认图标、手绘图标或个人偏好替换。仅当原型没有定义对应图标时,才按本节的默认图标语义补齐。
|
||||
|
||||
@@ -296,6 +296,8 @@ export interface MerchantFirstLegOrderRow {
|
||||
inboundedAt: string | null
|
||||
businessDate: string | null
|
||||
shipmentId: string | null
|
||||
fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null
|
||||
lxCode?: string | null
|
||||
createdAt: string
|
||||
cargoes?: Array<{ skuNo?: string | null }>
|
||||
}
|
||||
|
||||
@@ -25,14 +25,13 @@ test('supports a standalone dialog and keeps the unpushed billing-weight state v
|
||||
})
|
||||
|
||||
test('shows the published merchant inbound, measurement details and customer confirmation entry', () => {
|
||||
for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '对客单价', '基础费用', '附加费用', '费用合计', '平台备注']) {
|
||||
assert.match(source, new RegExp(label))
|
||||
for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '单价', '基础费用', '附加费用', '费用合计', '平台备注']) {
|
||||
assert.ok(source.includes(label), `missing label: ${label}`)
|
||||
}
|
||||
assert.match(source, /formatInboundMoney\(props\.data\.latestFee\.totalAmount/)
|
||||
assert.match(source, /formatConversionPrecision\(/)
|
||||
assert.match(source, /确认费用/)
|
||||
assert.match(source, /二次确认费用/)
|
||||
assert.match(source, /确认后该费用将锁定,平台不能再修改/)
|
||||
assert.match(source, /确认后当前费用版本将锁定/)
|
||||
assert.match(source, /customerConfirmationStatus === 'CONFIRMED'/)
|
||||
assert.match(source, /confirmMerchantInboundFee/)
|
||||
assert.match(source, /measurementItems/)
|
||||
@@ -42,7 +41,19 @@ test('shows the published merchant inbound, measurement details and customer con
|
||||
assert.match(source, /latestFee\.rateCode/)
|
||||
assert.match(source, /latestFee\.chargeRuleVersion/)
|
||||
assert.match(source, /latestFee\.carrierRateVersion/)
|
||||
assert.doesNotMatch(source, /箱规规则|calculationRuleVersion/)
|
||||
assert.match(source, /箱规规则/)
|
||||
assert.match(source, /getCalculationRuleLabel\(row\.calculationRuleVersion\)/)
|
||||
})
|
||||
|
||||
test('matches the confirmed billing prototype summary, table, notice and footer', () => {
|
||||
assert.match(source, /width="min\(1180px, calc\(100vw - 40px\)\)"/)
|
||||
assert.match(source, /class="fee-dialog-summary" aria-label="计费汇总"/)
|
||||
for (const label of ['计算重', '单价', '总金额', '箱规长(cm)', '箱规宽(cm)', '箱规高(cm)', '每箱体积', '总体积', '总材积重']) assert.ok(source.includes(label), `missing prototype label: ${label}`)
|
||||
assert.match(source, /class="fee-confirm-notice"/)
|
||||
assert.match(source, /请核对计算重、单价和总金额。确认后当前费用版本将锁定。/)
|
||||
assert.match(source, /确认计费量和费用/)
|
||||
assert.match(source, /\.fee-dialog-summary \{ display:grid; grid-template-columns:repeat\(3,minmax\(0,1fr\)\)/)
|
||||
assert.match(source, /\.fee-dialog-table :deep\(\.el-table__body td\.el-table__cell\)/)
|
||||
})
|
||||
|
||||
test('stacks core information for 720 and 360 pixel layouts', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Clock3, PackageCheck, RefreshCw, TriangleAlert } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CircleAlert, Clock3, PackageCheck, RefreshCw, TriangleAlert } from '@lucide/vue'
|
||||
import { confirmMerchantInboundFee } from '@/modules/first-leg/api/inboundBilling'
|
||||
import type { MerchantInboundBilling } from '@/modules/first-leg/types/inboundBilling'
|
||||
import {
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
formatInboundMeasure,
|
||||
formatInboundMoney,
|
||||
formatInboundNumber,
|
||||
getCalculationRuleLabel,
|
||||
getChargeModeLabel,
|
||||
getTaxModeLabel,
|
||||
} from '@/modules/first-leg/utils/inboundBilling'
|
||||
@@ -54,15 +55,6 @@ async function confirmFee() {
|
||||
ElMessage.info('当前费用已经确认,无需重复操作')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`请再次确认费用单 ${fee.feeNo},合计 ${formatInboundMoney(fee.totalAmount, fee.currency)}。确认后该费用将锁定,平台不能再修改。`,
|
||||
'二次确认费用',
|
||||
{ type: 'warning', confirmButtonText: '确认费用无误', cancelButtonText: '返回核对' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
confirming.value = true
|
||||
try {
|
||||
await confirmMerchantInboundFee(orderId, fee.feeVersion)
|
||||
@@ -139,6 +131,7 @@ async function confirmFee() {
|
||||
<el-table-column label="每箱体积" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.unitVolumeCbm, 'm³') }}</template></el-table-column>
|
||||
<el-table-column label="总体积" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.totalVolumeCbm, 'm³') }}</template></el-table-column>
|
||||
<el-table-column label="总材积重" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.volumeWeightKg, 'kg') }}</template></el-table-column>
|
||||
<el-table-column label="箱规规则" min-width="170"><template #default="{ row }">{{ getCalculationRuleLabel(row.calculationRuleVersion) }}</template></el-table-column>
|
||||
<el-table-column prop="boxSpecification" label="备注" min-width="130" />
|
||||
</el-table>
|
||||
</div>
|
||||
@@ -205,7 +198,7 @@ async function confirmFee() {
|
||||
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="feeDialogOpen" title="确认测量与计费数据" width="min(980px, 94vw)" append-to-body @closed="emit('closed')">
|
||||
<el-dialog v-model="feeDialogOpen" class="billing-confirm-dialog" title="确认测量与计费数据" width="min(1180px, calc(100vw - 40px))" append-to-body @closed="emit('closed')">
|
||||
<div v-if="props.loading" class="inbound-billing-loading" aria-live="polite" aria-busy="true">
|
||||
<el-skeleton :rows="4" animated />
|
||||
<span class="sr-only">正在加载计费重</span>
|
||||
@@ -220,19 +213,12 @@ async function confirmFee() {
|
||||
<div><strong>{{ props.unavailableMessage }}</strong><span>平台推送计费重后,可在这里核对并确认。</span></div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<dl class="inbound-measurements fee-dialog-summary">
|
||||
<div><dt>计费总箱数</dt><dd>{{ formatInboundNumber(props.data.measurementSummary.actualPackageCount, 0) }}</dd></div>
|
||||
<div><dt>计费总毛重</dt><dd>{{ formatInboundMeasure(props.data.measurementSummary.actualGrossWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>外箱规总体积</dt><dd>{{ formatInboundMeasure(props.data.measurementSummary.actualVolumeCbm, 'm³') }}</dd></div>
|
||||
<div><dt>外箱规总材积重</dt><dd>{{ formatInboundMeasure(props.data.measurementSummary.volumeWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>计费体积</dt><dd>{{ formatInboundMeasure(props.data.billingBasis.chargeableVolumeCbm, 'm³') }}</dd></div>
|
||||
<div><dt>计费重量</dt><dd>{{ formatInboundMeasure(props.data.billingBasis.chargeableWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>对客单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit }}</dd></div>
|
||||
<div><dt>基础费用</dt><dd>{{ formatInboundMoney(props.data.latestFee.subtotalAmount, props.data.latestFee.currency) }}</dd></div>
|
||||
<div><dt>附加费用</dt><dd>{{ formatInboundMoney(props.data.latestFee.additionalAmount, props.data.latestFee.currency) }}</dd></div>
|
||||
<div><dt>费用合计</dt><dd>{{ formatInboundMoney(props.data.latestFee.totalAmount, props.data.latestFee.currency) }}</dd></div>
|
||||
<dl class="fee-dialog-summary" aria-label="计费汇总">
|
||||
<div><dt>计算重</dt><dd>{{ formatInboundMeasure(props.data.billingBasis.chargeableWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit.toUpperCase() }}</dd></div>
|
||||
<div><dt>总金额</dt><dd>{{ formatInboundMoney(props.data.latestFee.totalAmount, props.data.latestFee.currency) }}</dd></div>
|
||||
</dl>
|
||||
<el-table v-if="props.data?.measurementItems.length" class="measurement-detail-table" :data="props.data.measurementItems" border size="small">
|
||||
<el-table class="measurement-detail-table fee-dialog-table" :data="props.data.measurementItems" border empty-text="暂无箱规测量明细">
|
||||
<el-table-column type="index" label="#" width="54" />
|
||||
<el-table-column prop="lengthCm" label="箱规长(cm)" min-width="105" />
|
||||
<el-table-column prop="widthCm" label="箱规宽(cm)" min-width="105" />
|
||||
@@ -241,17 +227,14 @@ async function confirmFee() {
|
||||
<el-table-column label="每箱体积" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.unitVolumeCbm, 'm³') }}</template></el-table-column>
|
||||
<el-table-column label="总体积" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.totalVolumeCbm, 'm³') }}</template></el-table-column>
|
||||
<el-table-column label="总材积重" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.volumeWeightKg, 'kg') }}</template></el-table-column>
|
||||
<el-table-column label="箱规规则" min-width="170"><template #default="{ row }">{{ getCalculationRuleLabel(row.calculationRuleVersion) }}</template></el-table-column>
|
||||
<el-table-column prop="boxSpecification" label="备注" min-width="130" />
|
||||
</el-table>
|
||||
<el-alert
|
||||
v-if="props.data.latestFee.customerConfirmationStatus === 'CONFIRMED'"
|
||||
class="fee-confirmed-alert"
|
||||
:title="`客户已于 ${formatInboundDateTime(props.data.latestFee.customerConfirmedAt)} 确认,费用已锁定,不允许修改。`"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert v-else class="fee-confirmed-alert" title="请核对计费量、对客单价和费用合计。确认后当前费用版本将锁定。" type="warning" :closable="false" show-icon />
|
||||
<div class="fee-confirm-notice" :class="{ 'is-confirmed': props.data.latestFee.customerConfirmationStatus === 'CONFIRMED' }" role="status">
|
||||
<CircleAlert :size="21" aria-hidden="true" />
|
||||
<span v-if="props.data.latestFee.customerConfirmationStatus === 'CONFIRMED'">客户已于 {{ formatInboundDateTime(props.data.latestFee.customerConfirmedAt) }} 确认,费用已锁定,不允许修改。</span>
|
||||
<span v-else>请核对计算重、单价和总金额。确认后当前费用版本将锁定。</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="feeDialogOpen = false">关闭</el-button>
|
||||
@@ -287,8 +270,25 @@ async function confirmFee() {
|
||||
.inbound-measurements { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 10px 0 0; background: #f6f8f8; }
|
||||
.inbound-measurements > div { min-width: 0; padding: 12px; }
|
||||
.measurement-detail-table { margin-top: 12px; }
|
||||
.fee-dialog-summary { margin-top: 0; }
|
||||
.fee-confirmed-alert { margin-top: 14px; }
|
||||
.fee-dialog-summary { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; margin:0; }
|
||||
.fee-dialog-summary > div { position:relative; min-width:0; overflow:hidden; padding:13px 17px; border:1px solid #e4e8f2; border-radius:11px; background:linear-gradient(145deg,#fff,#f8f9fc); box-shadow:0 5px 16px rgba(51,65,85,.04); }
|
||||
.fee-dialog-summary > div::before { position:absolute; inset:0 auto 0 0; width:4px; background:#a7b4ef; content:''; }
|
||||
.fee-dialog-summary > div:nth-child(2)::before { background:#7d91eb; }
|
||||
.fee-dialog-summary > div:nth-child(3) { border-color:#cdd7ff; background:linear-gradient(145deg,#f8faff,#eef2ff); box-shadow:0 8px 22px rgba(82,110,232,.09); }
|
||||
.fee-dialog-summary > div:nth-child(3)::before { background:#526ee8; }
|
||||
.fee-dialog-summary dt { margin:0 0 3px; color:#7b8494; font-size:13px; font-weight:500; }
|
||||
.fee-dialog-summary dd { margin:0; overflow-wrap:anywhere; color:#283246; font-size:20px; font-weight:700; font-variant-numeric:tabular-nums; letter-spacing:0; }
|
||||
.fee-dialog-summary > div:nth-child(3) dt { color:#6578c5; }
|
||||
.fee-dialog-summary > div:nth-child(3) dd { color:#405fd8; font-size:22px; }
|
||||
.fee-dialog-table { margin-top:16px; overflow:hidden; border-radius:10px; box-shadow:0 5px 18px rgba(51,65,85,.04); }
|
||||
.fee-dialog-table :deep(.el-table__header th.el-table__cell) { height:42px; padding:9px 0; background:linear-gradient(180deg,#f8f9fc,#f3f5f9); color:#697386; font-size:13px; font-weight:650; }
|
||||
.fee-dialog-table :deep(.el-table__body td.el-table__cell) { height:82px; padding:9px 0; color:#515a6c; font-size:14px; font-variant-numeric:tabular-nums; }
|
||||
.fee-dialog-table :deep(.el-table__body td:nth-child(7)), .fee-dialog-table :deep(.el-table__body td:nth-child(8)) { color:#34466b; font-weight:650; }
|
||||
.fee-dialog-table :deep(.cell) { line-height:1.6; }
|
||||
.fee-confirm-notice { display:flex; align-items:center; gap:10px; margin-top:14px; padding:10px 14px; border:1px solid #f5dfba; border-radius:9px; background:linear-gradient(90deg,#fff9ef,#fffaf2); color:#c6790c; font-size:14px; font-weight:500; }
|
||||
.fee-confirm-notice > svg { flex:0 0 auto; fill:#eda332; color:#fff; filter:drop-shadow(0 2px 3px rgba(233,154,37,.25)); }
|
||||
.fee-confirm-notice.is-confirmed { border-color:#b7dfc2; background:#f1faf3; color:#2d8a46; }
|
||||
.fee-confirm-notice.is-confirmed > svg { fill:#4eae67; }
|
||||
.pallet-location-list { margin-top: 9px; border-top: 1px solid var(--border); }
|
||||
.pallet-location-list article { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 11px; padding: 12px 0; border-bottom: 1px solid var(--border); }
|
||||
.pallet-location-list article > svg { color: var(--primary); }
|
||||
@@ -312,7 +312,26 @@ async function confirmFee() {
|
||||
.inbound-public-remark strong { color: var(--navy); font-size: 11px; }
|
||||
.inbound-public-remark p { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; white-space: pre-wrap; }
|
||||
|
||||
:global(.billing-confirm-dialog) { max-height:calc(100vh - 40px); overflow:hidden; border-radius:14px; box-shadow:0 28px 90px rgba(17,24,39,.33); }
|
||||
:global(.billing-confirm-dialog .el-dialog__header) { display:flex; align-items:center; min-height:66px; margin:0; padding:16px 22px 14px; border-bottom:1px solid #edf0f5; }
|
||||
:global(.billing-confirm-dialog .el-dialog__title) { color:#252b38; font-size:22px; font-weight:700; line-height:1.4; letter-spacing:0; }
|
||||
:global(.billing-confirm-dialog .el-dialog__headerbtn) { top:14px; right:20px; width:36px; height:36px; border-radius:50%; background:#f5f6f9; }
|
||||
:global(.billing-confirm-dialog .el-dialog__headerbtn:hover) { background:#e9edfa; }
|
||||
:global(.billing-confirm-dialog .el-dialog__headerbtn .el-dialog__close) { color:#8b92a0; font-size:22px; }
|
||||
:global(.billing-confirm-dialog .el-dialog__body) { max-height:calc(100vh - 174px); overflow:auto; padding:18px 22px; }
|
||||
:global(.billing-confirm-dialog .el-dialog__footer) { padding:14px 22px 16px; border-top:1px solid #edf0f5; }
|
||||
:global(.billing-confirm-dialog .el-dialog__footer .el-button) { min-height:40px; padding:0 20px; border-radius:8px; font-size:14px; font-weight:650; }
|
||||
:global(.billing-confirm-dialog .el-dialog__footer .el-button--primary) { min-width:168px; border-color:#526ee8; background:linear-gradient(135deg,#647fec,#526ee8); box-shadow:0 7px 16px rgba(82,110,232,.17); }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.fee-dialog-summary { grid-template-columns: 1fr; gap: 8px; }
|
||||
.fee-dialog-summary dd, .fee-dialog-summary > div:nth-child(3) dd { font-size: 18px; }
|
||||
:global(.billing-confirm-dialog) { width:calc(100vw - 16px) !important; max-height:calc(100vh - 16px); }
|
||||
:global(.billing-confirm-dialog .el-dialog__header) { padding:14px 16px 11px; }
|
||||
:global(.billing-confirm-dialog .el-dialog__title) { font-size:19px; }
|
||||
:global(.billing-confirm-dialog .el-dialog__body) { max-height:calc(100vh - 145px); padding:14px 16px; }
|
||||
:global(.billing-confirm-dialog .el-dialog__footer) { padding:12px 16px; }
|
||||
:global(.billing-confirm-dialog .el-dialog__footer .el-button) { min-height:38px; padding:0 14px; font-size:13px; }
|
||||
.inbound-billing-state { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.inbound-billing-state .el-button { grid-column: 1 / -1; width: 100%; min-height: 44px; }
|
||||
.inbound-overview, .inbound-measurements, .fee-calculation-grid { grid-template-columns: 1fr; }
|
||||
|
||||
@@ -12,7 +12,7 @@ test('maps the first-leg aggregate list fields without loading detail rows', ()
|
||||
assert.match(apiSource, /cargoes\?: Array<\{ skuNo\?: string \| null \}>/)
|
||||
assert.match(apiSource, /apiRequest<PageResult<MerchantFirstLegOrderRow>>/)
|
||||
assert.match(storeSource, /function mapFirstLegOrderRow/)
|
||||
for (const field of ['inboundId', 'inboundCode', 'channelName', 'destinationWarehouseName', 'pickupMethod', 'packageCount', 'expressCompany', 'trackingNo', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'remark', 'shipmentId', 'referenceId', 'createdAt']) {
|
||||
for (const field of ['inboundId', 'inboundCode', 'channelName', 'destinationWarehouseName', 'pickupMethod', 'packageCount', 'expressCompany', 'trackingNo', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'remark', 'shipmentId', 'fulfillmentType', 'lxCode', 'referenceId', 'createdAt']) {
|
||||
assert.match(storeSource, new RegExp(`row\\.${field}`))
|
||||
}
|
||||
assert.match(storeSource, /row\.skuNos/)
|
||||
@@ -102,7 +102,9 @@ test('keeps unified order pagination valid when the store omits page and size',
|
||||
|
||||
test('keeps drafts on the legacy read path without assigning a unified main status', () => {
|
||||
assert.match(storeSource, /getMerchantFirstLegOrders\(legacyQuery\(query, 'DRAFT'\)\)/)
|
||||
assert.match(storeSource, /getById\(id\)\?\.status === 'DRAFT' \? loadLegacyDetail\(id\) : loadUnifiedDetail\(id\)/)
|
||||
assert.match(storeSource, /const listOrder = getById\(id\)/)
|
||||
assert.match(storeSource, /listOrder\?\.status === 'DRAFT' \? loadLegacyDetail\(id, listOrder\) : loadUnifiedDetail\(id\)/)
|
||||
assert.match(storeSource, /detail\.order\.lxCode == null && listOrder\?\.lxCode/)
|
||||
assert.match(storeSource, /return loadLegacyDetail\(String\(createdId\)\)/)
|
||||
assert.match(storeSource, /draftLoadError/)
|
||||
})
|
||||
|
||||
@@ -359,7 +359,7 @@ function mapFirstLegOrderRow(row: MerchantFirstLegOrderRow): FirstLegOrder {
|
||||
const uniqueSkuNos = [...new Set(resolvedSkuNos)]
|
||||
return {
|
||||
id: String(row.orderId), orderNo: row.orderNo, referenceNo: row.referenceId ?? '', transportMode: null,
|
||||
businessType: 'EXPORT', origin: '', destination: row.destinationWarehouseName ?? '', receivingWarehouseId: '',
|
||||
businessType: 'EXPORT', fulfillmentType: row.fulfillmentType ?? 'STANDARD', lxCode: row.lxCode ?? '', origin: '', destination: row.destinationWarehouseName ?? '', receivingWarehouseId: '',
|
||||
receivingWarehouseName: '', receivingWarehouseSnapshot: null, expectedDepartureDate: '', etd: '', eta: '',
|
||||
shipperPartyId: '', shipperPartySnapshot: null, shipper: '', consigneePartyId: '', consigneePartySnapshot: null,
|
||||
consignee: row.receivingCompanyName ?? '', expressCompanyName: row.expressCompany ?? '', trackingNumber: row.trackingNo ?? '',
|
||||
@@ -542,9 +542,19 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
|
||||
return mapped
|
||||
}
|
||||
|
||||
async function loadLegacyDetail(id: string) {
|
||||
async function loadLegacyDetail(id: string, listOrder?: FirstLegOrder) {
|
||||
const detail = await getMerchantFirstLegOrder(id)
|
||||
return upsertOrder(mapOrder(detail.order, warehouses.value, detail))
|
||||
const mapped = mapOrder(detail.order, warehouses.value, detail)
|
||||
// Older detail responses may omit newly added dropship fields. Keep the
|
||||
// values already returned by the list row so editing a draft does not
|
||||
// clear the persisted LX code in the form.
|
||||
if (detail.order.fulfillmentType == null && listOrder?.fulfillmentType) {
|
||||
mapped.fulfillmentType = listOrder.fulfillmentType
|
||||
}
|
||||
if (detail.order.lxCode == null && listOrder?.lxCode) {
|
||||
mapped.lxCode = listOrder.lxCode
|
||||
}
|
||||
return upsertOrder(mapped)
|
||||
}
|
||||
|
||||
async function loadUnifiedDetail(id: string) {
|
||||
@@ -558,7 +568,8 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
|
||||
}
|
||||
|
||||
async function loadDetail(id: string) {
|
||||
return getById(id)?.status === 'DRAFT' ? loadLegacyDetail(id) : loadUnifiedDetail(id)
|
||||
const listOrder = getById(id)
|
||||
return listOrder?.status === 'DRAFT' ? loadLegacyDetail(id, listOrder) : loadUnifiedDetail(id)
|
||||
}
|
||||
|
||||
async function loadWarehouseDetail(id: string) {
|
||||
@@ -616,7 +627,7 @@ export const useFirstLegOrdersStore = defineStore('client-first-leg-orders', ()
|
||||
}
|
||||
|
||||
async function loadDraftDetail(id: string) {
|
||||
return loadLegacyDetail(id)
|
||||
return loadLegacyDetail(id, getById(id))
|
||||
}
|
||||
|
||||
return { orders, warehouses, loading, loadedFromApi, loadError, warehouseLoadError, draftLoadError, activeCount, pendingCount, getById, load, loadDetail, loadDraftDetail, loadWarehouseDetail, loadWarehouseType, saveDraft, saveDraftBatch, submitSaved, submit }
|
||||
|
||||
@@ -19,6 +19,13 @@ test('translates backend rounding modes into customer-facing calculation labels'
|
||||
assert.match(source, /return value \? labels\[value\] \|\| value : '-'/)
|
||||
})
|
||||
|
||||
test('translates carton calculation rules into customer-facing labels', () => {
|
||||
assert.match(source, /export function getCalculationRuleLabel/)
|
||||
assert.match(source, /ACTUAL_DIMENSION_V1/)
|
||||
assert.match(source, /按实际箱规计算/)
|
||||
assert.match(source, /CUSTOMER_DIMENSION_\(SMALL\|MEDIUM\|LARGE\|EXTRA_LARGE\)_V/)
|
||||
})
|
||||
|
||||
test('renders conversion metadata as one complete label or a single empty marker', () => {
|
||||
assert.match(source, /export function formatConversionPrecision/)
|
||||
assert.match(source, /roundingScale === null \|\| roundingScale === undefined/)
|
||||
|
||||
@@ -46,6 +46,20 @@ export function getTaxModeLabel(value: string) {
|
||||
return value || '-'
|
||||
}
|
||||
|
||||
export function getCalculationRuleLabel(value: string | null | undefined) {
|
||||
if (!value) return '-'
|
||||
if (value === 'ACTUAL_DIMENSION_V1' || value === 'DIMENSION_ACTUAL_V1') return '按实际箱规计算'
|
||||
const customerRule = /^CUSTOMER_DIMENSION_(SMALL|MEDIUM|LARGE|EXTRA_LARGE)_V(\d+)$/.exec(value)
|
||||
if (customerRule) {
|
||||
const sizeLabel: Record<string, string> = {
|
||||
SMALL: '小号', MEDIUM: '中号', LARGE: '大号', EXTRA_LARGE: '超大号',
|
||||
}
|
||||
return `客户箱规规则(${sizeLabel[customerRule[1]]},V${customerRule[2]})`
|
||||
}
|
||||
const warehouseRule = /^WAREHOUSE_OUTER_OVERRIDE_(.+)$/.exec(value)
|
||||
return warehouseRule ? `仓库外箱规覆盖(${warehouseRule[1]})` : value
|
||||
}
|
||||
|
||||
export function getRoundingModeLabel(value: string | null | undefined) {
|
||||
const labels: Record<string, string> = {
|
||||
UP: '向上舍入',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { overseasApiRequest, overseasBlobRequest } from '@/api/overseasHttp'
|
||||
import type { PageResult } from './warehouseServices'
|
||||
|
||||
export interface ReturnOrderSummary {
|
||||
relabelApplications?: Array<{ relabelNo: string; status: string; quantity: number; createdAt: 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
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface RelabelInventoryCandidate {
|
||||
availablePackages: number | string; version: number
|
||||
positions: Array<{ positionId: string; palletNo?: string; batchNo?: string; warehouseLocationCode?: string; availablePackages: number }>
|
||||
sourceType?: 'INBOUND' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string
|
||||
skuItems?: Array<{ skuCode: string; newSkuCode?: string; quantity: number; remark?: string }>
|
||||
skuItems?: Array<{ skuCode: string; newSkuCode?: string; quantity: number; remark?: string }>; relabelledPackages?: number
|
||||
}
|
||||
|
||||
export interface TransferCreate {
|
||||
|
||||
@@ -32,7 +32,7 @@ test('opens warehouse service and return receipt details as dedicated protected
|
||||
assert.match(routes, /path: 'overseas-warehouse\/return-relabel\/:returnNo\/detail'[\s\S]*?name: 'overseas-return-relabel-detail'[\s\S]*?props: \{ detailPage: true \}[\s\S]*?permission: 'merchant:overseas-warehouse:return-order:list'/)
|
||||
assert.match(routes, /path: 'overseas-warehouse\/storage-transfer\/:detailNo\/detail'[\s\S]*?name: 'overseas-storage-transfer-detail'[\s\S]*?props: \{ service: 'transfer', detailPage: true \}/)
|
||||
assert.match(routes, /path: 'overseas-warehouse\/cargo-disposal\/:detailNo\/detail'[\s\S]*?name: 'overseas-cargo-disposal-detail'[\s\S]*?props: \{ service: 'disposal', detailPage: true \}/)
|
||||
assert.match(services, /name: 'overseas-labeling-detail'/)
|
||||
assert.match(services, /labeling: 'overseas-labeling-detail'/)
|
||||
assert.match(services, /transfer: 'overseas-storage-transfer-detail'/)
|
||||
assert.match(services, /disposal: 'overseas-cargo-disposal-detail'/)
|
||||
assert.match(services, /class="warehouse-detail-error"/)
|
||||
@@ -40,6 +40,11 @@ test('opens warehouse service and return receipt details as dedicated protected
|
||||
assert.doesNotMatch(returns, /title="退货签收详情"/)
|
||||
})
|
||||
|
||||
test('returns to the labeling list after a labeling application is submitted', () => {
|
||||
assert.match(services, /ElMessage\.success\(`\$\{config\.value\.title\}提交成功`\)[\s\S]*?router\.push\(\{ name: 'overseas-labeling' \}\)/)
|
||||
assert.match(services, /uploadReturnRelabelFile\([\s\S]*?router\.push\(\{ name: 'overseas-labeling' \}\)/)
|
||||
})
|
||||
|
||||
test('keeps overseas warehouse writes behind operation-specific button permissions', () => {
|
||||
for (const permission of [
|
||||
'merchant:overseas-warehouse:relabel:create',
|
||||
|
||||
@@ -43,7 +43,7 @@ test('退货列表和详情使用客户可读仓库、状态与时间', () => {
|
||||
assert.match(view, /return warehouse \? warehouseLabel\(warehouse\) : '未关联'/)
|
||||
assert.match(view, /PENDING_RECEIPT: '待签收'/)
|
||||
assert.match(view, /PARTIALLY_RECEIVED: '部分签收'/)
|
||||
assert.match(view, /return needRelabel \? '已完成入库\/待换标' : '已完成入库'/)
|
||||
assert.match(view, /if \(status === 'COMPLETED'\) return '已完成入库'/)
|
||||
assert.match(view, /function formatDateTime\(value\?: string \| null\)/)
|
||||
assert.match(view, /\{\{ operationWarehouseLabel\(row\.overseasWarehouseId\) \}\}/)
|
||||
assert.match(view, /\{\{ statusLabel\(row\.status, row\.needRelabel\) \}\}/)
|
||||
@@ -77,9 +77,9 @@ test('退货签收详情使用独立路由和紧凑连续页面', () => {
|
||||
test('待审核退货可编辑,完成入库后从独立新增页发起贴标', () => {
|
||||
assert.match(view, /row\.status === 'PENDING_REVIEW'.*key: 'edit'/)
|
||||
assert.match(view, /await updateReturnOrder\(editingReturnNo\.value, payload\)/)
|
||||
assert.match(view, /order\.status === 'COMPLETED' && order\.needRelabel === true && !order\.relabelNo/)
|
||||
assert.match(view, /order\.status === 'COMPLETED' && order\.needRelabel === true/)
|
||||
assert.match(view, /name: 'overseas-labeling-create'/)
|
||||
assert.match(view, /query: \{ returnNo: order\.returnNo, warehouseId: String\(order\.overseasWarehouseId\) \}/)
|
||||
assert.match(view, /prop: 'relabelNo', label: '换标单号'/)
|
||||
assert.match(view, /label: '换标单状态'/)
|
||||
assert.match(view, /v-for="application in row\.relabelApplications \|\| \[\]"/)
|
||||
assert.match(view, /relabelStatusLabel\(application\.status\)/)
|
||||
})
|
||||
|
||||
@@ -49,7 +49,7 @@ const tableColumns: DataTableColumn[] = [
|
||||
{ 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: 'receivedQuantity', label: '实收数量', width: 110 }, { key: 'status', label: '状态', slotName: 'status', width: 150 },
|
||||
{ prop: 'relabelNo', label: '换标单号', minWidth: 190 }, { key: 'relabelStatus', label: '换标单状态', slotName: 'relabelStatus', width: 130 },
|
||||
{ key: 'relabelApplications', label: '换标子单', slotName: 'relabelApplications', minWidth: 340 },
|
||||
{ key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 180 },
|
||||
]
|
||||
const itemColumns: DataTableColumn[] = [
|
||||
@@ -191,15 +191,15 @@ async function submit() {
|
||||
} finally { creating.value = false }
|
||||
}
|
||||
function statusType(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? 'warning' : 'success'
|
||||
if (status === 'COMPLETED') return 'success'
|
||||
return ({ PENDING_REVIEW: 'warning', PENDING_RECEIPT: 'primary', PARTIALLY_RECEIVED: 'warning', REJECTED: 'danger', CANCELLED: 'info' } as Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'info'>)[status] || 'info'
|
||||
}
|
||||
function statusClass(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? 'return-status--relabel' : 'return-status--completed'
|
||||
if (status === 'COMPLETED') return 'return-status--completed'
|
||||
return `return-status--${status.toLowerCase().replaceAll('_', '-')}`
|
||||
}
|
||||
function statusLabel(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? '已完成入库/待换标' : '已完成入库'
|
||||
if (status === 'COMPLETED') return '已完成入库'
|
||||
return ({ PENDING_REVIEW: '待审核', PENDING_RECEIPT: '待签收', PARTIALLY_RECEIVED: '部分签收', REJECTED: '已驳回', CANCELLED: '已取消' } as Record<string, string>)[status] ?? (status || '未知状态')
|
||||
}
|
||||
function relabelStatusType(status?: string) {
|
||||
@@ -265,7 +265,7 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<template #warehouse="{ row }">{{ operationWarehouseLabel(row.overseasWarehouseId) }}</template>
|
||||
<template #relabel="{ row }"><el-tag v-if="row.needRelabel" type="warning" effect="light">需要换标</el-tag><span v-else>否</span></template>
|
||||
<template #status="{ row }"><el-tag :type="statusType(row.status, row.needRelabel)" :class="statusClass(row.status, row.needRelabel)" effect="light">{{ statusLabel(row.status, row.needRelabel) }}</el-tag></template>
|
||||
<template #relabelStatus="{ row }"><el-tag v-if="row.relabelStatus" :type="relabelStatusType(row.relabelStatus)" effect="light">{{ relabelStatusLabel(row.relabelStatus) }}</el-tag><span v-else>—</span></template>
|
||||
<template #relabelApplications="{ row }"><div v-for="application in row.relabelApplications || []" :key="application.relabelNo" class="relabel-suborder"><span>{{ application.relabelNo }}</span><el-tag :type="relabelStatusType(application.status)" effect="light">{{ relabelStatusLabel(application.status) }}</el-tag><span>{{ application.quantity }} 件</span></div><span v-if="!row.relabelApplications?.length">—</span></template>
|
||||
<template #createdAt="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
@@ -304,8 +304,6 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<div><span>实收 / 预报</span><strong>{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}</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.relabelNo || '—' }}</strong></div>
|
||||
<div><span>换标单状态</span><strong>{{ relabelStatusLabel(selected.order.relabelStatus) }}</strong></div>
|
||||
<div><span>客户参考号</span><strong>{{ selected.order.customerReferenceNo || '—' }}</strong></div>
|
||||
<div><span>海外仓联系人</span><strong>{{ selected.order.warehouseContactName || '—' }}</strong></div>
|
||||
<div><span>联系电话</span><strong>{{ selected.order.warehouseContactPhone || '—' }}</strong></div>
|
||||
@@ -313,6 +311,14 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
</div>
|
||||
<div v-if="selected.remark" class="return-detail-remark"><span>备注</span><strong>{{ selected.remark }}</strong></div>
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>换标子单({{ selected.order.relabelApplications?.length || 0 }})</h3>
|
||||
<el-table :data="selected.order.relabelApplications || []" empty-text="暂无换标申请">
|
||||
<el-table-column prop="relabelNo" label="换标单号" min-width="220" />
|
||||
<el-table-column label="状态" min-width="130"><template #default="{ row }"><el-tag :type="relabelStatusType(row.status)">{{ relabelStatusLabel(row.status) }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="quantity" label="本次换标数量(件)" min-width="160" />
|
||||
</el-table>
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>预报与实收</h3>
|
||||
<DataTable :data="selected.items" :columns="itemColumns" row-key="skuCode" empty-text="暂无 SKU 明细" />
|
||||
@@ -356,6 +362,7 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.relabel-suborder { display: flex; align-items: center; gap: 8px; min-height: 30px; white-space: nowrap; }
|
||||
.return-orders-page { display:grid; gap:12px; } .return-create-form { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:20px; } .return-form-wide { grid-column:1 / -1; } .items-title { display:flex; justify-content:space-between; align-items:center; margin:16px 0 4px; font-weight:600; } .items-hint { margin-bottom:10px; color:var(--el-text-color-secondary); font-size:12px; } h3 { margin:24px 0 10px; font-size:16px; } h4 { margin:16px 0 8px; font-size:14px; } .remark { white-space:pre-wrap; } .warehouse-error, .duplicate-sku-error { margin-top:6px; color:var(--el-color-danger); font-size:12px; } :deep(.duplicate-sku-input .el-input__wrapper) { box-shadow:0 0 0 1px var(--el-color-danger) inset; } :deep(.el-dialog .el-select) { width:100%; }
|
||||
.return-create-surface { overflow:hidden; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; }
|
||||
.return-create-header { display:flex; min-height:68px; align-items:center; justify-content:space-between; gap:20px; padding:12px 20px; border-bottom:1px solid var(--client-border); }
|
||||
|
||||
@@ -179,17 +179,21 @@ function inventoryQuantity(value: number | string | null | undefined) {
|
||||
return Number.isFinite(quantity) && quantity > 0 ? quantity : 0
|
||||
}
|
||||
const relabelInboundOptions = computed(() => {
|
||||
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件' }>()
|
||||
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; relabelledPackages: number; quantityUnit: '箱' | '件' }>()
|
||||
for (const candidate of relabelCandidates.value) {
|
||||
const returnOrderNo = candidate.inboundOrderNo || String(candidate.returnCartonId || '')
|
||||
const value = candidate.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(candidate.inboundOrderId)
|
||||
const current = grouped.get(value)
|
||||
if (current) current.availablePackages += inventoryQuantity(candidate.availablePackages)
|
||||
if (current) {
|
||||
current.availablePackages += inventoryQuantity(candidate.availablePackages)
|
||||
current.relabelledPackages += inventoryQuantity(candidate.relabelledPackages)
|
||||
}
|
||||
else grouped.set(value, {
|
||||
value,
|
||||
inboundOrderNo: candidate.sourceType === 'RETURN_CARTON' ? `退货单 ${candidate.inboundOrderNo || '—'}` : (candidate.inboundOrderNo || '—'),
|
||||
inboundCode: candidate.sourceType === 'RETURN_CARTON' ? '已签收退货库存' : (candidate.inboundCode || '—'),
|
||||
availablePackages: inventoryQuantity(candidate.availablePackages),
|
||||
relabelledPackages: inventoryQuantity(candidate.relabelledPackages),
|
||||
quantityUnit: candidate.sourceType === 'RETURN_CARTON' ? '件' : '箱',
|
||||
})
|
||||
}
|
||||
@@ -1012,14 +1016,14 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
|
||||
if (skuTotalFile.value) await uploadRelabelFile(created.application.relabelNo, 'SKU_TOTAL', skuTotalFile.value)
|
||||
if (shippingLabelFile.value) await uploadRelabelShippingLabel(created.application.relabelNo, shippingLabelFile.value)
|
||||
} catch (error) {
|
||||
// Creation already committed: leave the creation form even if the detail reload fails.
|
||||
if (props.createPage) await router.push({ name: 'overseas-labeling-detail', params: { detailNo: created.application.relabelNo } })
|
||||
// Creation already committed: return to the list even when an attachment still needs retrying.
|
||||
if (props.createPage) await router.push({ name: 'overseas-labeling' })
|
||||
else { mode.value = 'list'; await load(); await openDetail({ '0': created.application.relabelNo }) }
|
||||
await ElMessageBox.alert(`申请 ${created.application.relabelNo} 已创建,部分附件上传失败:${error instanceof Error ? error.message : '上传失败'}。请在原单详情补传失败附件,不要重复下单。`, '申请已创建,附件待补传', { confirmButtonText: '知道了' })
|
||||
return
|
||||
}
|
||||
ElMessage.success(`${config.value.title}提交成功`)
|
||||
await router.push({ name: 'overseas-labeling-detail', params: { detailNo: created.application.relabelNo } })
|
||||
await router.push({ name: 'overseas-labeling' })
|
||||
return
|
||||
} else if (props.service === 'disposal') {
|
||||
const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => {
|
||||
@@ -1346,7 +1350,14 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
const current = grouped.get(key)
|
||||
const quantity = inventoryQuantity(item.quantity)
|
||||
if (current) current.availableQuantity += quantity
|
||||
else grouped.set(key, { skuCode, productNameZh: candidate.skuCode?.trim().toUpperCase() === key ? (candidate.productNameZh || '') : '', availableQuantity: quantity, selected: false, quantity })
|
||||
else grouped.set(key, {
|
||||
skuCode,
|
||||
productNameZh: candidate.skuCode?.trim().toUpperCase() === key ? (candidate.productNameZh || '') : '',
|
||||
availableQuantity: quantity,
|
||||
selected: false,
|
||||
// 可贴库存和本次申请数量是两个概念;重复换标时默认不应自动带入上次/首个箱子的数量。
|
||||
quantity: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
returnRelabelSkuRows.value = [...grouped.values()]
|
||||
@@ -1399,7 +1410,7 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
<label v-if="props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<label v-else-if="props.service === 'disposal'"><span>进仓编号</span><el-input v-model="form.inboundNo" readonly /></label>
|
||||
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></label>
|
||||
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无可用库存" placeholder="请选择当前仍有可用库存的入仓单或退货单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|${option.inboundCode}|可用 ${option.availablePackages} ${option.quantityUnit}`" :value="option.value" /></el-select></label>
|
||||
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无可用库存" placeholder="请选择当前仍有可用库存的入仓单或退货单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|${option.inboundCode}|可用 ${option.availablePackages} ${option.quantityUnit}${option.relabelledPackages > 0 ? `|已换标 ${option.relabelledPackages} ${option.quantityUnit}` : ''}`" :value="option.value" /></el-select></label>
|
||||
<label v-if="props.service === 'transfer'"><span>库存转运来源(可选)</span><el-select v-model="selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="不选则按新货转运;选择后免 PDA 测量" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint">库存转运可直接进入目的仓、换标和排车流程,PDA 可拆托、重打或同目的仓合托。</small></label>
|
||||
<label v-if="props.service === 'labeling' && (isReturnRelabelSource || labelingMode !== 'sku')"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>入仓方式</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group></div>
|
||||
|
||||
@@ -251,6 +251,16 @@ test('贴标申请详情使用紧凑订单概览并中文展示换标状态', ()
|
||||
assert.match(view, /WAIT_PROCESSING: '待仓库处理'/)
|
||||
assert.match(view, /relabelFulfillmentLabel\(selectedRelabelDetail\?\.application\.fulfillmentStatus\)/)
|
||||
assert.match(view, />\.warehouse-detail-header\+\.warehouse-card \.warehouse-detail-grid\{grid-template-columns:repeat\(4,minmax\(180px,1fr\)\)/)
|
||||
test('仓储转运详情按接口具名字段展示订单概览', () => {
|
||||
const overview = view.split('<h3>订单概览</h3>')[1]?.split('warehouse-transfer-cargo-card')[0] || ''
|
||||
assert.match(overview, /v-if="props\.service === 'transfer'"/)
|
||||
assert.match(overview, /selectedDetail\?\.transferNo/)
|
||||
assert.match(overview, /selectedDetail\?\.packageCount \?\? '—'/)
|
||||
assert.match(overview, /warehouseDisplay\(selectedDetail\?\.operatingWarehouseId\)/)
|
||||
assert.match(overview, /selectedDetail\?\.destinationWarehouseName \|\| warehouseDisplay\(selectedDetail\?\.destinationWarehouseCode \|\| selectedDetail\?\.fbaWarehouseCode\)/)
|
||||
assert.match(overview, /deliveryMethodLabel\(selectedDetail\?\.deliveryMethod \|\| ''\)/)
|
||||
assert.match(overview, /statusLabel\(selectedDetail\?\.status \|\| ''\)/)
|
||||
assert.match(overview, /formatDateTime\(selectedDetail\?\.createdAt\)/)
|
||||
})
|
||||
|
||||
test('PDA 打托完成后客户可从列表或详情确认并查看实测数据', () => {
|
||||
|
||||
Reference in New Issue
Block a user